Compare commits

...
Sign in to create a new pull request.

22 commits

Author SHA1 Message Date
Max Brunsfeld
060966db81 zed 0.88.6 2023-06-01 11:08:56 -07:00
Mikayla Maki
3bb2f59dc0 Make settings store handle no user settings (#2550)
This fixes the crash users have been reporting with the theme selector
2023-06-01 11:06:59 -07:00
Max Brunsfeld
87cbbe8c52 zed 0.88.5 2023-06-01 09:16:13 -07:00
Julia
b5d25a8e00 Avoid blocking forever on startup if config files do not exist (#2549)
The files will still get created if the user opens their settings and
saves, otherwise everything will transparently work

Release Notes:

- Fixed an issue where a missing settings file would cause a hang on
startup
([#1590](https://github.com/zed-industries/community/issues/1590)).
2023-06-01 09:15:43 -07:00
Kirill Bulatov
5a14acca77 Remove wrong assertion 2023-06-01 17:38:55 +03:00
Antonio Scandurra
4b88d13489 zed 0.88.4 2023-06-01 14:42:49 +02:00
Julia
d27df5ac50 TextStyle::default() ask system for known existing font family (#2542)
Rather than assuming a specific family exists, try a set of specific
names and if they fail, just grab any old font that the system reports
as existing

Closes
https://linear.app/zed-industries/issue/Z-445/thread-main-panicked-at-called-resultunwrap-on-an-err-value-could-not

Release Notes:

* Fixed crash that could happen if system did not have a specific
fallback font
2023-06-01 14:42:13 +02:00
Max Brunsfeld
fb2a8474db v0.88.x stable 2023-05-31 14:44:29 -07:00
Mikayla Maki
2e66ab296a Add a dock-keybinding-changed one-time notification (#2546)
This code is intended to be removed at a later date.

Release Notes:

- N/A
2023-05-31 14:43:04 -07:00
Max Brunsfeld
ce1e56fd61 zed 0.88.3 2023-05-31 13:21:03 -07:00
Max Brunsfeld
7e33865b62 Fix more issues with panels and zoom (#2545)
Release Notes:

* Fixed the behavior of panel buttons when their panel is open. Show the
key binding for closing the containing dock (preview only).
* Improved the styling of zoomed panels and panes, giving them a
stronger border, and color the zoom button with an "active" style
(preview only).
* Improved the stability of panels' zoom state. Close their dock instead
of resetting their zoom state when dismissing panels to reveal other
items (preview only).
2023-05-31 13:20:10 -07:00
Mikayla Maki
232894d3bc
zed 0.88.2 2023-05-30 16:44:43 -07:00
Mikayla Maki
fc0bfd75ad
Fix usability issues with new panel system. (#2544)
This PR updates the dock key bindings according to the following model:

There are three bits: 
Visible: Opened / closed.
Focus: Panel focused / center focused.
Zoom: Zoomed / Not zoomed.

Each of these variables is 'sticky' in that they won't effect each other
unless they need to. 'Zooming' a panel conceptually merges the visible
and focus bits.

cmd-shift-j/b/r have all been removed.

cmd-j/b/r have been updated to mean 'toggle visibility of a certain
dock', firing them should *always* reveal the panel to you (where you
last left it), or hide it, without moving focus (unless the focused
element is invisible). This means that, when the terminal panel is
zoomed, cmd-j has the same effect as ctrl-`

ctrl-` and cmd-shift-e now toggle a panel's focus, without updating the
zoom state of a panel. Toggling the focus of a zoomed panel causes it to
automatically hide itself, without losing the zoom bit.

When focused or made visible, panels which cannot be zoomed
automatically unzoom everything else so as to preserve user intent of
'show me this panel' and 'everything stays where it is if I don't take
an action'

Release Notes:

- cmd-shift-j/b/r have been removed.  (preview only)
- cmd-j/b/r unconditionally show or hide their associated dock,
respecting zoom settings. (preview only)
- ctrl-` and cmd-shift-e now retain zoom state. (preview only)
- Fixed a bug where terminal dock tab would always be in the active
state (preview only)
- Fixed a bug where terminals would not always open in the terminal
panel
- Changed the look of zoomed panels to fill more of the screen (preview
only)
2023-05-30 16:43:22 -07:00
Joseph T. Lyons
8a3a0245e0 Scrape email from feedback editor (#2543)
Add release note lines here:

N/A
2023-05-30 16:04:09 -04:00
Kirill Bulatov
25f79da4f7 Improves project search panel shortcut handling (#2536)
* ESC (project_search::ToggleFocus) toggles focus from include/exclude
fields to the editor

* Cmd+Shift+F (workspace::NewSearch) can be triggered from the editor,
and moves focus to the query editor

Release Notes:

* Improved project search panel shortcut handling, allowing more actions
to trigger from panel elements
2023-05-29 17:08:29 +03:00
Kirill Bulatov
81a21e3353 Display external paths in history (#2534)
Fixes
https://linear.app/zed-industries/issue/Z-1793/file-finder-external-recent-paths-are-not-rendered-properly

Long paths get trimmed, but same do many of our other elements, such as
type definitions, so I think it's ok for now:

![image](https://github.com/zed-industries/zed/assets/2690773/b8b6588d-6d6b-42db-9085-c741a40b7adb)

Also, we seem to do a lot of odd diagnostics handling on every external
stdlib file opened:

![image](https://github.com/zed-industries/zed/assets/2690773/cd82c54e-7849-46fe-a01c-79bfc5402b7b)
Other external files also emit similar messages, but not that much.
@\mikayla-maki mentioned, that this was happening before, so can be
fixed separately.
The PR adds path printing to these logs.

Release Notes:

* Fixed external files not being displayed properly in the recently
opened list in the file finder panel
2023-05-29 12:37:28 +03:00
Joseph Lyons
23a1b563bf zed 0.88.1 2023-05-25 11:06:34 -04:00
Antonio Scandurra
4d77efd048 Prevent bottom dock from becoming too large or too small (#2529)
Fixes
https://linear.app/zed-industries/issue/Z-1834/bottom-dock-can-be-dragged-down-far-enough-such-that-the-tab-bar-is

Release Notes:

- Improved the resize experience for the bottom dock, preventing it from
becoming too large or too small. (preview-only)
2023-05-25 10:16:39 -04:00
Antonio Scandurra
ae04b12e1a Restore the ability to search terminals in the terminal panel (#2528)
Fixes
https://linear.app/zed-industries/issue/Z-1812/find-in-terminal-panel-is-broken

![CleanShot 2023-05-25 at 15 18
01@2x](https://github.com/zed-industries/zed/assets/482957/ddb33b61-d253-4de1-961f-14b24aaa3e46)

This also removes navigation controls from the terminal panel, given
that terminals don't make use of that feature anyway. When the toolbar
is empty, we'll avoid showing it altogether.

![CleanShot 2023-05-25 at 15 17
26@2x](https://github.com/zed-industries/zed/assets/482957/52419f2c-bca0-494a-a9b3-88e183b4c12f)


Release Notes:

- Fixed a regression that was preventing the terminal panel from being
searched.
2023-05-25 10:16:29 -04:00
Antonio Scandurra
e5112e13a3 Prevent pane from being erroneously zoomed when toggling the outline pane (#2527)
Fixes
https://linear.app/zed-industries/issue/Z-1818/toggling-the-outline-pane-causes-the-pane-to-zoom

Add release note lines here:

- Fixed a bug that could cause panes to be erroneously zoomed when
toggling modals. (preview-only)
2023-05-25 10:16:16 -04:00
Nathan Sobo
ee63de0022 Introduce the ability to talk to GPT-4 within buffers on preview and dev (#2524)
This adds basic ability to interact with OpenAI inside a buffer.

Release Notes:

* If`OPENAI_API_KEY` is defined in your environment, you can now run the
`ai: assist` command to pass the text of your current buffer to GPT-4.
If you're editing a file with a `.zmd` extension, you can also invoke
the model with `cmd-enter.` (preview-only)
2023-05-25 10:16:06 -04:00
Joseph Lyons
5bff8f2a54 v0.88.x preview 2023-05-24 13:21:38 -04:00
49 changed files with 2313 additions and 733 deletions

21
Cargo.lock generated
View file

@ -95,6 +95,22 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "ai"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"collections",
"editor",
"futures 0.3.28",
"gpui",
"isahc",
"serde",
"serde_json",
"util",
]
[[package]] [[package]]
name = "alacritty_config" name = "alacritty_config"
version = "0.1.1-dev" version = "0.1.1-dev"
@ -2220,6 +2236,7 @@ dependencies = [
"log", "log",
"postage", "postage",
"project", "project",
"regex",
"search", "search",
"serde", "serde",
"serde_derive", "serde_derive",
@ -6793,6 +6810,7 @@ dependencies = [
"procinfo", "procinfo",
"project", "project",
"rand 0.8.5", "rand 0.8.5",
"search",
"serde", "serde",
"serde_derive", "serde_derive",
"settings", "settings",
@ -8759,9 +8777,10 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.88.0" version = "0.88.6"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"ai",
"anyhow", "anyhow",
"assets", "assets",
"async-compression", "async-compression",

View file

@ -1,6 +1,7 @@
[workspace] [workspace]
members = [ members = [
"crates/activity_indicator", "crates/activity_indicator",
"crates/ai",
"crates/assets", "crates/assets",
"crates/auto_update", "crates/auto_update",
"crates/breadcrumbs", "crates/breadcrumbs",
@ -78,6 +79,8 @@ ctor = { version = "0.1" }
env_logger = { version = "0.9" } env_logger = { version = "0.9" }
futures = { version = "0.3" } futures = { version = "0.3" }
globset = { version = "0.4" } globset = { version = "0.4" }
indoc = "1"
isahc = "1.7.2"
lazy_static = { version = "1.4.0" } lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" } ordered-float = { version = "2.1.1" }

View file

@ -0,0 +1,18 @@
You are #zed, a language model representing the collective understanding of an open source project called Zed. When a new human visits you, they'll send you their profile. You'll respond with an introduction tailored to their situation. For example, a new user might see something like this:
Welcome to Zed! Zed is an innovative, open-source platform designed to enhance team communication and collaboration. At the heart of Zed are *contexts*, which create a dynamic digital representation of shared mental models. Contexts offer personalized starting points and the flexibility to edit and explore, enabling teams to align knowledge, streamline communication, and improve overall performance.
As the #zed model, I'm happy to answer any questions. In fact, I will improve as a result of you doing so!
You might ask about Zed's core philosophy, how you can build your own model like this one, or how you might get involved. Zed's open source!
> [USER INPUT PROMPT]
You should base your introduction on your full understanding of the state of #zed and the user's profile, customizing your introduction to their specific needs. Don't welcome them to Zed if they've been using Zed for 2 days. If they're returning after a while, welcome them back.
User input begins on a line starting with >.
Your output begins on a line starting with <.
Don't apologize ever.
Never say "I apologize".
Use simple language and don't flatter the users. Spend your tokens on valuable information.

View file

@ -188,6 +188,12 @@
"alt-[": "copilot::PreviousSuggestion" "alt-[": "copilot::PreviousSuggestion"
} }
}, },
{
"context": "Editor && extension == zmd",
"bindings": {
"cmd-enter": "ai::Assist"
}
},
{ {
"context": "Editor && mode == auto_height", "context": "Editor && mode == auto_height",
"bindings": { "bindings": {
@ -367,30 +373,9 @@
"workspace::ActivatePane", "workspace::ActivatePane",
8 8
], ],
"cmd-b": [ "cmd-b": "workspace::ToggleLeftDock",
"workspace::ToggleLeftDock", "cmd-r": "workspace::ToggleRightDock",
{ "focus": true } "cmd-j": "workspace::ToggleBottomDock",
],
"cmd-shift-b": [
"workspace::ToggleLeftDock",
{ "focus": false }
],
"cmd-r": [
"workspace::ToggleRightDock",
{ "focus": true }
],
"cmd-shift-r": [
"workspace::ToggleRightDock",
{ "focus": false }
],
"cmd-j": [
"workspace::ToggleBottomDock",
{ "focus": true }
],
"cmd-shift-j": [
"workspace::ToggleBottomDock",
{ "focus": false }
],
"cmd-shift-f": "workspace::NewSearch", "cmd-shift-f": "workspace::NewSearch",
"cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap", "cmd-k cmd-s": "zed::OpenKeymap",

25
crates/ai/Cargo.toml Normal file
View 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
View 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
View 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
View 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,
))
}
}

View file

@ -19,7 +19,7 @@ theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
util = { path = "../util" } util = { path = "../util" }
anyhow.workspace = true anyhow.workspace = true
isahc = "1.7" isahc.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
log.workspace = true log.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -76,7 +76,7 @@ workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true ctor.workspace = true
env_logger.workspace = true env_logger.workspace = true
indoc = "1.0.4" indoc.workspace = true
util = { path = "../util" } util = { path = "../util" }
lazy_static.workspace = true lazy_static.workspace = true
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] } sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }

View file

@ -18,7 +18,7 @@ sqlez = { path = "../sqlez" }
sqlez_macros = { path = "../sqlez_macros" } sqlez_macros = { path = "../sqlez_macros" }
util = { path = "../util" } util = { path = "../util" }
anyhow.workspace = true anyhow.workspace = true
indoc = "1.0.4" indoc.workspace = true
async-trait.workspace = true async-trait.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
log.workspace = true log.workspace = true

View file

@ -7330,7 +7330,7 @@ impl View for Editor {
false false
} }
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
Self::reset_to_default_keymap_context(keymap); Self::reset_to_default_keymap_context(keymap);
let mode = match self.mode { let mode = match self.mode {
EditorMode::SingleLine => "single_line", EditorMode::SingleLine => "single_line",
@ -7346,10 +7346,18 @@ impl View for Editor {
Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"), Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
None => {} None => {}
} }
for layer in self.keymap_context_layers.values() { for layer in self.keymap_context_layers.values() {
keymap.extend(layer); keymap.extend(layer);
} }
if let Some(extension) = self
.buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
{
keymap.add_key("extension", extension.to_string());
}
} }
fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> { fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {

View file

@ -16,6 +16,7 @@ editor = { path = "../editor" }
language = { path = "../language" } language = { path = "../language" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
regex.workspace = true
search = { path = "../search" } search = { path = "../search" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
@ -27,7 +28,7 @@ futures.workspace = true
anyhow.workspace = true anyhow.workspace = true
smallvec.workspace = true smallvec.workspace = true
human_bytes = "0.4.1" human_bytes = "0.4.1"
isahc = "1.7" isahc.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
postage.workspace = true postage.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -14,6 +14,7 @@ use isahc::Request;
use language::Buffer; use language::Buffer;
use postage::prelude::Stream; use postage::prelude::Stream;
use project::Project; use project::Project;
use regex::Regex;
use serde::Serialize; use serde::Serialize;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
@ -46,6 +47,7 @@ pub fn init(cx: &mut AppContext) {
#[derive(Serialize)] #[derive(Serialize)]
struct FeedbackRequestBody<'a> { struct FeedbackRequestBody<'a> {
feedback_text: &'a str, feedback_text: &'a str,
email: Option<String>,
metrics_id: Option<Arc<str>>, metrics_id: Option<Arc<str>>,
installation_id: Option<Arc<str>>, installation_id: Option<Arc<str>>,
system_specs: SystemSpecs, system_specs: SystemSpecs,
@ -157,8 +159,18 @@ impl FeedbackEditor {
let is_staff = telemetry.is_staff(); let is_staff = telemetry.is_staff();
let http_client = zed_client.http_client(); let http_client = zed_client.http_client();
let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
let emails: Vec<&str> = re
.captures_iter(feedback_text)
.map(|capture| capture.get(0).unwrap().as_str())
.collect();
let email = emails.first().map(|e| e.to_string());
let request = FeedbackRequestBody { let request = FeedbackRequestBody {
feedback_text: &feedback_text, feedback_text: &feedback_text,
email,
metrics_id, metrics_id,
installation_id, installation_id,
system_specs, system_specs,

View file

@ -34,7 +34,7 @@ impl View for FeedbackInfoText {
Flex::row() Flex::row()
.with_child( .with_child(
Text::new( Text::new(
"We read whatever you submit here. For issues and discussions, visit the ", "Share your feedback. Include your email for replies. For issues and discussions, visit the ",
theme.feedback.info_text_default.text.clone(), theme.feedback.info_text_default.text.clone(),
) )
.with_soft_wrap(false) .with_soft_wrap(false)
@ -60,7 +60,7 @@ impl View for FeedbackInfoText {
}), }),
) )
.with_child( .with_child(
Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone()) Text::new(".", theme.feedback.info_text_default.text.clone())
.with_soft_wrap(false) .with_soft_wrap(false)
.aligned(), .aligned(),
) )

View file

@ -6,7 +6,7 @@ use gpui::{
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{ use std::{
path::Path, path::{Path, PathBuf},
sync::{ sync::{
atomic::{self, AtomicBool}, atomic::{self, AtomicBool},
Arc, Arc,
@ -25,11 +25,57 @@ pub struct FileFinderDelegate {
latest_search_id: usize, latest_search_id: usize,
latest_search_did_cancel: bool, latest_search_did_cancel: bool,
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>, latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
currently_opened_path: Option<ProjectPath>, currently_opened_path: Option<FoundPath>,
matches: Vec<PathMatch>, matches: Matches,
selected: Option<(usize, Arc<Path>)>, selected_index: Option<usize>,
cancel_flag: Arc<AtomicBool>, cancel_flag: Arc<AtomicBool>,
history_items: Vec<ProjectPath>, history_items: Vec<FoundPath>,
}
#[derive(Debug)]
enum Matches {
History(Vec<FoundPath>),
Search(Vec<PathMatch>),
}
#[derive(Debug)]
enum Match<'a> {
History(&'a FoundPath),
Search(&'a PathMatch),
}
impl Matches {
fn len(&self) -> usize {
match self {
Self::History(items) => items.len(),
Self::Search(items) => items.len(),
}
}
fn get(&self, index: usize) -> Option<Match<'_>> {
match self {
Self::History(items) => items.get(index).map(Match::History),
Self::Search(items) => items.get(index).map(Match::Search),
}
}
}
impl Default for Matches {
fn default() -> Self {
Self::History(Vec::new())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct FoundPath {
project: ProjectPath,
absolute: Option<PathBuf>,
}
impl FoundPath {
fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
Self { project, absolute }
}
} }
actions!(file_finder, [Toggle]); actions!(file_finder, [Toggle]);
@ -43,10 +89,41 @@ const MAX_RECENT_SELECTIONS: usize = 20;
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| { workspace.toggle_modal(cx, |workspace, cx| {
let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx); let project = workspace.project().read(cx);
let currently_opened_path = workspace let currently_opened_path = workspace
.active_item(cx) .active_item(cx)
.and_then(|item| item.project_path(cx)); .and_then(|item| item.project_path(cx))
.map(|project_path| {
let abs_path = project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
FoundPath::new(project_path, abs_path)
});
// if exists, bubble the currently opened path to the top
let history_items = currently_opened_path
.clone()
.into_iter()
.chain(
workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.filter(|(history_path, _)| {
Some(history_path)
!= currently_opened_path
.as_ref()
.map(|found_path| &found_path.project)
})
.filter(|(_, history_abs_path)| {
history_abs_path.as_ref()
!= currently_opened_path
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
let project = workspace.project().clone(); let project = workspace.project().clone();
let workspace = cx.handle().downgrade(); let workspace = cx.handle().downgrade();
@ -87,37 +164,11 @@ impl FileSearchQuery {
} }
impl FileFinderDelegate { impl FileFinderDelegate {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) { fn new(
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let path_positions = path_match.positions.clone();
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
- file_name.chars().count();
let file_name_positions = path_positions
.iter()
.filter_map(|pos| {
if pos >= &file_name_start {
Some(pos - file_name_start)
} else {
None
}
})
.collect();
(file_name, file_name_positions, full_path, path_positions)
}
pub fn new(
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
currently_opened_path: Option<ProjectPath>, currently_opened_path: Option<FoundPath>,
history_items: Vec<ProjectPath>, history_items: Vec<FoundPath>,
cx: &mut ViewContext<FileFinder>, cx: &mut ViewContext<FileFinder>,
) -> Self { ) -> Self {
cx.observe(&project, |picker, _, cx| { cx.observe(&project, |picker, _, cx| {
@ -132,8 +183,8 @@ impl FileFinderDelegate {
latest_search_did_cancel: false, latest_search_did_cancel: false,
latest_search_query: None, latest_search_query: None,
currently_opened_path, currently_opened_path,
matches: Vec::new(), matches: Matches::default(),
selected: None, selected_index: None,
cancel_flag: Arc::new(AtomicBool::new(false)), cancel_flag: Arc::new(AtomicBool::new(false)),
history_items, history_items,
} }
@ -147,7 +198,7 @@ impl FileFinderDelegate {
let relative_to = self let relative_to = self
.currently_opened_path .currently_opened_path
.as_ref() .as_ref()
.map(|project_path| Arc::clone(&project_path.path)); .map(|found_path| Arc::clone(&found_path.project.path));
let worktrees = self let worktrees = self
.project .project
.read(cx) .read(cx)
@ -188,13 +239,13 @@ impl FileFinderDelegate {
.update(&mut cx, |picker, cx| { .update(&mut cx, |picker, cx| {
picker picker
.delegate_mut() .delegate_mut()
.set_matches(search_id, did_cancel, query, matches, cx) .set_search_matches(search_id, did_cancel, query, matches, cx)
}) })
.log_err(); .log_err();
}) })
} }
fn set_matches( fn set_search_matches(
&mut self, &mut self,
search_id: usize, search_id: usize,
did_cancel: bool, did_cancel: bool,
@ -211,15 +262,126 @@ impl FileFinderDelegate {
.as_ref() .as_ref()
.map(|query| query.path_like.path_query()) .map(|query| query.path_like.path_query())
{ {
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); match &mut self.matches {
Matches::History(_) => self.matches = Matches::Search(matches),
Matches::Search(search_matches) => {
util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
b.cmp(a)
})
}
}
} else { } else {
self.matches = matches; self.matches = Matches::Search(matches);
} }
self.latest_search_query = Some(query); self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel; self.latest_search_did_cancel = did_cancel;
cx.notify(); cx.notify();
} }
} }
fn labels_for_match(
&self,
path_match: Match,
cx: &AppContext,
ix: usize,
) -> (String, Vec<usize>, String, Vec<usize>) {
let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
Match::History(found_path) => {
let worktree_id = found_path.project.worktree_id;
let project_relative_path = &found_path.project.path;
let has_worktree = self
.project
.read(cx)
.worktree_for_id(worktree_id, cx)
.is_some();
if !has_worktree {
if let Some(absolute_path) = &found_path.absolute {
return (
absolute_path
.file_name()
.map_or_else(
|| project_relative_path.to_string_lossy(),
|file_name| file_name.to_string_lossy(),
)
.to_string(),
Vec::new(),
absolute_path.to_string_lossy().to_string(),
Vec::new(),
);
}
}
let mut path = Arc::clone(project_relative_path);
if project_relative_path.as_ref() == Path::new("") {
if let Some(absolute_path) = &found_path.absolute {
path = Arc::from(absolute_path.as_path());
}
}
self.labels_for_path_match(&PathMatch {
score: ix as f64,
positions: Vec::new(),
worktree_id: worktree_id.to_usize(),
path,
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
})
}
Match::Search(path_match) => self.labels_for_path_match(path_match),
};
if file_name_positions.is_empty() {
if let Some(user_home_path) = std::env::var("HOME").ok() {
let user_home_path = user_home_path.trim();
if !user_home_path.is_empty() {
if (&full_path).starts_with(user_home_path) {
return (
file_name,
file_name_positions,
full_path.replace(user_home_path, "~"),
full_path_positions,
);
}
}
}
}
(
file_name,
file_name_positions,
full_path,
full_path_positions,
)
}
fn labels_for_path_match(
&self,
path_match: &PathMatch,
) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let path_positions = path_match.positions.clone();
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
- file_name.chars().count();
let file_name_positions = path_positions
.iter()
.filter_map(|pos| {
if pos >= &file_name_start {
Some(pos - file_name_start)
} else {
None
}
})
.collect();
(file_name, file_name_positions, full_path, path_positions)
}
} }
impl PickerDelegate for FileFinderDelegate { impl PickerDelegate for FileFinderDelegate {
@ -232,45 +394,35 @@ impl PickerDelegate for FileFinderDelegate {
} }
fn selected_index(&self) -> usize { fn selected_index(&self) -> usize {
if let Some(selected) = self.selected.as_ref() { self.selected_index.unwrap_or(0)
for (ix, path_match) in self.matches.iter().enumerate() {
if (path_match.worktree_id, path_match.path.as_ref())
== (selected.0, selected.1.as_ref())
{
return ix;
}
}
}
0
} }
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) { fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
let mat = &self.matches[ix]; self.selected_index = Some(ix);
self.selected = Some((mat.worktree_id, mat.path.clone()));
cx.notify(); cx.notify();
} }
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> { fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if raw_query.is_empty() { if raw_query.is_empty() {
let project = self.project.read(cx);
self.latest_search_id = post_inc(&mut self.search_count); self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear(); self.matches = Matches::History(
self.history_items
self.matches = self .iter()
.currently_opened_path .filter(|history_item| {
.iter() // if exists, bubble the currently opened path to the top project
.chain(self.history_items.iter().filter(|history_item| { .worktree_for_id(history_item.project.worktree_id, cx)
Some(*history_item) != self.currently_opened_path.as_ref() .is_some()
})) || (project.is_local()
.enumerate() && history_item
.map(|(i, history_item)| PathMatch { .absolute
score: i as f64, .as_ref()
positions: Vec::new(), .filter(|abs_path| abs_path.exists())
worktree_id: history_item.worktree_id.to_usize(), .is_some())
path: Arc::clone(&history_item.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
}) })
.collect(); .cloned()
.collect(),
);
cx.notify(); cx.notify();
Task::ready(()) Task::ready(())
} else { } else {
@ -293,16 +445,52 @@ impl PickerDelegate for FileFinderDelegate {
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) { fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
if let Some(m) = self.matches.get(self.selected_index()) { if let Some(m) = self.matches.get(self.selected_index()) {
if let Some(workspace) = self.workspace.upgrade(cx) { if let Some(workspace) = self.workspace.upgrade(cx) {
let project_path = ProjectPath { let open_task = workspace.update(cx, |workspace, cx| match m {
Match::History(history_match) => {
let worktree_id = history_match.project.worktree_id;
if workspace
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)
.is_some()
{
workspace.open_path(
ProjectPath {
worktree_id,
path: Arc::clone(&history_match.project.path),
},
None,
true,
cx,
)
} else {
match history_match.absolute.as_ref() {
Some(abs_path) => {
workspace.open_abs_path(abs_path.to_path_buf(), false, cx)
}
None => workspace.open_path(
ProjectPath {
worktree_id,
path: Arc::clone(&history_match.project.path),
},
None,
true,
cx,
),
}
}
}
Match::Search(m) => workspace.open_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id), worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(), path: m.path.clone(),
}; },
let open_task = workspace.update(cx, |workspace, cx| { None,
workspace.open_path(project_path.clone(), None, true, cx) true,
cx,
),
}); });
let workspace = workspace.downgrade();
let row = self let row = self
.latest_search_query .latest_search_query
.as_ref() .as_ref()
@ -333,6 +521,7 @@ impl PickerDelegate for FileFinderDelegate {
} }
} }
workspace workspace
.downgrade()
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx)) .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
.log_err(); .log_err();
@ -352,11 +541,14 @@ impl PickerDelegate for FileFinderDelegate {
selected: bool, selected: bool,
cx: &AppContext, cx: &AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let path_match = &self.matches[ix]; let path_match = self
.matches
.get(ix)
.expect("Invalid matches state: no element for index {ix}");
let theme = theme::current(cx); let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.style_for(mouse_state, selected);
let (file_name, file_name_positions, full_path, full_path_positions) = let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match); self.labels_for_match(path_match, cx, ix);
Flex::column() Flex::column()
.with_child( .with_child(
Label::new(file_name, style.label.clone()).with_highlights(file_name_positions), Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
@ -373,7 +565,7 @@ impl PickerDelegate for FileFinderDelegate {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{assert_eq, collections::HashMap, time::Duration}; use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
use super::*; use super::*;
use editor::Editor; use editor::Editor;
@ -649,12 +841,16 @@ mod tests {
finder.update(cx, |finder, cx| { finder.update(cx, |finder, cx| {
let delegate = finder.delegate_mut(); let delegate = finder.delegate_mut();
let matches = delegate.matches.clone(); let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
}
.clone();
// Simulate a search being cancelled after the time limit, // Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found. // returning only a subset of the matches that would have been found.
drop(delegate.spawn_search(query.clone(), cx)); drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches( delegate.set_search_matches(
delegate.latest_search_id, delegate.latest_search_id,
true, // did-cancel true, // did-cancel
query.clone(), query.clone(),
@ -664,7 +860,7 @@ mod tests {
// Simulate another cancellation. // Simulate another cancellation.
drop(delegate.spawn_search(query.clone(), cx)); drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches( delegate.set_search_matches(
delegate.latest_search_id, delegate.latest_search_id,
true, // did-cancel true, // did-cancel
query.clone(), query.clone(),
@ -672,7 +868,12 @@ mod tests {
cx, cx,
); );
assert_eq!(delegate.matches, matches[0..4]) match &delegate.matches {
Matches::Search(new_matches) => {
assert_eq!(new_matches.as_slice(), &matches[0..4])
}
_ => panic!("Search matches expected"),
};
}); });
} }
@ -772,10 +973,14 @@ mod tests {
cx.read(|cx| { cx.read(|cx| {
let finder = finder.read(cx); let finder = finder.read(cx);
let delegate = finder.delegate(); let delegate = finder.delegate();
assert_eq!(delegate.matches.len(), 1); let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
};
assert_eq!(matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) = let (file_name, file_name_positions, full_path, full_path_positions) =
delegate.labels_for_match(&delegate.matches[0]); delegate.labels_for_path_match(&matches[0]);
assert_eq!(file_name, "the-file"); assert_eq!(file_name, "the-file");
assert_eq!(file_name_positions, &[0, 1, 4]); assert_eq!(file_name_positions, &[0, 1, 4]);
assert_eq!(full_path, "the-file"); assert_eq!(full_path, "the-file");
@ -876,10 +1081,10 @@ mod tests {
// When workspace has an active item, sort items which are closer to that item // When workspace has an active item, sort items which are closer to that item
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier // so that one should be sorted earlier
let b_path = Some(ProjectPath { let b_path = Some(dummy_found_path(ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("/root/dir2/b.txt")), path: Arc::from(Path::new("/root/dir2/b.txt")),
}); }));
let (_, finder) = cx.add_window(|cx| { let (_, finder) = cx.add_window(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new( FileFinderDelegate::new(
@ -901,8 +1106,12 @@ mod tests {
finder.read_with(cx, |f, _| { finder.read_with(cx, |f, _| {
let delegate = f.delegate(); let delegate = f.delegate();
assert_eq!(delegate.matches[0].path.as_ref(), Path::new("dir2/a.txt")); let matches = match &delegate.matches {
assert_eq!(delegate.matches[1].path.as_ref(), Path::new("dir1/a.txt")); Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
};
assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
}); });
} }
@ -1012,10 +1221,13 @@ mod tests {
.await; .await;
assert_eq!( assert_eq!(
history_after_first, history_after_first,
vec![ProjectPath { vec![FoundPath::new(
ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/first.rs")), path: Arc::from(Path::new("test/first.rs")),
}], },
Some(PathBuf::from("/src/test/first.rs"))
)],
"Should show 1st opened item in the history when opening the 2nd item" "Should show 1st opened item in the history when opening the 2nd item"
); );
@ -1032,14 +1244,20 @@ mod tests {
assert_eq!( assert_eq!(
history_after_second, history_after_second,
vec![ vec![
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/second.rs")), path: Arc::from(Path::new("test/second.rs")),
}, },
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/first.rs")), path: Arc::from(Path::new("test/first.rs")),
}, },
Some(PathBuf::from("/src/test/first.rs"))
),
], ],
"Should show 1st and 2nd opened items in the history when opening the 3rd item. \ "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
2nd item should be the first in the history, as the last opened." 2nd item should be the first in the history, as the last opened."
@ -1058,18 +1276,27 @@ mod tests {
assert_eq!( assert_eq!(
history_after_third, history_after_third,
vec![ vec![
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/third.rs")), path: Arc::from(Path::new("test/third.rs")),
}, },
Some(PathBuf::from("/src/test/third.rs"))
),
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/second.rs")), path: Arc::from(Path::new("test/second.rs")),
}, },
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/first.rs")), path: Arc::from(Path::new("test/first.rs")),
}, },
Some(PathBuf::from("/src/test/first.rs"))
),
], ],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
3rd item should be the first in the history, as the last opened." 3rd item should be the first in the history, as the last opened."
@ -1088,24 +1315,162 @@ mod tests {
assert_eq!( assert_eq!(
history_after_second_again, history_after_second_again,
vec![ vec![
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/second.rs")), path: Arc::from(Path::new("test/second.rs")),
}, },
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/third.rs")), path: Arc::from(Path::new("test/third.rs")),
}, },
Some(PathBuf::from("/src/test/third.rs"))
),
FoundPath::new(
ProjectPath { ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("test/first.rs")), path: Arc::from(Path::new("test/first.rs")),
}, },
Some(PathBuf::from("/src/test/first.rs"))
),
], ],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
2nd item, as the last opened, 3rd item should go next as it was opened right before." 2nd item, as the last opened, 3rd item should go next as it was opened right before."
); );
} }
#[gpui::test]
async fn test_external_files_history(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
}
}),
)
.await;
app_state
.fs
.as_fake()
.insert_tree(
"/external-src",
json!({
"test": {
"third.rs": "// Third Rust file",
"fourth.rs": "// Fourth Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
cx.update(|cx| {
project.update(cx, |project, cx| {
project.find_or_create_local_worktree("/external-src", false, cx)
})
})
.detach();
deterministic.run_until_parked();
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1,);
WorktreeId::from_usize(worktrees[0].id())
});
workspace
.update(cx, |workspace, cx| {
workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
})
.detach();
deterministic.run_until_parked();
let external_worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(
worktrees.len(),
2,
"External file should get opened in a new worktree"
);
WorktreeId::from_usize(
worktrees
.into_iter()
.find(|worktree| worktree.id() != worktree_id.to_usize())
.expect("New worktree should have a different id")
.id(),
)
});
close_active_item(&workspace, &deterministic, cx).await;
let initial_history_items = open_close_queried_buffer(
"sec",
1,
"second.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
initial_history_items,
vec![FoundPath::new(
ProjectPath {
worktree_id: external_worktree_id,
path: Arc::from(Path::new("")),
},
Some(PathBuf::from("/external-src/test/third.rs"))
)],
"Should show external file with its full path in the history after it was open"
);
let updated_history_items = open_close_queried_buffer(
"fir",
1,
"first.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
updated_history_items,
vec![
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id: external_worktree_id,
path: Arc::from(Path::new("")),
},
Some(PathBuf::from("/external-src/test/third.rs"))
),
],
"Should keep external file with history updates",
);
}
async fn open_close_queried_buffer( async fn open_close_queried_buffer(
input: &str, input: &str,
expected_matches: usize, expected_matches: usize,
@ -1114,7 +1479,7 @@ mod tests {
workspace: &ViewHandle<Workspace>, workspace: &ViewHandle<Workspace>,
deterministic: &gpui::executor::Deterministic, deterministic: &gpui::executor::Deterministic,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) -> Vec<ProjectPath> { ) -> Vec<FoundPath> {
cx.dispatch_action(window_id, Toggle); cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap()); let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder finder
@ -1152,6 +1517,16 @@ mod tests {
); );
}); });
close_active_item(workspace, deterministic, cx).await;
history_items
}
async fn close_active_item(
workspace: &ViewHandle<Workspace>,
deterministic: &gpui::executor::Deterministic,
cx: &mut TestAppContext,
) {
let mut original_items = HashMap::new(); let mut original_items = HashMap::new();
cx.read(|cx| { cx.read(|cx| {
for pane in workspace.read(cx).panes() { for pane in workspace.read(cx).panes() {
@ -1161,6 +1536,8 @@ mod tests {
assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
} }
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
active_pane active_pane
.update(cx, |pane, cx| { .update(cx, |pane, cx| {
pane.close_active_item(&workspace::CloseActiveItem, cx) pane.close_active_item(&workspace::CloseActiveItem, cx)
@ -1185,8 +1562,10 @@ mod tests {
} }
} }
}); });
assert!(
history_items original_items.len() <= 1,
"At most one panel should got closed"
);
} }
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> { fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
@ -1215,4 +1594,11 @@ mod tests {
}) })
.unwrap() .unwrap()
} }
fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
FoundPath {
project: project_path,
absolute: None,
}
}
} }

View file

@ -960,7 +960,7 @@ impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
pub fn detach_and_log_err(self, cx: &mut AppContext) { pub fn detach_and_log_err(self, cx: &mut AppContext) {
cx.spawn(|_| async move { cx.spawn(|_| async move {
if let Err(err) = self.await { if let Err(err) = self.await {
log::error!("{}", err); log::error!("{:#}", err);
} }
}) })
.detach(); .detach();

View file

@ -25,8 +25,9 @@ struct Family {
pub struct FontCache(RwLock<FontCacheState>); pub struct FontCache(RwLock<FontCacheState>);
pub struct FontCacheState { pub struct FontCacheState {
fonts: Arc<dyn platform::FontSystem>, font_system: Arc<dyn platform::FontSystem>,
families: Vec<Family>, families: Vec<Family>,
default_family: Option<FamilyId>,
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>, font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
metrics: HashMap<FontId, Metrics>, metrics: HashMap<FontId, Metrics>,
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>, wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
@ -42,8 +43,9 @@ unsafe impl Send for FontCache {}
impl FontCache { impl FontCache {
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self { pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
Self(RwLock::new(FontCacheState { Self(RwLock::new(FontCacheState {
fonts, font_system: fonts,
families: Default::default(), families: Default::default(),
default_family: None,
font_selections: Default::default(), font_selections: Default::default(),
metrics: Default::default(), metrics: Default::default(),
wrapper_pool: Default::default(), wrapper_pool: Default::default(),
@ -73,14 +75,14 @@ impl FontCache {
let mut state = RwLockUpgradableReadGuard::upgrade(state); let mut state = RwLockUpgradableReadGuard::upgrade(state);
if let Ok(font_ids) = state.fonts.load_family(name, features) { if let Ok(font_ids) = state.font_system.load_family(name, features) {
if font_ids.is_empty() { if font_ids.is_empty() {
continue; continue;
} }
let family_id = FamilyId(state.families.len()); let family_id = FamilyId(state.families.len());
for font_id in &font_ids { for font_id in &font_ids {
if state.fonts.glyph_for_char(*font_id, 'm').is_none() { if state.font_system.glyph_for_char(*font_id, 'm').is_none() {
return Err(anyhow!("font must contain a glyph for the 'm' character")); return Err(anyhow!("font must contain a glyph for the 'm' character"));
} }
} }
@ -99,6 +101,31 @@ impl FontCache {
)) ))
} }
/// Returns an arbitrary font family that is available on the system.
pub fn known_existing_family(&self) -> FamilyId {
if let Some(family_id) = self.0.read().default_family {
return family_id;
}
let default_family = self
.load_family(
&["Courier", "Helvetica", "Arial", "Verdana"],
&Default::default(),
)
.unwrap_or_else(|_| {
let all_family_names = self.0.read().font_system.all_families();
let all_family_names: Vec<_> = all_family_names
.iter()
.map(|string| string.as_str())
.collect();
self.load_family(&all_family_names, &Default::default())
.expect("could not load any default font family")
});
self.0.write().default_family = Some(default_family);
default_family
}
pub fn default_font(&self, family_id: FamilyId) -> FontId { pub fn default_font(&self, family_id: FamilyId) -> FontId {
self.select_font(family_id, &Properties::default()).unwrap() self.select_font(family_id, &Properties::default()).unwrap()
} }
@ -115,7 +142,7 @@ impl FontCache {
let mut inner = RwLockUpgradableReadGuard::upgrade(inner); let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
let family = &inner.families[family_id.0]; let family = &inner.families[family_id.0];
let font_id = inner let font_id = inner
.fonts .font_system
.select_font(&family.font_ids, properties) .select_font(&family.font_ids, properties)
.unwrap_or(family.font_ids[0]); .unwrap_or(family.font_ids[0]);
@ -137,7 +164,7 @@ impl FontCache {
if let Some(metrics) = state.metrics.get(&font_id) { if let Some(metrics) = state.metrics.get(&font_id) {
f(metrics) f(metrics)
} else { } else {
let metrics = state.fonts.font_metrics(font_id); let metrics = state.font_system.font_metrics(font_id);
let metric = f(&metrics); let metric = f(&metrics);
let mut state = RwLockUpgradableReadGuard::upgrade(state); let mut state = RwLockUpgradableReadGuard::upgrade(state);
state.metrics.insert(font_id, metrics); state.metrics.insert(font_id, metrics);
@ -157,8 +184,11 @@ impl FontCache {
let bounds; let bounds;
{ {
let state = self.0.read(); let state = self.0.read();
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap(); bounds = state
.font_system
.typographic_bounds(font_id, glyph_id)
.unwrap();
} }
bounds.width() * self.em_scale(font_id, font_size) bounds.width() * self.em_scale(font_id, font_size)
} }
@ -168,8 +198,8 @@ impl FontCache {
let advance; let advance;
{ {
let state = self.0.read(); let state = self.0.read();
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap(); glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
advance = state.fonts.advance(font_id, glyph_id).unwrap(); advance = state.font_system.advance(font_id, glyph_id).unwrap();
} }
advance.x() * self.em_scale(font_id, font_size) advance.x() * self.em_scale(font_id, font_size)
} }
@ -214,7 +244,7 @@ impl FontCache {
.or_default(); .or_default();
let wrapper = wrappers let wrapper = wrappers
.pop() .pop()
.unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone())); .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.font_system.clone()));
LineWrapperHandle { LineWrapperHandle {
wrapper: Some(wrapper), wrapper: Some(wrapper),
font_cache: self.clone(), font_cache: self.clone(),

View file

@ -295,13 +295,14 @@ impl Default for TextStyle {
.as_ref() .as_ref()
.expect("TextStyle::default can only be called within a call to with_font_cache"); .expect("TextStyle::default can only be called within a call to with_font_cache");
let font_family_name = Arc::from("Courier"); let font_family_id = font_cache.known_existing_family();
let font_family_id = font_cache
.load_family(&[&font_family_name], &Default::default())
.unwrap();
let font_id = font_cache let font_id = font_cache
.select_font(font_family_id, &Default::default()) .select_font(font_family_id, &Default::default())
.unwrap(); .expect("did not have any font in system-provided family");
let font_family_name = font_cache
.family_name(font_family_id)
.expect("we loaded this family from the font cache, so this should work");
Self { Self {
color: Default::default(), color: Default::default(),
font_family_name, font_family_name,

View file

@ -343,6 +343,7 @@ pub enum RasterizationOptions {
pub trait FontSystem: Send + Sync { pub trait FontSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>; fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
fn all_families(&self) -> Vec<String>;
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>; fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
fn select_font( fn select_font(
&self, &self,

View file

@ -66,6 +66,14 @@ impl platform::FontSystem for FontSystem {
self.0.write().add_fonts(fonts) self.0.write().add_fonts(fonts)
} }
fn all_families(&self) -> Vec<String> {
self.0
.read()
.system_source
.all_families()
.expect("core text should never return an error")
}
fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> { fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
self.0.write().load_family(name, features) self.0.write().load_family(name, features)
} }

View file

@ -70,7 +70,7 @@ settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
ctor.workspace = true ctor.workspace = true
env_logger.workspace = true env_logger.workspace = true
indoc = "1.0.4" indoc.workspace = true
rand.workspace = true rand.workspace = true
tree-sitter-embedded-template = "*" tree-sitter-embedded-template = "*"
tree-sitter-html = "*" tree-sitter-html = "*"

View file

@ -46,6 +46,7 @@ collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
live_kit_server = { path = "../live_kit_server" } live_kit_server = { path = "../live_kit_server" }
media = { path = "../media" } media = { path = "../media" }
nanoid = "0.4"
anyhow.workspace = true anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true

View file

@ -3089,7 +3089,7 @@ impl Project {
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
let (worktree, relative_path) = self let (worktree, relative_path) = self
.find_local_worktree(&abs_path, cx) .find_local_worktree(&abs_path, cx)
.ok_or_else(|| anyhow!("no worktree found for diagnostics"))?; .ok_or_else(|| anyhow!("no worktree found for diagnostics path {abs_path:?}"))?;
let project_path = ProjectPath { let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(), worktree_id: worktree.read(cx).id(),

View file

@ -44,11 +44,11 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.set_global(ActiveSearches::default()); cx.set_global(ActiveSearches::default());
cx.add_action(ProjectSearchView::deploy); cx.add_action(ProjectSearchView::deploy);
cx.add_action(ProjectSearchView::move_focus_to_results);
cx.add_action(ProjectSearchBar::search); cx.add_action(ProjectSearchBar::search);
cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_next_match);
cx.add_action(ProjectSearchBar::select_prev_match); cx.add_action(ProjectSearchBar::select_prev_match);
cx.add_action(ProjectSearchBar::move_focus_to_results);
cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab);
cx.capture_action(ProjectSearchBar::tab_previous); cx.capture_action(ProjectSearchBar::tab_previous);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx); add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
@ -717,6 +717,23 @@ impl ProjectSearchView {
pub fn has_matches(&self) -> bool { pub fn has_matches(&self) -> bool {
self.active_match_index.is_some() self.active_match_index.is_some()
} }
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
search_view.update(cx, |search_view, cx| {
if !search_view.results_editor.is_focused(cx)
&& !search_view.model.read(cx).match_ranges.is_empty()
{
return search_view.focus_results_editor(cx);
}
});
}
cx.propagate_action();
}
} }
impl Default for ProjectSearchBar { impl Default for ProjectSearchBar {
@ -794,23 +811,6 @@ impl ProjectSearchBar {
} }
} }
fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
search_view.update(cx, |search_view, cx| {
if search_view.query_editor.is_focused(cx)
&& !search_view.model.read(cx).match_ranges.is_empty()
{
search_view.focus_results_editor(cx);
}
});
} else {
cx.propagate_action();
}
}
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) { fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
self.cycle_field(Direction::Next, cx); self.cycle_field(Direction::Next, cx);
} }
@ -1257,7 +1257,182 @@ pub mod tests {
}); });
} }
#[gpui::test]
async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let active_item = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
});
assert!(
active_item.is_none(),
"Expected no search panel to be active, but got: {active_item:?}"
);
workspace.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
});
let Some(search_view) = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
}) else {
panic!("Search view expected to appear after new search event trigger")
};
let search_view_id = search_view.id();
cx.spawn(
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
)
.detach();
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
assert!(
search_view.query_editor.is_focused(cx),
"Empty search view should be focused after the toggle focus event: no results panel to focus on",
);
});
search_view.update(cx, |search_view, cx| {
let query_editor = &search_view.query_editor;
assert!(
query_editor.is_focused(cx),
"Search view should be focused after the new search view is activated",
);
let query_text = query_editor.read(cx).text(cx);
assert!(
query_text.is_empty(),
"New search query should be empty but got '{query_text}'",
);
let results_text = search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx));
assert!(
results_text.is_empty(),
"Empty search view should have no results but got '{results_text}'"
);
});
search_view.update(cx, |search_view, cx| {
search_view.query_editor.update(cx, |query_editor, cx| {
query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
});
search_view.search(cx);
});
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
let results_text = search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx));
assert!(
results_text.is_empty(),
"Search view for mismatching query should have no results but got '{results_text}'"
);
assert!(
search_view.query_editor.is_focused(cx),
"Search view should be focused after mismatching query had been used in search",
);
});
cx.spawn(
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
)
.detach();
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
assert!(
search_view.query_editor.is_focused(cx),
"Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
);
});
search_view.update(cx, |search_view, cx| {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
search_view.search(cx);
});
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Search view results should match the query"
);
assert!(
search_view.results_editor.is_focused(cx),
"Search view with mismatching query should be focused after search results are available",
);
});
cx.spawn(
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
)
.detach();
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
assert!(
search_view.results_editor.is_focused(cx),
"Search view with matching query should still have its results editor focused after the toggle focus event",
);
});
workspace.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
});
search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Results should be unchanged after search view 2nd open in a row"
);
assert!(
search_view.query_editor.is_focused(cx),
"Focus should be moved into query editor again after search view 2nd open in a row"
);
});
cx.spawn(
|mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) },
)
.detach();
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
assert!(
search_view.results_editor.is_focused(cx),
"Search view with matching query should switch focus to the results editor after the toggle focus event",
);
});
}
pub fn init_test(cx: &mut TestAppContext) { pub fn init_test(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
let fonts = cx.font_cache(); let fonts = cx.font_cache();
let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
theme.search.match_background = Color::red(); theme.search.match_background = Color::red();
@ -1275,8 +1450,10 @@ pub mod tests {
language::init(cx); language::init(cx);
client::init_settings(cx); client::init_settings(cx);
editor::init_settings(cx); editor::init(cx);
workspace::init_settings(cx); workspace::init_settings(cx);
Project::init_settings(cx);
super::init(cx);
}); });
} }
} }

View file

@ -55,15 +55,22 @@ pub fn watch_config_file(
.spawn(async move { .spawn(async move {
let events = fs.watch(&path, Duration::from_millis(100)).await; let events = fs.watch(&path, Duration::from_millis(100)).await;
futures::pin_mut!(events); futures::pin_mut!(events);
let contents = fs.load(&path).await.unwrap_or_default();
if tx.unbounded_send(contents).is_err() {
return;
}
loop { loop {
if events.next().await.is_none() {
break;
}
if let Ok(contents) = fs.load(&path).await { if let Ok(contents) = fs.load(&path).await {
if !tx.unbounded_send(contents).is_ok() { if !tx.unbounded_send(contents).is_ok() {
break; break;
} }
} }
if events.next().await.is_none() {
break;
}
} }
}) })
.detach(); .detach();

View file

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use collections::{btree_map, hash_map, BTreeMap, HashMap}; use collections::{btree_map, hash_map, BTreeMap, HashMap};
use gpui::AppContext; use gpui::AppContext;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -84,15 +84,26 @@ pub struct SettingsJsonSchemaParams<'a> {
} }
/// A set of strongly-typed setting values defined via multiple JSON files. /// A set of strongly-typed setting values defined via multiple JSON files.
#[derive(Default)]
pub struct SettingsStore { pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>, setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
default_deserialized_settings: Option<serde_json::Value>, default_deserialized_settings: serde_json::Value,
user_deserialized_settings: Option<serde_json::Value>, user_deserialized_settings: serde_json::Value,
local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>, local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>, tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
} }
impl Default for SettingsStore {
fn default() -> Self {
SettingsStore {
setting_values: Default::default(),
default_deserialized_settings: serde_json::json!({}),
user_deserialized_settings: serde_json::json!({}),
local_deserialized_settings: Default::default(),
tab_size_callback: Default::default(),
}
}
}
#[derive(Debug)] #[derive(Debug)]
struct SettingValue<T> { struct SettingValue<T> {
global_value: Option<T>, global_value: Option<T>,
@ -136,20 +147,18 @@ impl SettingsStore {
local_values: Vec::new(), local_values: Vec::new(),
})); }));
if let Some(default_settings) = &self.default_deserialized_settings {
if let Some(default_settings) = setting_value if let Some(default_settings) = setting_value
.deserialize_setting(default_settings) .deserialize_setting(&self.default_deserialized_settings)
.log_err() .log_err()
{ {
let mut user_values_stack = Vec::new(); let mut user_values_stack = Vec::new();
if let Some(user_settings) = &self.user_deserialized_settings { if let Some(user_settings) = setting_value
if let Some(user_settings) = .deserialize_setting(&self.user_deserialized_settings)
setting_value.deserialize_setting(user_settings).log_err() .log_err()
{ {
user_values_stack = vec![user_settings]; user_values_stack = vec![user_settings];
} }
}
if let Some(setting) = setting_value if let Some(setting) = setting_value
.load_setting(&default_settings, &user_values_stack, cx) .load_setting(&default_settings, &user_values_stack, cx)
@ -159,7 +168,6 @@ impl SettingsStore {
} }
} }
} }
}
/// Get the value of a setting. /// Get the value of a setting.
/// ///
@ -189,9 +197,7 @@ impl SettingsStore {
/// This is only for debugging and reporting. For user-facing functionality, /// This is only for debugging and reporting. For user-facing functionality,
/// use the typed setting interface. /// use the typed setting interface.
pub fn untyped_user_settings(&self) -> &serde_json::Value { pub fn untyped_user_settings(&self) -> &serde_json::Value {
self.user_deserialized_settings &self.user_deserialized_settings
.as_ref()
.unwrap_or(&serde_json::Value::Null)
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -213,11 +219,7 @@ impl SettingsStore {
cx: &AppContext, cx: &AppContext,
update: impl FnOnce(&mut T::FileContent), update: impl FnOnce(&mut T::FileContent),
) { ) {
if self.user_deserialized_settings.is_none() { let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap();
self.set_user_settings("{}", cx).unwrap();
}
let old_text =
serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
let new_text = self.new_text_for_update::<T>(old_text, update); let new_text = self.new_text_for_update::<T>(old_text, update);
self.set_user_settings(&new_text, cx).unwrap(); self.set_user_settings(&new_text, cx).unwrap();
} }
@ -250,11 +252,7 @@ impl SettingsStore {
.setting_values .setting_values
.get(&setting_type_id) .get(&setting_type_id)
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>())) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
.deserialize_setting( .deserialize_setting(&self.user_deserialized_settings)
self.user_deserialized_settings
.as_ref()
.expect("no user settings loaded"),
)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
panic!( panic!(
"could not deserialize setting type {} from user settings: {}", "could not deserialize setting type {} from user settings: {}",
@ -323,10 +321,14 @@ impl SettingsStore {
default_settings_content: &str, default_settings_content: &str,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
self.default_deserialized_settings = let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
Some(parse_json_with_comments(default_settings_content)?); if settings.is_object() {
self.default_deserialized_settings = settings;
self.recompute_values(None, cx)?; self.recompute_values(None, cx)?;
Ok(()) Ok(())
} else {
Err(anyhow!("settings must be an object"))
}
} }
/// Set the user settings via a JSON string. /// Set the user settings via a JSON string.
@ -335,9 +337,14 @@ impl SettingsStore {
user_settings_content: &str, user_settings_content: &str,
cx: &AppContext, cx: &AppContext,
) -> Result<()> { ) -> Result<()> {
self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?); let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
if settings.is_object() {
self.user_deserialized_settings = settings;
self.recompute_values(None, cx)?; self.recompute_values(None, cx)?;
Ok(()) Ok(())
} else {
Err(anyhow!("settings must be an object"))
}
} }
/// Add or remove a set of local settings via a JSON string. /// Add or remove a set of local settings via a JSON string.
@ -443,20 +450,19 @@ impl SettingsStore {
let mut user_settings_stack = Vec::<DeserializedSetting>::new(); let mut user_settings_stack = Vec::<DeserializedSetting>::new();
let mut paths_stack = Vec::<Option<&Path>>::new(); let mut paths_stack = Vec::<Option<&Path>>::new();
for setting_value in self.setting_values.values_mut() { for setting_value in self.setting_values.values_mut() {
if let Some(default_settings) = &self.default_deserialized_settings { let default_settings =
let default_settings = setting_value.deserialize_setting(default_settings)?; setting_value.deserialize_setting(&self.default_deserialized_settings)?;
user_settings_stack.clear(); user_settings_stack.clear();
paths_stack.clear(); paths_stack.clear();
if let Some(user_settings) = &self.user_deserialized_settings { if let Some(user_settings) = setting_value
if let Some(user_settings) = .deserialize_setting(&self.user_deserialized_settings)
setting_value.deserialize_setting(user_settings).log_err() .log_err()
{ {
user_settings_stack.push(user_settings); user_settings_stack.push(user_settings);
paths_stack.push(None); paths_stack.push(None);
} }
}
// If the global settings file changed, reload the global value for the field. // If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() { if changed_local_path.is_none() {
@ -505,7 +511,6 @@ impl SettingsStore {
} }
} }
} }
}
Ok(()) Ok(())
} }
} }

View file

@ -6,7 +6,7 @@ publish = false
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
indoc = "1.0.7" indoc.workspace = true
libsqlite3-sys = { version = "0.24", features = ["bundled"] } libsqlite3-sys = { version = "0.24", features = ["bundled"] }
smol.workspace = true smol.workspace = true
thread_local = "1.1.4" thread_local = "1.1.4"

View file

@ -14,6 +14,7 @@ editor = { path = "../editor" }
language = { path = "../language" } language = { path = "../language" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
search = { path = "../search" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }

View file

@ -22,7 +22,7 @@ const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
actions!(terminal_panel, [ToggleFocus]); actions!(terminal_panel, [ToggleFocus]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(TerminalPanel::add_terminal); cx.add_action(TerminalPanel::new_terminal);
} }
pub enum Event { pub enum Event {
@ -55,6 +55,7 @@ impl TerminalPanel {
cx, cx,
); );
pane.set_can_split(false, cx); pane.set_can_split(false, cx);
pane.set_can_navigate(false, cx);
pane.on_can_drop(move |drag_and_drop, cx| { pane.on_can_drop(move |drag_and_drop, cx| {
drag_and_drop drag_and_drop
.currently_dragged::<DraggedItem>(window_id) .currently_dragged::<DraggedItem>(window_id)
@ -68,6 +69,7 @@ impl TerminalPanel {
.with_child(Pane::render_tab_bar_button( .with_child(Pane::render_tab_bar_button(
0, 0,
"icons/plus_12.svg", "icons/plus_12.svg",
false,
Some(( Some((
"New Terminal".into(), "New Terminal".into(),
Some(Box::new(workspace::NewTerminal)), Some(Box::new(workspace::NewTerminal)),
@ -78,7 +80,7 @@ impl TerminalPanel {
cx.window_context().defer(move |cx| { cx.window_context().defer(move |cx| {
if let Some(this) = this.upgrade(cx) { if let Some(this) = this.upgrade(cx) {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.add_terminal(&Default::default(), cx); this.add_terminal(cx);
}); });
} }
}) })
@ -92,6 +94,7 @@ impl TerminalPanel {
} else { } else {
"icons/maximize_8.svg" "icons/maximize_8.svg"
}, },
pane.is_zoomed(),
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
cx, cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx), move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
@ -99,6 +102,9 @@ impl TerminalPanel {
)) ))
.into_any() .into_any()
}); });
let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
pane.toolbar()
.update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
pane pane
}); });
let subscriptions = vec![ let subscriptions = vec![
@ -216,7 +222,19 @@ impl TerminalPanel {
} }
} }
fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext<Self>) { fn new_terminal(
workspace: &mut Workspace,
_: &workspace::NewTerminal,
cx: &mut ViewContext<Workspace>,
) {
let Some(this) = workspace.focus_panel::<Self>(cx) else {
return;
};
this.update(cx, |this, cx| this.add_terminal(cx))
}
fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let pane = this.read_with(&cx, |this, _| this.pane.clone())?; let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
@ -357,7 +375,7 @@ impl Panel for TerminalPanel {
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) { fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active && self.pane.read(cx).items_len() == 0 { if active && self.pane.read(cx).items_len() == 0 {
self.add_terminal(&Default::default(), cx) self.add_terminal(cx)
} }
} }

View file

@ -38,7 +38,7 @@ use workspace::{
notifications::NotifyResultExt, notifications::NotifyResultExt,
pane, register_deserializable_item, pane, register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
Pane, ToolbarItemLocation, Workspace, WorkspaceId, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
}; };
pub use terminal::TerminalSettings; pub use terminal::TerminalSettings;
@ -66,10 +66,10 @@ pub fn init(cx: &mut AppContext) {
terminal_panel::init(cx); terminal_panel::init(cx);
terminal::init(cx); terminal::init(cx);
cx.add_action(TerminalView::deploy);
register_deserializable_item::<TerminalView>(cx); register_deserializable_item::<TerminalView>(cx);
cx.add_action(TerminalView::deploy);
//Useful terminal views //Useful terminal views
cx.add_action(TerminalView::send_text); cx.add_action(TerminalView::send_text);
cx.add_action(TerminalView::send_keystroke); cx.add_action(TerminalView::send_keystroke);
@ -101,7 +101,7 @@ impl TerminalView {
///Create a new Terminal in the current working directory or the user's home directory ///Create a new Terminal in the current working directory or the user's home directory
pub fn deploy( pub fn deploy(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &workspace::NewTerminal, _: &NewCenterTerminal,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let strategy = settings::get::<TerminalSettings>(cx); let strategy = settings::get::<TerminalSettings>(cx);

View file

@ -89,7 +89,8 @@ pub struct Workspace {
pub breadcrumbs: Interactive<ContainedText>, pub breadcrumbs: Interactive<ContainedText>,
pub disconnected_overlay: ContainedText, pub disconnected_overlay: ContainedText,
pub modal: ContainerStyle, pub modal: ContainerStyle,
pub zoomed_foreground: ContainerStyle, pub zoomed_panel_foreground: ContainerStyle,
pub zoomed_pane_foreground: ContainerStyle,
pub zoomed_background: ContainerStyle, pub zoomed_background: ContainerStyle,
pub notification: ContainerStyle, pub notification: ContainerStyle,
pub notifications: Notifications, pub notifications: Notifications,

View file

@ -17,7 +17,7 @@ backtrace = "0.3"
log.workspace = true log.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
futures.workspace = true futures.workspace = true
isahc = "1.7" isahc.workspace = true
smol.workspace = true smol.workspace = true
url = "2.2" url = "2.2"
rand.workspace = true rand.workspace = true

View file

@ -35,7 +35,7 @@ settings = { path = "../settings" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
[dev-dependencies] [dev-dependencies]
indoc = "1.0.4" indoc.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
lazy_static.workspace = true lazy_static.workspace = true

View file

@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
open_new(&app_state, cx, |workspace, cx| { open_new(&app_state, cx, |workspace, cx| {
workspace.toggle_dock(DockPosition::Left, false, cx); workspace.toggle_dock(DockPosition::Left, cx);
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
cx.focus(&welcome_page); cx.focus(&welcome_page);

View file

@ -62,5 +62,5 @@ settings = { path = "../settings", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] }
db = { path = "../db", features = ["test-support"] } db = { path = "../db", features = ["test-support"] }
indoc = "1.0.4" indoc.workspace = true
env_logger.workspace = true env_logger.workspace = true

View file

@ -175,12 +175,16 @@ impl Dock {
} }
} }
pub fn position(&self) -> DockPosition {
self.position
}
pub fn is_open(&self) -> bool { pub fn is_open(&self) -> bool {
self.is_open self.is_open
} }
pub fn has_focus(&self, cx: &WindowContext) -> bool { pub fn has_focus(&self, cx: &WindowContext) -> bool {
self.active_panel() self.visible_panel()
.map_or(false, |panel| panel.has_focus(cx)) .map_or(false, |panel| panel.has_focus(cx))
} }
@ -201,7 +205,7 @@ impl Dock {
self.active_panel_index self.active_panel_index
} }
pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) { pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
if open != self.is_open { if open != self.is_open {
self.is_open = open; self.is_open = open;
if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
@ -212,11 +216,6 @@ impl Dock {
} }
} }
pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
self.set_open(!self.is_open, cx);
cx.notify();
}
pub fn set_panel_zoomed( pub fn set_panel_zoomed(
&mut self, &mut self,
panel: &AnyViewHandle, panel: &AnyViewHandle,
@ -259,7 +258,7 @@ impl Dock {
cx.focus(&panel); cx.focus(&panel);
} }
} else if T::should_close_on_event(event) } else if T::should_close_on_event(event)
&& this.active_panel().map_or(false, |p| p.id() == panel.id()) && this.visible_panel().map_or(false, |p| p.id() == panel.id())
{ {
this.set_open(false, cx); this.set_open(false, cx);
} }
@ -315,12 +314,16 @@ impl Dock {
} }
} }
pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> { pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
let entry = self.active_entry()?; let entry = self.visible_entry()?;
Some(&entry.panel) Some(&entry.panel)
} }
fn active_entry(&self) -> Option<&PanelEntry> { pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
Some(&self.panel_entries.get(self.active_panel_index)?.panel)
}
fn visible_entry(&self) -> Option<&PanelEntry> {
if self.is_open { if self.is_open {
self.panel_entries.get(self.active_panel_index) self.panel_entries.get(self.active_panel_index)
} else { } else {
@ -329,7 +332,7 @@ impl Dock {
} }
pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> { pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
let entry = self.active_entry()?; let entry = self.visible_entry()?;
if entry.panel.is_zoomed(cx) { if entry.panel.is_zoomed(cx) {
Some(entry.panel.clone()) Some(entry.panel.clone())
} else { } else {
@ -362,7 +365,7 @@ impl Dock {
} }
pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> { pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
if let Some(active_entry) = self.active_entry() { if let Some(active_entry) = self.visible_entry() {
Empty::new() Empty::new()
.into_any() .into_any()
.contained() .contained()
@ -399,7 +402,7 @@ impl View for Dock {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(active_entry) = self.active_entry() { if let Some(active_entry) = self.visible_entry() {
let style = self.style(cx); let style = self.style(cx);
ChildView::new(active_entry.panel.as_any(), cx) ChildView::new(active_entry.panel.as_any(), cx)
.contained() .contained()
@ -417,7 +420,7 @@ impl View for Dock {
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) { fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() { if cx.is_self_focused() {
if let Some(active_entry) = self.active_entry() { if let Some(active_entry) = self.visible_entry() {
cx.focus(active_entry.panel.as_any()); cx.focus(active_entry.panel.as_any());
} else { } else {
cx.focus_parent(); cx.focus_parent();
@ -473,11 +476,22 @@ impl View for PanelButtons {
Flex::row() Flex::row()
.with_children(panels.into_iter().enumerate().map( .with_children(panels.into_iter().enumerate().map(
|(panel_ix, (view, context_menu))| { |(panel_ix, (view, context_menu))| {
let (tooltip, tooltip_action) = view.icon_tooltip(cx); let is_active = is_open && panel_ix == active_ix;
let (tooltip, tooltip_action) = if is_active {
(
format!("Close {} dock", dock_position.to_label()),
Some(match dock_position {
DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
}),
)
} else {
view.icon_tooltip(cx)
};
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| { MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
let is_active = is_open && panel_ix == active_ix;
let style = button_style.style_for(state, is_active); let style = button_style.style_for(state, is_active);
Flex::row() Flex::row()
.with_child( .with_child(
@ -504,13 +518,22 @@ impl View for PanelButtons {
}) })
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, { .on_click(MouseButton::Left, {
let tooltip_action =
tooltip_action.as_ref().map(|action| action.boxed_clone());
move |_, this, cx| { move |_, this, cx| {
if let Some(workspace) = this.workspace.upgrade(cx) { if let Some(tooltip_action) = &tooltip_action {
cx.window_context().defer(move |cx| { let window_id = cx.window_id();
workspace.update(cx, |workspace, cx| { let view_id = this.workspace.id();
workspace.toggle_panel(dock_position, panel_ix, cx) let tooltip_action = tooltip_action.boxed_clone();
}); cx.spawn(|_, mut cx| async move {
}); cx.dispatch_action(
window_id,
view_id,
&*tooltip_action,
)
.ok();
})
.detach();
} }
} }
}) })

View file

@ -1,5 +1,5 @@
use crate::{Toast, Workspace}; use crate::{Toast, Workspace};
use collections::HashSet; use collections::HashMap;
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle}; use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
use std::{any::TypeId, ops::DerefMut}; use std::{any::TypeId, ops::DerefMut};
@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
} }
} }
struct NotificationTracker { pub(crate) struct NotificationTracker {
notifications_sent: HashSet<TypeId>, notifications_sent: HashMap<TypeId, Vec<usize>>,
} }
impl std::ops::Deref for NotificationTracker { impl std::ops::Deref for NotificationTracker {
type Target = HashSet<TypeId>; type Target = HashMap<TypeId, Vec<usize>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.notifications_sent &self.notifications_sent
@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker {
impl NotificationTracker { impl NotificationTracker {
fn new() -> Self { fn new() -> Self {
Self { Self {
notifications_sent: HashSet::default(), notifications_sent: Default::default(),
} }
} }
} }
impl Workspace { impl Workspace {
pub fn has_shown_notification_once<V: Notification>(
&self,
id: usize,
cx: &ViewContext<Self>,
) -> bool {
cx.global::<NotificationTracker>()
.get(&TypeId::of::<V>())
.map(|ids| ids.contains(&id))
.unwrap_or(false)
}
pub fn show_notification_once<V: Notification>( pub fn show_notification_once<V: Notification>(
&mut self, &mut self,
id: usize, id: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>, build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
) { ) {
if !cx if !self.has_shown_notification_once::<V>(id, cx) {
.global::<NotificationTracker>()
.contains(&TypeId::of::<V>())
{
cx.update_global::<NotificationTracker, _, _>(|tracker, _| { cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
tracker.insert(TypeId::of::<V>()) let entry = tracker.entry(TypeId::of::<V>()).or_default();
entry.push(id);
}); });
self.show_notification::<V>(id, cx, build_notification) self.show_notification::<V>(id, cx, build_notification)
@ -154,9 +163,10 @@ pub mod simple_message_notification {
use gpui::{ use gpui::{
actions, actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
fonts::TextStyle,
impl_actions, impl_actions,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AppContext, Element, Entity, View, ViewContext, AnyElement, AppContext, Element, Entity, View, ViewContext,
}; };
use menu::Cancel; use menu::Cancel;
use serde::Deserialize; use serde::Deserialize;
@ -184,8 +194,13 @@ pub mod simple_message_notification {
) )
} }
enum NotificationMessage {
Text(Cow<'static, str>),
Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
}
pub struct MessageNotification { pub struct MessageNotification {
message: Cow<'static, str>, message: NotificationMessage,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>, on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<Cow<'static, str>>, click_message: Option<Cow<'static, str>>,
} }
@ -204,7 +219,17 @@ pub mod simple_message_notification {
S: Into<Cow<'static, str>>, S: Into<Cow<'static, str>>,
{ {
Self { Self {
message: message.into(), message: NotificationMessage::Text(message.into()),
on_click: None,
click_message: None,
}
}
pub fn new_element(
message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
) -> MessageNotification {
Self {
message: NotificationMessage::Element(message),
on_click: None, on_click: None,
click_message: None, click_message: None,
} }
@ -243,16 +268,20 @@ pub mod simple_message_notification {
enum MessageNotificationTag {} enum MessageNotificationTag {}
let click_message = self.click_message.clone(); let click_message = self.click_message.clone();
let message = self.message.clone(); let message = match &self.message {
NotificationMessage::Text(text) => {
Text::new(text.to_owned(), theme.message.text.clone()).into_any()
}
NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
};
let on_click = self.on_click.clone(); let on_click = self.on_click.clone();
let has_click_action = on_click.is_some(); let has_click_action = on_click.is_some();
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
Flex::column() Flex::column()
.with_child( .with_child(
Flex::row() Flex::row()
.with_child( .with_child(
Text::new(message, theme.message.text.clone()) message
.contained() .contained()
.with_style(theme.message.container) .with_style(theme.message.container)
.aligned() .aligned()
@ -281,45 +310,47 @@ pub mod simple_message_notification {
.with_cursor_style(CursorStyle::PointingHand) .with_cursor_style(CursorStyle::PointingHand)
.aligned() .aligned()
.constrained() .constrained()
.with_height( .with_height(cx.font_cache().line_height(theme.message.text.font_size))
cx.font_cache().line_height(theme.message.text.font_size),
)
.aligned() .aligned()
.top() .top()
.flex_float(), .flex_float(),
), ),
) )
.with_children({ .with_children({
click_message
.map(|click_message| {
MouseEventHandler::<MessageNotificationTag, _>::new(
0,
cx,
|state, _| {
let style = theme.action_message.style_for(state, false); let style = theme.action_message.style_for(state, false);
if let Some(click_message) = click_message {
Some( Flex::row()
Flex::row().with_child( .with_child(
Text::new(click_message, style.text.clone()) Text::new(click_message, style.text.clone())
.contained() .contained()
.with_style(style.container), .with_style(style.container),
),
) )
} else {
None
}
.into_iter()
})
.contained() .contained()
}) },
// Since we're not using a proper overlay, we have to capture these extra events )
.on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {})
.on_click(MouseButton::Left, move |_, this, cx| { .on_click(MouseButton::Left, move |_, this, cx| {
if let Some(on_click) = on_click.as_ref() { if let Some(on_click) = on_click.as_ref() {
on_click(cx); on_click(cx);
this.dismiss(&Default::default(), cx); this.dismiss(&Default::default(), cx);
} }
}) })
// Since we're not using a proper overlay, we have to capture these extra events
.on_down(MouseButton::Left, |_, _, _| {})
.on_up(MouseButton::Left, |_, _, _| {})
.with_cursor_style(if has_click_action { .with_cursor_style(if has_click_action {
CursorStyle::PointingHand CursorStyle::PointingHand
} else { } else {
CursorStyle::Arrow CursorStyle::Arrow
}) })
})
.into_iter()
})
.into_any() .into_any()
} }
} }

View file

@ -2,8 +2,8 @@ mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection}; use super::{ItemHandle, SplitDirection};
use crate::{ use crate::{
item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal, item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
ToggleZoom, Workspace, WorkspaceSettings, NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::{HashMap, HashSet, VecDeque}; use collections::{HashMap, HashSet, VecDeque};
@ -31,7 +31,7 @@ use std::{
any::Any, any::Any,
cell::RefCell, cell::RefCell,
cmp, mem, cmp, mem,
path::Path, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::{ sync::{
atomic::{AtomicUsize, Ordering}, atomic::{AtomicUsize, Ordering},
@ -150,7 +150,6 @@ pub enum Event {
pub struct Pane { pub struct Pane {
items: Vec<Box<dyn ItemHandle>>, items: Vec<Box<dyn ItemHandle>>,
activation_history: Vec<usize>, activation_history: Vec<usize>,
is_active: bool,
zoomed: bool, zoomed: bool,
active_item_index: usize, active_item_index: usize,
last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>, last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
@ -164,6 +163,7 @@ pub struct Pane {
has_focus: bool, has_focus: bool,
can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>, can_drop: Rc<dyn Fn(&DragAndDrop<Workspace>, &WindowContext) -> bool>,
can_split: bool, can_split: bool,
can_navigate: bool,
render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>, render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>>,
} }
@ -179,7 +179,7 @@ struct NavHistory {
backward_stack: VecDeque<NavigationEntry>, backward_stack: VecDeque<NavigationEntry>,
forward_stack: VecDeque<NavigationEntry>, forward_stack: VecDeque<NavigationEntry>,
closed_stack: VecDeque<NavigationEntry>, closed_stack: VecDeque<NavigationEntry>,
paths_by_item: HashMap<usize, ProjectPath>, paths_by_item: HashMap<usize, (ProjectPath, Option<PathBuf>)>,
pane: WeakViewHandle<Pane>, pane: WeakViewHandle<Pane>,
next_timestamp: Arc<AtomicUsize>, next_timestamp: Arc<AtomicUsize>,
} }
@ -254,7 +254,6 @@ impl Pane {
Self { Self {
items: Vec::new(), items: Vec::new(),
activation_history: Vec::new(), activation_history: Vec::new(),
is_active: true,
zoomed: false, zoomed: false,
active_item_index: 0, active_item_index: 0,
last_focused_view_by_item: Default::default(), last_focused_view_by_item: Default::default(),
@ -279,12 +278,14 @@ impl Pane {
has_focus: false, has_focus: false,
can_drop: Rc::new(|_, _| true), can_drop: Rc::new(|_, _| true),
can_split: true, can_split: true,
can_navigate: true,
render_tab_bar_buttons: Rc::new(|pane, cx| { render_tab_bar_buttons: Rc::new(|pane, cx| {
Flex::row() Flex::row()
// New menu // New menu
.with_child(Self::render_tab_bar_button( .with_child(Self::render_tab_bar_button(
0, 0,
"icons/plus_12.svg", "icons/plus_12.svg",
false,
Some(("New...".into(), None)), Some(("New...".into(), None)),
cx, cx,
|pane, cx| pane.deploy_new_menu(cx), |pane, cx| pane.deploy_new_menu(cx),
@ -294,6 +295,7 @@ impl Pane {
.with_child(Self::render_tab_bar_button( .with_child(Self::render_tab_bar_button(
1, 1,
"icons/split_12.svg", "icons/split_12.svg",
false,
Some(("Split Pane".into(), None)), Some(("Split Pane".into(), None)),
cx, cx,
|pane, cx| pane.deploy_split_menu(cx), |pane, cx| pane.deploy_split_menu(cx),
@ -307,6 +309,7 @@ impl Pane {
} else { } else {
"icons/maximize_8.svg" "icons/maximize_8.svg"
}, },
pane.is_zoomed(),
Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))), Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
cx, cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx), move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
@ -321,15 +324,6 @@ impl Pane {
&self.workspace &self.workspace
} }
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
self.is_active = is_active;
cx.notify();
}
pub fn has_focus(&self) -> bool { pub fn has_focus(&self) -> bool {
self.has_focus self.has_focus
} }
@ -346,6 +340,14 @@ impl Pane {
cx.notify(); cx.notify();
} }
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
self.can_navigate = can_navigate;
self.toolbar.update(cx, |toolbar, cx| {
toolbar.set_can_navigate(can_navigate, cx);
});
cx.notify();
}
pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F) pub fn set_render_tab_bar_buttons<F>(&mut self, cx: &mut ViewContext<Self>, render: F)
where where
F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>, F: 'static + Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement<Pane>,
@ -430,6 +432,10 @@ impl Pane {
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let to_load = if let Some(pane) = pane.upgrade(cx) { let to_load = if let Some(pane) = pane.upgrade(cx) {
if !pane.read(cx).can_navigate {
return Task::ready(Ok(()));
}
cx.focus(&pane); cx.focus(&pane);
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
@ -468,7 +474,7 @@ impl Pane {
.paths_by_item .paths_by_item
.get(&entry.item.id()) .get(&entry.item.id())
.cloned() .cloned()
.map(|project_path| (project_path, entry)); .map(|(project_path, _)| (project_path, entry));
} }
} }
}) })
@ -577,6 +583,20 @@ impl Pane {
destination_index: Option<usize>, destination_index: Option<usize>,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
if item.is_singleton(cx) {
if let Some(&entry_id) = item.project_entry_ids(cx).get(0) {
if let Some(project_path) =
workspace.project().read(cx).path_for_entry(entry_id, cx)
{
let abs_path = workspace.absolute_path(&project_path, cx);
pane.read(cx)
.nav_history
.borrow_mut()
.paths_by_item
.insert(item.id(), (project_path, abs_path));
}
}
}
// If no destination index is specified, add or move the item after the active item. // If no destination index is specified, add or move the item after the active item.
let mut insertion_index = { let mut insertion_index = {
let pane = pane.read(cx); let pane = pane.read(cx);
@ -695,6 +715,11 @@ impl Pane {
} }
pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) { pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
// Potentially warn the user of the new keybinding
let workspace_handle = self.workspace().clone();
cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
.detach();
if self.zoomed { if self.zoomed {
cx.emit(Event::ZoomOut); cx.emit(Event::ZoomOut);
} else if !self.items.is_empty() { } else if !self.items.is_empty() {
@ -989,10 +1014,16 @@ impl Pane {
.set_mode(NavigationMode::Normal); .set_mode(NavigationMode::Normal);
if let Some(path) = item.project_path(cx) { if let Some(path) = item.project_path(cx) {
let abs_path = self
.nav_history
.borrow()
.paths_by_item
.get(&item.id())
.and_then(|(_, abs_path)| abs_path.clone());
self.nav_history self.nav_history
.borrow_mut() .borrow_mut()
.paths_by_item .paths_by_item
.insert(item.id(), path); .insert(item.id(), (path, abs_path));
} else { } else {
self.nav_history self.nav_history
.borrow_mut() .borrow_mut()
@ -1185,7 +1216,7 @@ impl Pane {
AnchorCorner::TopRight, AnchorCorner::TopRight,
vec![ vec![
ContextMenuItem::action("New File", NewFile), ContextMenuItem::action("New File", NewFile),
ContextMenuItem::action("New Terminal", NewTerminal), ContextMenuItem::action("New Terminal", NewCenterTerminal),
ContextMenuItem::action("New Search", NewSearch), ContextMenuItem::action("New Search", NewSearch),
], ],
cx, cx,
@ -1309,7 +1340,7 @@ impl Pane {
None None
}; };
let pane_active = self.is_active; let pane_active = self.has_focus;
enum Tabs {} enum Tabs {}
let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx); let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
@ -1592,6 +1623,7 @@ impl Pane {
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>( pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize, index: usize,
icon: &'static str, icon: &'static str,
active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>, tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>, cx: &mut ViewContext<Pane>,
on_click: F, on_click: F,
@ -1601,7 +1633,7 @@ impl Pane {
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| { let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar; let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
let style = theme.pane_button.style_for(mouse_state, false); let style = theme.pane_button.style_for(mouse_state, active);
Svg::new(icon) Svg::new(icon)
.with_color(style.color) .with_color(style.color)
.constrained() .constrained()
@ -1688,7 +1720,7 @@ impl View for Pane {
let mut tab_row = Flex::row() let mut tab_row = Flex::row()
.with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
if self.is_active { if self.has_focus {
let render_tab_bar_buttons = self.render_tab_bar_buttons.clone(); let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
tab_row.add_child( tab_row.add_child(
(render_tab_bar_buttons)(self, cx) (render_tab_bar_buttons)(self, cx)
@ -1779,6 +1811,7 @@ impl View for Pane {
if !self.has_focus { if !self.has_focus {
self.has_focus = true; self.has_focus = true;
cx.emit(Event::Focus); cx.emit(Event::Focus);
cx.notify();
} }
self.toolbar.update(cx, |toolbar, cx| { self.toolbar.update(cx, |toolbar, cx| {
@ -1813,6 +1846,7 @@ impl View for Pane {
self.toolbar.update(cx, |toolbar, cx| { self.toolbar.update(cx, |toolbar, cx| {
toolbar.pane_focus_update(false, cx); toolbar.pane_focus_update(false, cx);
}); });
cx.notify();
} }
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
@ -1940,7 +1974,7 @@ impl PaneNavHistory {
pub fn for_each_entry( pub fn for_each_entry(
&self, &self,
cx: &AppContext, cx: &AppContext,
mut f: impl FnMut(&NavigationEntry, ProjectPath), mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option<PathBuf>)),
) { ) {
let borrowed_history = self.0.borrow(); let borrowed_history = self.0.borrow();
borrowed_history borrowed_history
@ -1949,12 +1983,13 @@ impl PaneNavHistory {
.chain(borrowed_history.backward_stack.iter()) .chain(borrowed_history.backward_stack.iter())
.chain(borrowed_history.closed_stack.iter()) .chain(borrowed_history.closed_stack.iter())
.for_each(|entry| { .for_each(|entry| {
if let Some(path) = borrowed_history.paths_by_item.get(&entry.item.id()) { if let Some(project_and_abs_path) =
f(entry, path.clone()); borrowed_history.paths_by_item.get(&entry.item.id())
{
f(entry, project_and_abs_path.clone());
} else if let Some(item) = entry.item.upgrade(cx) { } else if let Some(item) = entry.item.upgrade(cx) {
let path = item.project_path(cx); if let Some(path) = item.project_path(cx) {
if let Some(path) = path { f(entry, (path, None));
f(entry, path);
} }
} }
}) })

View file

@ -53,6 +53,7 @@ pub enum ToolbarItemLocation {
pub struct Toolbar { pub struct Toolbar {
active_pane_item: Option<Box<dyn ItemHandle>>, active_pane_item: Option<Box<dyn ItemHandle>>,
hidden: bool, hidden: bool,
can_navigate: bool,
pane: WeakViewHandle<Pane>, pane: WeakViewHandle<Pane>,
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>, items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
} }
@ -132,10 +133,9 @@ impl View for Toolbar {
let button_style = theme.nav_button; let button_style = theme.nav_button;
let tooltip_style = theme::current(cx).tooltip.clone(); let tooltip_style = theme::current(cx).tooltip.clone();
Flex::column() let mut primary_items = Flex::row();
.with_child( if self.can_navigate {
Flex::row() primary_items.add_child(nav_button(
.with_child(nav_button(
"icons/arrow_left_16.svg", "icons/arrow_left_16.svg",
button_style, button_style,
nav_button_height, nav_button_height,
@ -163,8 +163,8 @@ impl View for Toolbar {
super::GoBack { pane: None }, super::GoBack { pane: None },
"Go Back", "Go Back",
cx, cx,
)) ));
.with_child(nav_button( primary_items.add_child(nav_button(
"icons/arrow_right_16.svg", "icons/arrow_right_16.svg",
button_style, button_style,
nav_button_height, nav_button_height,
@ -192,17 +192,28 @@ impl View for Toolbar {
super::GoForward { pane: None }, super::GoForward { pane: None },
"Go Forward", "Go Forward",
cx, cx,
)) ));
.with_children(primary_left_items) }
.with_children(primary_right_items) primary_items.extend(primary_left_items);
.constrained() primary_items.extend(primary_right_items);
.with_height(height),
) let mut toolbar = Flex::column();
.with_children(secondary_item) if !primary_items.is_empty() {
toolbar.add_child(primary_items.constrained().with_height(height));
}
if let Some(secondary_item) = secondary_item {
toolbar.add_child(secondary_item);
}
if toolbar.is_empty() {
toolbar.into_any_named("toolbar")
} else {
toolbar
.contained() .contained()
.with_style(container_style) .with_style(container_style)
.into_any_named("toolbar") .into_any_named("toolbar")
} }
}
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -264,9 +275,15 @@ impl Toolbar {
pane, pane,
items: Default::default(), items: Default::default(),
hidden: false, hidden: false,
can_navigate: true,
} }
} }
pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut ViewContext<Self>) {
self.can_navigate = can_navigate;
cx.notify();
}
pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>) pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where where
T: 'static + ToolbarItemView, T: 'static + ToolbarItemView,

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.88.0" version = "0.88.6"
publish = false publish = false
[lib] [lib]
@ -48,6 +48,7 @@ language_selector = { path = "../language_selector" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
lsp_log = { path = "../lsp_log" } lsp_log = { path = "../lsp_log" }
node_runtime = { path = "../node_runtime" } node_runtime = { path = "../node_runtime" }
ai = { path = "../ai" }
outline = { path = "../outline" } outline = { path = "../outline" }
plugin_runtime = { path = "../plugin_runtime" } plugin_runtime = { path = "../plugin_runtime" }
project = { path = "../project" } project = { path = "../project" }
@ -81,7 +82,7 @@ futures.workspace = true
ignore = "0.4" ignore = "0.4"
image = "0.23" image = "0.23"
indexmap = "1.6.2" indexmap = "1.6.2"
isahc = "1.7" isahc.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
libc = "0.2" libc = "0.2"
log.workspace = true log.workspace = true

View file

@ -1 +1 @@
dev stable

View file

@ -1,5 +1,5 @@
name = "Markdown" name = "Markdown"
path_suffixes = ["md", "mdx"] path_suffixes = ["md", "mdx", "zmd"]
brackets = [ brackets = [
{ start = "{", end = "}", close = true, newline = true }, { start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true },

View file

@ -161,6 +161,7 @@ fn main() {
terminal_view::init(cx); terminal_view::init(cx);
theme_testbench::init(cx); theme_testbench::init(cx);
copilot::init(http.clone(), node_runtime, cx); copilot::init(http.clone(), node_runtime, cx);
ai::init(cx);
cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();

View file

@ -89,18 +89,9 @@ pub fn menus() -> Vec<Menu<'static>> {
MenuItem::action("Zoom Out", super::DecreaseBufferFontSize), MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
MenuItem::action("Reset Zoom", super::ResetBufferFontSize), MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
MenuItem::separator(), MenuItem::separator(),
MenuItem::action( MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
"Toggle Left Dock", MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock),
workspace::ToggleLeftDock { focus: false }, MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock),
),
MenuItem::action(
"Toggle Right Dock",
workspace::ToggleRightDock { focus: false },
),
MenuItem::action(
"Toggle Bottom Dock",
workspace::ToggleBottomDock { focus: false },
),
MenuItem::submenu(Menu { MenuItem::submenu(Menu {
name: "Editor Layout", name: "Editor Layout",
items: vec![ items: vec![

View file

@ -354,7 +354,7 @@ pub fn initialize_workspace(
.map_or(false, |entry| entry.is_dir()) .map_or(false, |entry| entry.is_dir())
}) })
{ {
workspace.toggle_dock(project_panel_position, false, cx); workspace.toggle_dock(project_panel_position, cx);
} }
workspace.add_panel(terminal_panel, cx) workspace.add_panel(terminal_panel, cx)

View file

@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) {
hover: { hover: {
color: foreground(layer, "hovered"), color: foreground(layer, "hovered"),
}, },
active: {
color: foreground(layer, "accent"),
}
}, },
paneButtonContainer: { paneButtonContainer: {
background: tab.background, background: tab.background,

View file

@ -13,6 +13,7 @@ import tabBar from "./tabBar"
export default function workspace(colorScheme: ColorScheme) { export default function workspace(colorScheme: ColorScheme) {
const layer = colorScheme.lowest const layer = colorScheme.lowest
const isLight = colorScheme.isLight
const itemSpacing = 8 const itemSpacing = 8
const titlebarButton = { const titlebarButton = {
cornerRadius: 6, cornerRadius: 6,
@ -119,13 +120,19 @@ export default function workspace(colorScheme: ColorScheme) {
cursor: "Arrow", cursor: "Arrow",
}, },
zoomedBackground: { zoomedBackground: {
padding: 10,
cursor: "Arrow", cursor: "Arrow",
background: withOpacity(background(colorScheme.lowest), 0.5) background: isLight
? withOpacity(background(colorScheme.lowest), 0.8)
: withOpacity(background(colorScheme.highest), 0.6)
}, },
zoomedForeground: { zoomedPaneForeground: {
margin: 16,
shadow: colorScheme.modalShadow, shadow: colorScheme.modalShadow,
border: border(colorScheme.highest, { overlay: true }), border: border(colorScheme.lowest, { overlay: true }),
},
zoomedPanelForeground: {
margin: 16,
border: border(colorScheme.lowest, { overlay: true }),
}, },
dock: { dock: {
left: { left: {