Compare commits

..

2 Commits

Author SHA1 Message Date
Bennet Bo Fenner
f448d7e2e8 remove app_version from gpui platform 2024-05-07 16:28:03 +02:00
Bennet Bo Fenner
4624a0a9f4 allow specifying version for gpui app 2024-05-07 16:27:35 +02:00
113 changed files with 2619 additions and 4995 deletions

View File

@@ -6,8 +6,6 @@ Release Notes:
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
### Or...
Release Notes:
**or**
- N/A

View File

@@ -130,7 +130,7 @@ jobs:
# todo(windows): Actually run the tests
windows_tests:
name: (Windows) Run Clippy and tests
runs-on: hosted-windows-1
runs-on: windows-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

20
Cargo.lock generated
View File

@@ -382,9 +382,7 @@ dependencies = [
"editor",
"env_logger",
"feature_flags",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"language",
"languages",
@@ -392,7 +390,6 @@ dependencies = [
"nanoid",
"node_runtime",
"open_ai",
"picker",
"project",
"rand 0.8.5",
"release_channel",
@@ -423,7 +420,6 @@ dependencies = [
"serde_json",
"settings",
"sum_tree",
"ui",
"unindent",
"util",
]
@@ -1491,7 +1487,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
dependencies = [
"ash",
"ash-window",
@@ -1521,7 +1517,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
dependencies = [
"proc-macro2",
"quote",
@@ -4451,16 +4447,6 @@ dependencies = [
"util",
]
[[package]]
name = "git_ui"
version = "0.1.0"
dependencies = [
"git",
"gpui",
"serde",
"workspace",
]
[[package]]
name = "glob"
version = "0.3.1"
@@ -12767,7 +12753,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.136.0"
version = "0.135.0"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -35,7 +35,6 @@ members = [
"crates/fsevent",
"crates/fuzzy",
"crates/git",
"crates/git_ui",
"crates/git_hosting_providers",
"crates/go_to_line",
"crates/google_ai",
@@ -257,8 +256,8 @@ async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
cap-std = "3.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 15C13.433 15 15 13.433 15 11.5C15 9.567 13.433 8 11.5 8C9.567 8 8 9.567 8 11.5C8 13.433 9.567 15 11.5 15Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 240 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4662 14.9152C13.5801 15.0291 13.7648 15.0291 13.8787 14.9152L14.9145 13.8793C15.0284 13.7654 15.0284 13.5807 14.9145 13.4667L12.9483 11.5004L14.9145 9.53392C15.0285 9.42004 15.0285 9.23533 14.9145 9.12137L13.8787 8.08547C13.7648 7.97154 13.5801 7.97154 13.4662 8.08547L11.5 10.0519L9.53376 8.08545C9.41988 7.97152 9.23517 7.97152 9.12124 8.08545L8.08543 9.12136C7.97152 9.23533 7.97152 9.42004 8.08543 9.53392L10.0517 11.5004L8.08545 13.4667C7.97155 13.5807 7.97155 13.7654 8.08545 13.8793L9.12126 14.9152C9.23517 15.0292 9.41988 15.0292 9.53376 14.9152L11.5 12.9489L13.4662 14.9152Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 756 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4L13 12" stroke="black" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 181 B

View File

@@ -316,8 +316,6 @@
"autosave": "off",
// Settings related to the editor's tab bar.
"tab_bar": {
// Whether or not to show the tab bar in the editor
"show": true,
// Whether or not to show the navigation history buttons.
"show_nav_history_buttons": true
},
@@ -372,13 +370,11 @@
//
// 1. Do not soft wrap.
// "soft_wrap": "none",
// 2. Prefer a single line generally, unless an overly long line is encountered.
// "soft_wrap": "prefer_line",
// 3. Soft wrap lines that overflow the editor:
// 2. Soft wrap lines that overflow the editor:
// "soft_wrap": "editor_width",
// 4. Soft wrap lines at the preferred line length
// 3. Soft wrap lines at the preferred line length
// "soft_wrap": "preferred_line_length",
"soft_wrap": "prefer_line",
"soft_wrap": "none",
// The column at which to soft-wrap lines, for buffers where soft-wrap
// is enabled.
"preferred_line_length": 80,
@@ -602,12 +598,7 @@
"format_on_save": "off"
},
"Elixir": {
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Gleam": {
"tab_size": 2
@@ -618,19 +609,11 @@
}
},
"HEEX": {
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Make": {
"hard_tabs": true
},
"Markdown": {
"format_on_save": "off"
},
"Prisma": {
"tab_size": 2
}

View File

@@ -281,14 +281,11 @@ impl ActivityIndicator {
message: "Installing Zed update…".to_string(),
on_click: None,
},
AutoUpdateStatus::Updated { binary_path } => Content {
AutoUpdateStatus::Updated => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({
let restart = workspace::Restart {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::restart(&restart, cx)
on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
})),
},
AutoUpdateStatus::Errored => Content {

View File

@@ -106,11 +106,6 @@ impl SavedConversationMetadata {
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out conversations saved by the new assistant.
if !re.is_match(file_name) {
continue;
}
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),

View File

@@ -22,15 +22,12 @@ client.workspace = true
collections.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
picker.workspace = true
project.workspace = true
rich_text.workspace = true
schemars.workspace = true

View File

@@ -1 +1 @@
> Give me a comprehensive list of all the elements defined in my project using the following query: `impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})`
> Give me a comprehensive list of all the elements define in my project (impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})

View File

@@ -1,3 +0,0 @@
Use tools frequently, especially when referring to files and code. The Zed editor we're working in can show me files directly when you add annotations. Be concise in chat, bountiful in tool calling.
Teach me everything you can about how zed loads settings. Please annotate the code inline.

View File

@@ -1,25 +1,24 @@
mod assistant_settings;
mod attachments;
mod completion_provider;
mod saved_conversation;
mod saved_conversation_picker;
mod tools;
pub mod ui;
use crate::ui::UserOrAssistant;
use ::ui::{div, prelude::*, Color, Tooltip, ViewContext};
use crate::{
attachments::ActiveEditorAttachmentTool,
tools::{CreateBufferTool, ProjectIndexTool},
ui::UserOrAssistant,
};
use ::ui::{div, prelude::*, Color, ViewContext};
use anyhow::{Context, Result};
use assistant_tooling::{
tool_running_placeholder, AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry,
UserAttachment,
AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment,
};
use attachments::ActiveEditorAttachmentTool;
use client::{proto, Client, UserStore};
use collections::HashMap;
use completion_provider::*;
use editor::Editor;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use futures::{future::join_all, StreamExt};
use gpui::{
list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
@@ -28,15 +27,11 @@ use gpui::{
use language::{language_settings::SoftWrap, LanguageRegistry};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use rich_text::RichText;
use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
use saved_conversation_picker::SavedConversationPicker;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
use serde::{Deserialize, Serialize};
use serde::Deserialize;
use settings::Settings;
use std::sync::Arc;
use tools::{AnnotationTool, CreateBufferTool, ProjectIndexTool};
use ui::{ActiveFileButton, Composer, ProjectIndexButton};
use util::paths::CONVERSATIONS_DIR;
use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -61,15 +56,7 @@ pub enum SubmitMode {
Codebase,
}
gpui::actions!(
assistant2,
[
Cancel,
ToggleFocus,
DebugProjectIndex,
ToggleSavedConversations
]
);
gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex]);
gpui::impl_actions!(assistant2, [Submit]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
@@ -109,8 +96,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
},
)
.detach();
cx.observe_new_views(SavedConversationPicker::register)
.detach();
}
pub fn enabled(cx: &AppContext) -> bool {
@@ -140,26 +125,23 @@ impl AssistantPanel {
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(ProjectIndexTool::new(project_index.clone()), cx)
.unwrap();
.context("failed to register ProjectIndexTool")
.log_err();
tool_registry
.register(
CreateBufferTool::new(workspace.clone(), project.clone()),
cx,
)
.unwrap();
tool_registry
.register(AnnotationTool::new(workspace.clone(), project.clone()), cx)
.unwrap();
.context("failed to register CreateBufferTool")
.log_err();
let mut attachment_registry = AttachmentRegistry::new();
attachment_registry
.register(ActiveEditorAttachmentTool::new(workspace.clone(), cx));
let mut attachment_store = AttachmentRegistry::new();
attachment_store.register(ActiveEditorAttachmentTool::new(workspace.clone(), cx));
Self::new(
project.read(cx).fs().clone(),
app_state.languages.clone(),
Arc::new(tool_registry),
Arc::new(attachment_registry),
Arc::new(attachment_store),
app_state.user_store.clone(),
project_index,
workspace,
@@ -169,12 +151,10 @@ impl AssistantPanel {
})
}
#[allow(clippy::too_many_arguments)]
pub fn new(
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>,
attachment_store: Arc<AttachmentRegistry>,
user_store: Model<UserStore>,
project_index: Model<ProjectIndex>,
workspace: WeakView<Workspace>,
@@ -182,10 +162,9 @@ impl AssistantPanel {
) -> Self {
let chat = cx.new_view(|cx| {
AssistantChat::new(
fs,
language_registry,
tool_registry.clone(),
attachment_registry,
attachment_store,
user_store,
project_index,
workspace,
@@ -259,7 +238,6 @@ pub struct AssistantChat {
model: String,
messages: Vec<ChatMessage>,
list_state: ListState,
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
composer_editor: View<Editor>,
project_index_button: View<ProjectIndexButton>,
@@ -281,9 +259,7 @@ struct EditingMessage {
}
impl AssistantChat {
#[allow(clippy::too_many_arguments)]
fn new(
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>,
@@ -328,7 +304,6 @@ impl AssistantChat {
}),
list_state,
user_store,
fs,
language_registry,
project_index_button,
active_file_button,
@@ -367,8 +342,8 @@ impl AssistantChat {
}
if self.pending_completion.take().is_some() {
if let Some(ChatMessage::Assistant(grouping)) = self.messages.last() {
if grouping.messages.is_empty() {
if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
if message.body.text.is_empty() {
self.pop_message(cx);
}
}
@@ -419,8 +394,8 @@ impl AssistantChat {
let mode = *mode;
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
let attachments_task = this.update(&mut cx, |this, cx| {
let attachment_registry = this.attachment_registry.clone();
attachment_registry.call_all_attachment_tools(cx)
let attachment_store = this.attachment_registry.clone();
attachment_store.call_all_attachment_tools(cx)
});
let attachments = maybe!(async {
@@ -503,28 +478,22 @@ impl AssistantChat {
while let Some(delta) = stream.next().await {
let delta = delta?;
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
this.messages.last_mut()
if let Some(ChatMessage::Assistant(AssistantMessage {
body: message_body,
tool_calls: message_tool_calls,
..
})) = this.messages.last_mut()
{
if messages.is_empty() {
messages.push(AssistantMessagePart {
body: RichText::default(),
tool_calls: Vec::new(),
})
}
let message = messages.last_mut().unwrap();
if let Some(content) = &delta.content {
body.push_str(content);
}
for tool_call in delta.tool_calls {
let index = tool_call.index as usize;
if index >= message.tool_calls.len() {
message.tool_calls.resize_with(index + 1, Default::default);
if index >= message_tool_calls.len() {
message_tool_calls.resize_with(index + 1, Default::default);
}
let call = &mut message.tool_calls[index];
let call = &mut message_tool_calls[index];
if let Some(id) = &tool_call.id {
call.id.push_str(id);
@@ -543,7 +512,7 @@ impl AssistantChat {
}
}
message.body =
*message_body =
RichText::new(body.clone(), &[], &this.language_registry);
cx.notify();
} else {
@@ -560,7 +529,7 @@ impl AssistantChat {
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage {
error: message_error,
messages,
tool_calls,
..
})) = this.messages.last_mut()
{
@@ -568,10 +537,8 @@ impl AssistantChat {
message_error.replace(SharedString::from(error.to_string()));
cx.notify();
} else {
if let Some(current_message) = messages.last_mut() {
for tool_call in current_message.tool_calls.iter() {
tool_tasks.push(this.tool_registry.call(tool_call, cx));
}
for tool_call in tool_calls.iter() {
tool_tasks.push(this.tool_registry.call(tool_call, cx));
}
}
}
@@ -587,38 +554,21 @@ impl AssistantChat {
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
this.messages.last_mut()
{
if let Some(current_message) = messages.last_mut() {
current_message.tool_calls = tools;
cx.notify();
} else {
unreachable!()
}
*tool_calls = tools;
cx.notify();
}
})?;
}
}
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
// If the last message is a grouped assistant message, add to the grouped message
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
self.messages.last_mut()
{
messages.push(AssistantMessagePart {
body: RichText::default(),
tool_calls: Vec::new(),
});
return;
}
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
messages: vec![AssistantMessagePart {
body: RichText::default(),
tool_calls: Vec::new(),
}],
body: RichText::default(),
tool_calls: Vec::new(),
error: None,
});
self.push_message(message, cx);
@@ -664,59 +614,6 @@ impl AssistantChat {
*entry = !*entry;
}
fn reset(&mut self) {
self.messages.clear();
self.list_state.reset(0);
self.editing_message.take();
self.collapsed_messages.clear();
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
let messages = std::mem::take(&mut self.messages)
.into_iter()
.map(|message| self.serialize_message(message, cx))
.collect::<Vec<_>>();
self.reset();
let title = messages
.first()
.map(|message| match message {
SavedChatMessage::User { body, .. } => body.clone(),
SavedChatMessage::Assistant { messages, .. } => messages
.first()
.map(|message| message.body.to_string())
.unwrap_or_default(),
})
.unwrap_or_else(|| "A conversation with the assistant.".to_string());
let saved_conversation = SavedConversation {
version: "0.3.0".to_string(),
title,
messages,
};
let discriminant = 1;
let path = CONVERSATIONS_DIR.join(&format!(
"{title} - {discriminant}.zed.{version}.json",
title = saved_conversation.title,
version = saved_conversation.version
));
cx.spawn({
let fs = self.fs.clone();
|_this, _cx| async move {
fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
fs.atomic_write(path, serde_json::to_string(&saved_conversation)?)
.await?;
anyhow::Ok(())
}
})
.detach_and_log_err(cx);
}
fn render_error(
&self,
error: Option<SharedString>,
@@ -744,7 +641,7 @@ impl AssistantChat {
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let is_first = ix == 0;
let is_last = ix == self.messages.len().saturating_sub(1);
let is_last = ix == self.messages.len() - 1;
let padding = Spacing::Large.rems(cx);
@@ -790,14 +687,15 @@ impl AssistantChat {
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
// todo!(): clean up the vec usage
vec![
Some(
RichText::new(
body.read(cx).text(cx),
&[],
&self.language_registry,
)
.element(ElementId::from(id.0), cx),
),
Some(
h_flex()
.gap_2()
.children(
@@ -806,7 +704,7 @@ impl AssistantChat {
.map(|attachment| attachment.view.clone()),
)
.into_any_element(),
],
),
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
@@ -823,36 +721,31 @@ impl AssistantChat {
.into_any(),
ChatMessage::Assistant(AssistantMessage {
id,
messages,
body,
error,
tool_calls,
..
}) => {
let mut message_elements = Vec::new();
let assistant_body = if body.text.is_empty() {
None
} else {
Some(
div()
.child(body.element(ElementId::from(id.0), cx))
.into_any_element(),
)
};
for message in messages {
if !message.body.text.is_empty() {
message_elements.push(
div()
// todo!(): The element Id will need to be a combo of the base ID and the index within the grouping
.child(message.body.element(ElementId::from(id.0), cx))
.into_any_element(),
)
}
let tools = tool_calls
.iter()
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.collect::<Vec<AnyElement>>();
let tools = message
.tool_calls
.iter()
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.collect::<Vec<AnyElement>>();
if !tools.is_empty() {
message_elements.push(div().children(tools).into_any_element())
}
}
if message_elements.is_empty() {
message_elements.push(tool_running_placeholder());
}
let tools_body = if tools.is_empty() {
None
} else {
Some(div().children(tools).into_any_element())
};
div()
.when(is_first, |this| this.pt(padding))
@@ -860,7 +753,8 @@ impl AssistantChat {
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::Assistant,
message_elements,
assistant_body,
tools_body,
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
@@ -902,47 +796,46 @@ impl AssistantChat {
content: body.read(cx).text(cx),
});
}
ChatMessage::Assistant(AssistantMessage { messages, .. }) => {
for message in messages {
let body = message.body.clone();
ChatMessage::Assistant(AssistantMessage {
body, tool_calls, ..
}) => {
// In no case do we want to send an empty message. This shouldn't happen, but we might as well
// not break the Chat API if it does.
if body.text.is_empty() && tool_calls.is_empty() {
continue;
}
if body.text.is_empty() && message.tool_calls.is_empty() {
continue;
}
let tool_calls_from_assistant = message
.tool_calls
.iter()
.map(|tool_call| ToolCall {
content: ToolCallContent::Function {
function: FunctionContent {
name: tool_call.name.clone(),
arguments: tool_call.arguments.clone(),
},
let tool_calls_from_assistant = tool_calls
.iter()
.map(|tool_call| ToolCall {
content: ToolCallContent::Function {
function: FunctionContent {
name: tool_call.name.clone(),
arguments: tool_call.arguments.clone(),
},
id: tool_call.id.clone(),
})
.collect();
},
id: tool_call.id.clone(),
})
.collect();
completion_messages.push(CompletionMessage::Assistant {
content: Some(body.text.to_string()),
tool_calls: tool_calls_from_assistant,
completion_messages.push(CompletionMessage::Assistant {
content: Some(body.text.to_string()),
tool_calls: tool_calls_from_assistant,
});
for tool_call in tool_calls {
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
let content = match &tool_call.result {
Some(result) => {
result.generate(&tool_call.name, &mut project_context, cx)
}
None => "".to_string(),
};
completion_messages.push(CompletionMessage::Tool {
content,
tool_call_id: tool_call.id.clone(),
});
for tool_call in &message.tool_calls {
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
let content = match &tool_call.result {
Some(result) => {
result.generate(&tool_call.name, &mut project_context, cx)
}
None => "".to_string(),
};
completion_messages.push(CompletionMessage::Tool {
content,
tool_call_id: tool_call.id.clone(),
});
}
}
}
}
@@ -956,49 +849,10 @@ impl AssistantChat {
Ok(completion_messages)
})
}
fn serialize_message(
&self,
message: ChatMessage,
cx: &mut ViewContext<AssistantChat>,
) -> SavedChatMessage {
match message {
ChatMessage::User(message) => SavedChatMessage::User {
id: message.id,
body: message.body.read(cx).text(cx),
attachments: message
.attachments
.iter()
.map(|attachment| {
self.attachment_registry
.serialize_user_attachment(attachment)
})
.collect(),
},
ChatMessage::Assistant(message) => SavedChatMessage::Assistant {
id: message.id,
error: message.error,
messages: message
.messages
.iter()
.map(|message| SavedAssistantMessagePart {
body: message.body.text.clone(),
tool_calls: message
.tool_calls
.iter()
.map(|tool_call| self.tool_registry.serialize_tool_call(tool_call))
.collect(),
})
.collect(),
},
}
}
}
impl Render for AssistantChat {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let header_height = Spacing::Small.rems(cx) * 2.0 + ButtonSize::Default.rems();
div()
.relative()
.flex_1()
@@ -1007,51 +861,7 @@ impl Render for AssistantChat {
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.text_color(Color::Default.color(cx))
.child(list(self.list_state.clone()).flex_1().pt(header_height))
.child(
h_flex()
.absolute()
.top_0()
.justify_between()
.w_full()
.h(header_height)
.p(Spacing::Small.rems(cx))
.child(
IconButton::new("open-saved-conversations", IconName::ChevronLeft)
.on_click(|_event, cx| {
cx.dispatch_action(Box::new(ToggleSavedConversations))
})
.tooltip(move |cx| {
Tooltip::with_meta(
"Switch Conversations",
Some(&ToggleSavedConversations),
"UI will change, temporary.",
cx,
)
}),
)
.child(
h_flex()
.gap(Spacing::Large.rems(cx))
.child(
IconButton::new("new-conversation", IconName::Plus)
.on_click(cx.listener(move |this, _event, cx| {
this.new_conversation(cx);
}))
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
)
.child(
IconButton::new("assistant-menu", IconName::Menu)
.disabled(true)
.tooltip(move |cx| {
Tooltip::text(
"Coming soon Assistant settings & controls",
cx,
)
}),
),
),
)
.child(list(self.list_state.clone()).flex_1())
.child(Composer::new(
self.composer_editor.clone(),
self.project_index_button.clone(),
@@ -1062,7 +872,7 @@ impl Render for AssistantChat {
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct MessageId(usize);
impl MessageId {
@@ -1088,18 +898,14 @@ impl ChatMessage {
}
struct UserMessage {
pub id: MessageId,
pub body: View<Editor>,
pub attachments: Vec<UserAttachment>,
}
struct AssistantMessagePart {
pub body: RichText,
pub tool_calls: Vec<ToolFunctionCall>,
id: MessageId,
body: View<Editor>,
attachments: Vec<UserAttachment>,
}
struct AssistantMessage {
pub id: MessageId,
pub messages: Vec<AssistantMessagePart>,
pub error: Option<SharedString>,
id: MessageId,
body: RichText,
tool_calls: Vec<ToolFunctionCall>,
error: Option<SharedString>,
}

View File

@@ -1,3 +1,114 @@
mod active_file;
pub mod active_file;
pub use active_file::*;
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
pub struct ActiveEditorAttachment {
buffer: WeakModel<Buffer>,
path: Option<ProjectPath>,
}
pub struct FileAttachmentView {
output: Result<ActiveEditorAttachment>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(attachment) => {
let filename: SharedString = attachment
.path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
// todo!(): make the button link to the actual file to open
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip({
move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)
})
.into_any_element()
}
Err(err) => div().child(err.to_string()).into_any_element(),
}
}
}
impl ToolOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Ok(result) = &self.output {
if let Some(path) = &result.path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
} else if let Some(buffer) = result.buffer.upgrade() {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
}
String::new()
}
}
pub struct ActiveEditorAttachmentTool {
workspace: WeakView<Workspace>,
}
impl ActiveEditorAttachmentTool {
pub fn new(workspace: WeakView<Workspace>, _cx: &mut WindowContext) -> Self {
Self { workspace }
}
}
impl LanguageModelAttachment for ActiveEditorAttachmentTool {
type Output = ActiveEditorAttachment;
type View = FileAttachmentView;
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
Task::ready(maybe!({
let active_buffer = self
.workspace
.update(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()))
})?
.ok_or_else(|| anyhow!("no active buffer"))?;
let buffer = active_buffer.read(cx);
if let Some(buffer) = buffer.as_singleton() {
let path =
project::File::from_dyn(buffer.read(cx).file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
return Ok(ActiveEditorAttachment {
buffer: buffer.downgrade(),
path,
});
} else {
Err(anyhow!("no active buffer"))
}
}))
}
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| FileAttachmentView { output })
}
}

View File

@@ -1,144 +1 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use serde::{Deserialize, Serialize};
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
#[derive(Serialize, Deserialize)]
pub struct ActiveEditorAttachment {
#[serde(skip)]
buffer: Option<WeakModel<Buffer>>,
path: Option<PathBuf>,
}
pub struct FileAttachmentView {
project_path: Option<ProjectPath>,
buffer: Option<WeakModel<Buffer>>,
error: Option<anyhow::Error>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(error) = &self.error {
return div().child(error.to_string()).into_any_element();
}
let filename: SharedString = self
.project_path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx))
.into_any_element()
}
}
impl ToolOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Some(path) = &self.project_path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
}
if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
String::new()
}
}
pub struct ActiveEditorAttachmentTool {
workspace: WeakView<Workspace>,
}
impl ActiveEditorAttachmentTool {
pub fn new(workspace: WeakView<Workspace>, _cx: &mut WindowContext) -> Self {
Self { workspace }
}
}
impl LanguageModelAttachment for ActiveEditorAttachmentTool {
type Output = ActiveEditorAttachment;
type View = FileAttachmentView;
fn name(&self) -> Arc<str> {
"active-editor-attachment".into()
}
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
Task::ready(maybe!({
let active_buffer = self
.workspace
.update(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()))
})?
.ok_or_else(|| anyhow!("no active buffer"))?;
let buffer = active_buffer.read(cx);
if let Some(buffer) = buffer.as_singleton() {
let path = project::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok());
return Ok(ActiveEditorAttachment {
buffer: Some(buffer.downgrade()),
path,
});
} else {
Err(anyhow!("no active buffer"))
}
}))
}
fn view(
&self,
output: Result<ActiveEditorAttachment>,
cx: &mut WindowContext,
) -> View<Self::View> {
let error;
let project_path;
let buffer;
match output {
Ok(output) => {
error = None;
let workspace = self.workspace.upgrade().unwrap();
let project = workspace.read(cx).project();
project_path = output
.path
.and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx));
buffer = output.buffer;
}
Err(err) => {
error = Some(err);
buffer = None;
project_path = None;
}
}
cx.new_view(|_cx| FileAttachmentView {
project_path,
buffer,
error,
})
}
}

View File

@@ -1,57 +0,0 @@
use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use crate::MessageId;
#[derive(Serialize, Deserialize)]
pub struct SavedConversation {
/// The schema version of the conversation.
pub version: String,
/// The title of the conversation, generated by the Assistant.
pub title: String,
pub messages: Vec<SavedChatMessage>,
}
#[derive(Serialize, Deserialize)]
pub enum SavedChatMessage {
User {
id: MessageId,
body: String,
attachments: Vec<SavedUserAttachment>,
},
Assistant {
id: MessageId,
messages: Vec<SavedAssistantMessagePart>,
error: Option<SharedString>,
},
}
#[derive(Serialize, Deserialize)]
pub struct SavedAssistantMessagePart {
pub body: SharedString,
pub tool_calls: Vec<SavedToolFunctionCall>,
}
/// Returns a list of placeholder conversations for mocking the UI.
///
/// Once we have real saved conversations to pull from we can use those instead.
pub fn placeholder_conversations() -> Vec<SavedConversation> {
vec![
SavedConversation {
version: "0.3.0".to_string(),
title: "How to get a list of exported functions in an Erlang module".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "7 wonders of the ancient world".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "Size difference between u8 and a reference to u8 in Rust".to_string(),
messages: vec![],
},
]
}

View File

@@ -1,188 +0,0 @@
use std::sync::Arc;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakView};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::saved_conversation::{self, SavedConversation};
use crate::ToggleSavedConversations;
pub struct SavedConversationPicker {
picker: View<Picker<SavedConversationPickerDelegate>>,
}
impl EventEmitter<DismissEvent> for SavedConversationPicker {}
impl ModalView for SavedConversationPicker {}
impl FocusableView for SavedConversationPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl SavedConversationPicker {
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| {
workspace.toggle_modal(cx, move |cx| {
let delegate = SavedConversationPickerDelegate::new(cx.view().downgrade());
Self::new(delegate, cx)
});
});
}
pub fn new(delegate: SavedConversationPickerDelegate, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl Render for SavedConversationPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
pub struct SavedConversationPickerDelegate {
view: WeakView<SavedConversationPicker>,
saved_conversations: Vec<SavedConversation>,
selected_index: usize,
matches: Vec<StringMatch>,
}
impl SavedConversationPickerDelegate {
pub fn new(weak_view: WeakView<SavedConversationPicker>) -> Self {
let saved_conversations = saved_conversation::placeholder_conversations();
let matches = saved_conversations
.iter()
.map(|conversation| StringMatch {
candidate_id: 0,
score: 0.0,
positions: Default::default(),
string: conversation.title.clone(),
})
.collect();
Self {
view: weak_view,
saved_conversations,
selected_index: 0,
matches,
}
}
}
impl PickerDelegate for SavedConversationPickerDelegate {
type ListItem = ui::ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select saved conversation...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let background_executor = cx.background_executor().clone();
let candidates = self
.saved_conversations
.iter()
.enumerate()
.map(|(id, conversation)| {
let text = conversation.title.clone();
StringMatchCandidate {
id,
char_bag: text.as_str().into(),
string: text,
}
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background_executor,
)
.await
};
this.update(&mut cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(cx);
return;
}
// TODO: Implement selecting a saved conversation.
}
fn dismissed(&mut self, cx: &mut ui::prelude::ViewContext<Picker<Self>>) {
self.view
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let conversation_match = &self.matches[ix];
let _conversation = &self.saved_conversations[conversation_match.candidate_id];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(
conversation_match.string.clone(),
conversation_match.positions.clone(),
)),
)
}
}

View File

@@ -1,7 +1,5 @@
mod annotate_code;
mod create_buffer;
mod project_index;
pub use annotate_code::*;
pub use create_buffer::*;
pub use project_index::*;

View File

@@ -1,209 +0,0 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use editor::{
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
Editor, MultiBuffer,
};
use gpui::{prelude::*, AnyElement, Model, Task, View, WeakView};
use language::ToPoint;
use project::{search::SearchQuery, Project, ProjectPath};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::Path;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct AnnotationTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl AnnotationTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
pub struct AnnotationInput {
/// Name for this set of annotations
title: String,
/// Excerpts from the file to show to the user.
excerpts: Vec<Excerpt>,
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
struct Excerpt {
/// Path to the file
path: String,
/// A short, distinctive string that appears in the file, used to define a location in the file.
text_passage: String,
/// Text to display above the code excerpt
annotation: String,
}
impl LanguageModelTool for AnnotationTool {
type Input = AnnotationInput;
type Output = String;
type View = AnnotationResultView;
fn name(&self) -> String {
"annotate_code".to_string()
}
fn description(&self) -> String {
"Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let workspace = self.workspace.clone();
let project = self.project.clone();
let excerpts = input.excerpts.clone();
let title = input.title.clone();
let worktree_id = project.update(cx, |project, cx| {
let worktree = project.worktrees().next()?;
let worktree_id = worktree.read(cx).id();
Some(worktree_id)
});
let worktree_id = if let Some(worktree_id) = worktree_id {
worktree_id
} else {
return Task::ready(Err(anyhow::anyhow!("No worktree found")));
};
let buffer_tasks = project.update(cx, |project, cx| {
excerpts
.iter()
.map(|excerpt| {
project.open_buffer(
ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
},
cx,
)
})
.collect::<Vec<_>>()
});
cx.spawn(move |mut cx| async move {
let buffers = futures::future::try_join_all(buffer_tasks).await?;
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(title)
})?;
let editor =
cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))?;
for (excerpt, buffer) in excerpts.iter().zip(buffers.iter()) {
let snapshot = buffer.update(&mut cx, |buffer, _cx| buffer.snapshot())?;
let query =
SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?;
let matches = query.search(&snapshot, None).await;
let Some(first_match) = matches.first() else {
log::warn!(
"text {:?} does not appear in '{}'",
excerpt.text_passage,
excerpt.path
);
continue;
};
let mut start = first_match.start.to_point(&snapshot);
start.column = 0;
editor.update(&mut cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
let annotation = SharedString::from(excerpt.annotation.clone());
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: annotation.split('\n').count() as u8 + 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| Self::render_note_block(&annotation, cx)),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
})?;
}
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
anyhow::Ok("showed comments to users in a new view".into())
})
}
fn view(
&self,
_: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| AnnotationResultView { output })
}
}
impl AnnotationTool {
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
let anchor_x = cx.anchor_x;
let gutter_width = cx.gutter_dimensions.width;
h_flex()
.w_full()
.py_2()
.border_y_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_center()
.w(gutter_width)
.child(Icon::new(IconName::Ai).color(Color::Hint)),
)
.child(
h_flex()
.w_full()
.ml(anchor_x - gutter_width)
.child(explanation.clone()),
)
.into_any_element()
}
}
pub struct AnnotationResultView {
output: Result<String>,
}
impl Render for AnnotationResultView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(output) => div().child(output.clone().into_any_element()),
Err(error) => div().child(format!("failed to open path: {:?}", error)),
}
}
}
impl ToolOutput for AnnotationResultView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(output) => output.clone(),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
}
}

View File

@@ -86,8 +86,7 @@ impl LanguageModelTool for CreateBufferTool {
})
}
fn view(
&self,
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,

View File

@@ -1,13 +1,12 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolOutput};
use collections::BTreeMap;
use gpui::{prelude::*, Model, Task};
use project::ProjectPath;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{fmt::Write as _, ops::Range, path::Path, sync::Arc};
use serde::Deserialize;
use std::{fmt::Write as _, ops::Range};
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
@@ -29,24 +28,28 @@ pub struct CodebaseQuery {
pub struct ProjectIndexView {
input: CodebaseQuery,
status: Status,
excerpts: Result<BTreeMap<ProjectPath, Vec<Range<usize>>>>,
output: Result<ProjectIndexOutput>,
element_id: ElementId,
expanded_header: bool,
}
#[derive(Serialize, Deserialize)]
pub struct ProjectIndexOutput {
status: Status,
worktrees: BTreeMap<Arc<Path>, WorktreeIndexOutput>,
}
#[derive(Serialize, Deserialize)]
struct WorktreeIndexOutput {
excerpts: BTreeMap<Arc<Path>, Vec<Range<usize>>>,
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
}
impl ProjectIndexView {
fn new(input: CodebaseQuery, output: Result<ProjectIndexOutput>) -> Self {
let element_id = ElementId::Name(nanoid::nanoid!().into());
Self {
input,
output,
element_id,
expanded_header: false,
}
}
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
self.expanded_header = !self.expanded_header;
cx.notify();
@@ -56,14 +59,18 @@ impl ProjectIndexView {
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let excerpts = match &self.excerpts {
let result = &self.output;
let output = match result {
Err(err) => {
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
}
Ok(excerpts) => excerpts,
Ok(output) => output,
};
let file_count = excerpts.len();
let file_count = output.excerpts.len();
let header = h_flex()
.gap_2()
.child(Icon::new(IconName::File))
@@ -89,12 +96,16 @@ impl Render for ProjectIndexView {
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.child(v_flex().gap_2().children(excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string())
.color(Color::Muted),
)
}))),
.child(
v_flex()
.gap_2()
.children(output.excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string())
.color(Color::Muted),
)
})),
),
),
)
}
@@ -106,16 +117,16 @@ impl ToolOutput for ProjectIndexView {
context: &mut assistant_tooling::ProjectContext,
_: &mut WindowContext,
) -> String {
match &self.excerpts {
Ok(excerpts) => {
match &self.output {
Ok(output) => {
let mut body = "found results in the following paths:\n".to_string();
for (project_path, ranges) in excerpts {
for (project_path, ranges) in &output.excerpts {
context.add_excerpts(project_path.clone(), ranges);
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
}
if self.status != Status::Idle {
if output.status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
@@ -160,20 +171,16 @@ impl LanguageModelTool for ProjectIndexTool {
cx.update(|cx| {
let mut output = ProjectIndexOutput {
status,
worktrees: Default::default(),
excerpts: Default::default(),
};
for search_result in search_results {
let worktree_path = search_result.worktree.read(cx).abs_path();
let excerpts = &mut output
.worktrees
.entry(worktree_path)
.or_insert(WorktreeIndexOutput {
excerpts: Default::default(),
})
.excerpts;
let path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path.clone(),
};
let excerpts_for_path = excerpts.entry(search_result.path).or_default();
let excerpts_for_path = output.excerpts.entry(path).or_default();
let ix = match excerpts_for_path
.binary_search_by_key(&search_result.range.start, |r| r.start)
{
@@ -187,67 +194,16 @@ impl LanguageModelTool for ProjectIndexTool {
})
}
fn view(
&self,
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|cx| {
let status;
let excerpts;
match output {
Ok(output) => {
status = output.status;
let project_index = self.project_index.read(cx);
if let Some(project) = project_index.project().upgrade() {
let project = project.read(cx);
excerpts = Ok(output
.worktrees
.into_iter()
.filter_map(|(abs_path, output)| {
for worktree in project.worktrees() {
let worktree = worktree.read(cx);
if worktree.abs_path() == abs_path {
return Some((worktree.id(), output.excerpts));
}
}
None
})
.flat_map(|(worktree_id, excerpts)| {
excerpts.into_iter().map(move |(path, ranges)| {
(ProjectPath { worktree_id, path }, ranges)
})
})
.collect::<BTreeMap<_, _>>());
} else {
excerpts = Err(anyhow!("project was dropped"));
}
}
Err(err) => {
status = Status::Idle;
excerpts = Err(err);
}
};
ProjectIndexView {
input,
status,
excerpts,
element_id: ElementId::Name(nanoid::nanoid!().into()),
expanded_header: false,
}
})
cx.new_view(|_cx| ProjectIndexView::new(input, output))
}
fn render_running(arguments: &Option<Value>, _: &mut WindowContext) -> impl IntoElement {
let text: String = arguments
.as_ref()
.and_then(|arguments| arguments.get("query"))
.and_then(|query| query.as_str())
.map(|query| format!("Searching for: {}", query))
.unwrap_or_else(|| "Preparing search...".to_string());
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false).start_slot(text)
fn render_running(_: &mut WindowContext) -> impl IntoElement {
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
.start_slot("Searching code base")
}
}

View File

@@ -22,7 +22,7 @@ pub struct ActiveFileButton {
impl ActiveFileButton {
pub fn new(
attachment_registry: Arc<AttachmentRegistry>,
attachment_store: Arc<AttachmentRegistry>,
workspace: View<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
@@ -31,7 +31,7 @@ impl ActiveFileButton {
cx.defer(move |this, cx| this.update_active_buffer(workspace.clone(), cx));
Self {
attachment_registry,
attachment_registry: attachment_store,
status: Status::NoFile,
workspace_subscription,
}

View File

@@ -15,7 +15,8 @@ pub enum UserOrAssistant {
pub struct ChatMessage {
id: MessageId,
player: UserOrAssistant,
messages: Vec<AnyElement>,
message: Option<AnyElement>,
tools_used: Option<AnyElement>,
selected: bool,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
@@ -25,14 +26,16 @@ impl ChatMessage {
pub fn new(
id: MessageId,
player: UserOrAssistant,
messages: Vec<AnyElement>,
message: Option<AnyElement>,
tools_used: Option<AnyElement>,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
) -> Self {
Self {
id,
player,
messages,
message,
tools_used,
selected: false,
collapsed,
on_collapse_handle_click,
@@ -114,10 +117,19 @@ impl RenderOnce for ChatMessage {
.icon_color(Color::Muted)
.on_click(self.on_collapse_handle_click)
.tooltip(|cx| Tooltip::text("Collapse Message", cx)),
),
), // .child(
// IconButton::new("copy-message", IconName::Copy)
// .icon_color(Color::Muted)
// .icon_size(IconSize::XSmall),
// )
// .child(
// IconButton::new("menu", IconName::Ellipsis)
// .icon_color(Color::Muted)
// .icon_size(IconSize::XSmall),
// ),
),
)
.when(self.messages.len() > 0, |el| {
.when(self.message.is_some() || self.tools_used.is_some(), |el| {
el.child(
h_flex().child(
v_flex()
@@ -132,7 +144,8 @@ impl RenderOnce for ChatMessage {
this.bg(background_color)
})
.when(self.collapsed, |this| this.h(collapsed_height))
.children(self.messages),
.children(self.message)
.when_some(self.tools_used, |this, tools_used| this.child(tools_used)),
),
)
})

View File

@@ -48,73 +48,68 @@ impl RenderOnce for Composer {
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let mut editor_border = cx.theme().colors().text;
editor_border.fade_out(0.90);
// Remove the extra 1px added by the border
let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.);
h_flex()
.p(Spacing::Small.rems(cx))
.w_full()
.items_start()
.child(
v_flex()
.w_full()
.rounded_lg()
.p(padding)
.border_1()
.border_color(editor_border)
.bg(cx.theme().colors().editor_background)
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
v_flex().size_full().gap_1().child(
v_flex()
.w_full()
.p_3()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
.child(h_flex().gap_1().child(self.model_selector)),
),
),
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
)
.child(h_flex().gap_1().child(self.model_selector)),
),
),
),
)
}
}

View File

@@ -28,7 +28,8 @@ impl Render for ChatMessageStory {
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What can I do here?").into_any_element()],
Some(div().child("What can I do here?").into_any_element()),
None,
false,
Box::new(|_, _| {}),
),
@@ -38,7 +39,8 @@ impl Render for ChatMessageStory {
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What can I do here?").into_any_element()],
Some(div().child("What can I do here?").into_any_element()),
None,
true,
Box::new(|_, _| {}),
),
@@ -51,7 +53,8 @@ impl Render for ChatMessageStory {
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child("You can talk to me!").into_any_element()],
Some(div().child("You can talk to me!").into_any_element()),
None,
false,
Box::new(|_, _| {}),
),
@@ -61,7 +64,8 @@ impl Render for ChatMessageStory {
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child(MULTI_LINE_MESSAGE).into_any_element()],
Some(div().child(MULTI_LINE_MESSAGE).into_any_element()),
None,
true,
Box::new(|_, _| {}),
),
@@ -75,21 +79,24 @@ impl Render for ChatMessageStory {
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What is Rust??").into_any_element()],
Some(div().child("What is Rust??").into_any_element()),
None,
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()],
Some(div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()),
None,
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1)),
vec![div().child("Sounds pretty cool!").into_any_element()],
Some(div().child("Sounds pretty cool!").into_any_element()),
None,
false,
Box::new(|_, _| {}),
)),

View File

@@ -21,7 +21,6 @@ schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
sum_tree.workspace = true
ui.workspace = true
util.workspace = true
[dev-dependencies]

View File

@@ -2,12 +2,8 @@ mod attachment_registry;
mod project_context;
mod tool_registry;
pub use attachment_registry::{
AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment, UserAttachment,
};
pub use attachment_registry::{AttachmentRegistry, LanguageModelAttachment, UserAttachment};
pub use project_context::ProjectContext;
pub use tool_registry::{
tool_running_placeholder, LanguageModelTool, SavedToolFunctionCall,
SavedToolFunctionCallResult, ToolFunctionCall, ToolFunctionCallResult, ToolFunctionDefinition,
ToolOutput, ToolRegistry,
LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition, ToolOutput, ToolRegistry,
};

View File

@@ -3,8 +3,6 @@ use anyhow::{anyhow, Result};
use collections::HashMap;
use futures::future::join_all;
use gpui::{AnyView, Render, Task, View, WindowContext};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::value::RawValue;
use std::{
any::TypeId,
sync::{
@@ -19,34 +17,24 @@ pub struct AttachmentRegistry {
}
pub trait LanguageModelAttachment {
type Output: DeserializeOwned + Serialize + 'static;
type Output: 'static;
type View: Render + ToolOutput;
fn name(&self) -> Arc<str>;
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
}
/// A collected attachment from running an attachment tool
pub struct UserAttachment {
pub view: AnyView,
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
}
#[derive(Serialize, Deserialize)]
pub struct SavedUserAttachment {
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
}
/// Internal representation of an attachment tool to allow us to treat them dynamically
struct RegisteredAttachment {
name: Arc<str>,
enabled: AtomicBool,
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
}
impl AttachmentRegistry {
@@ -57,65 +45,24 @@ impl AttachmentRegistry {
}
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
let attachment = Arc::new(attachment);
let call = Box::new(move |cx: &mut WindowContext| {
let result = attachment.run(cx);
let call = Box::new({
let attachment = attachment.clone();
move |cx: &mut WindowContext| {
let result = attachment.run(cx);
let attachment = attachment.clone();
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let serialized_output =
result
.as_ref()
.map_err(ToString::to_string)
.and_then(|output| {
Ok(RawValue::from_string(
serde_json::to_string(output).map_err(|e| e.to_string())?,
)
.unwrap())
});
let view = cx.update(|cx| attachment.view(result, cx))?;
Ok(UserAttachment {
name: attachment.name(),
view: view.into(),
generate_fn: generate::<A>,
serialized_output,
})
})
}
});
let deserialize = Box::new({
let attachment = attachment.clone();
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
let serialized_output = saved_attachment.serialized_output.clone();
let output = match &serialized_output {
Ok(serialized_output) => {
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
}
Err(error) => Err(anyhow!("{error}")),
};
let view = attachment.view(output, cx).into();
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let view = cx.update(|cx| A::view(result, cx))?;
Ok(UserAttachment {
name: saved_attachment.name.clone(),
view,
serialized_output,
view: view.into(),
generate_fn: generate::<A>,
})
}
})
});
self.registered_attachments.insert(
TypeId::of::<A>(),
RegisteredAttachment {
name: attachment.name(),
call,
deserialize,
enabled: AtomicBool::new(true),
},
);
@@ -187,35 +134,6 @@ impl AttachmentRegistry {
.collect())
})
}
pub fn serialize_user_attachment(
&self,
user_attachment: &UserAttachment,
) -> SavedUserAttachment {
SavedUserAttachment {
name: user_attachment.name.clone(),
serialized_output: user_attachment.serialized_output.clone(),
}
}
pub fn deserialize_user_attachment(
&self,
saved_user_attachment: SavedUserAttachment,
cx: &mut WindowContext,
) -> Result<UserAttachment> {
if let Some(registered_attachment) = self
.registered_attachments
.values()
.find(|attachment| attachment.name == saved_user_attachment.name)
{
(registered_attachment.deserialize)(&saved_user_attachment, cx)
} else {
Err(anyhow!(
"no attachment tool for name {}",
saved_user_attachment.name
))
}
}
}
impl UserAttachment {

View File

@@ -1,60 +1,40 @@
use crate::ProjectContext;
use anyhow::{anyhow, Result};
use gpui::{
div, AnyElement, AnyView, IntoElement, ParentElement, Render, Styled, Task, View, WindowContext,
};
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{value::RawValue, Value};
use serde::Deserialize;
use std::{
any::TypeId,
collections::HashMap,
fmt::Display,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
sync::atomic::{AtomicBool, Ordering::SeqCst},
};
use crate::ProjectContext;
pub struct ToolRegistry {
registered_tools: HashMap<String, RegisteredTool>,
}
#[derive(Default)]
#[derive(Default, Deserialize)]
pub struct ToolFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
#[serde(skip)]
pub result: Option<ToolFunctionCallResult>,
}
#[derive(Default, Serialize, Deserialize)]
pub struct SavedToolFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
pub result: Option<SavedToolFunctionCallResult>,
}
pub enum ToolFunctionCallResult {
NoSuchTool,
ParsingFailed,
Finished {
view: AnyView,
serialized_output: Result<Box<RawValue>, String>,
generate_fn: fn(AnyView, &mut ProjectContext, &mut WindowContext) -> String,
},
}
#[derive(Serialize, Deserialize)]
pub enum SavedToolFunctionCallResult {
NoSuchTool,
ParsingFailed,
Finished {
serialized_output: Result<Box<RawValue>, String>,
},
}
#[derive(Clone)]
pub struct ToolFunctionDefinition {
pub name: String,
@@ -65,10 +45,10 @@ pub struct ToolFunctionDefinition {
pub trait LanguageModelTool {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: DeserializeOwned + JsonSchema;
type Input: for<'de> Deserialize<'de> + JsonSchema;
/// The output returned by executing the tool.
type Output: DeserializeOwned + Serialize + 'static;
type Output: 'static;
type View: Render + ToolOutput;
@@ -98,23 +78,17 @@ pub trait LanguageModelTool {
/// Executes the tool with the given input.
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
/// A view of the output of running the tool, for displaying to the user.
fn view(
&self,
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View>;
fn render_running(_arguments: &Option<Value>, _cx: &mut WindowContext) -> impl IntoElement {
tool_running_placeholder()
fn render_running(_cx: &mut WindowContext) -> impl IntoElement {
div()
}
}
pub fn tool_running_placeholder() -> AnyElement {
ui::Label::new("Researching...").into_any_element()
}
pub trait ToolOutput: Sized {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
}
@@ -122,9 +96,8 @@ pub trait ToolOutput: Sized {
struct RegisteredTool {
enabled: AtomicBool,
type_id: TypeId,
execute: Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
deserialize: Box<dyn Fn(&SavedToolFunctionCall, &mut WindowContext) -> ToolFunctionCall>,
render_running: fn(&ToolFunctionCall, &mut WindowContext) -> gpui::AnyElement,
call: Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
render_running: fn(&mut WindowContext) -> gpui::AnyElement,
definition: ToolFunctionDefinition,
}
@@ -171,52 +144,11 @@ impl ToolRegistry {
.p_2()
.child(result.into_any_element(&tool_call.name))
.into_any_element(),
None => {
let tool = self.registered_tools.get(&tool_call.name);
if let Some(tool) = tool {
(tool.render_running)(&tool_call, cx)
} else {
tool_running_placeholder()
}
}
}
}
pub fn serialize_tool_call(&self, call: &ToolFunctionCall) -> SavedToolFunctionCall {
SavedToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
result: call.result.as_ref().map(|result| match result {
ToolFunctionCallResult::NoSuchTool => SavedToolFunctionCallResult::NoSuchTool,
ToolFunctionCallResult::ParsingFailed => SavedToolFunctionCallResult::ParsingFailed,
ToolFunctionCallResult::Finished {
serialized_output, ..
} => SavedToolFunctionCallResult::Finished {
serialized_output: match serialized_output {
Ok(value) => Ok(value.clone()),
Err(e) => Err(e.to_string()),
},
},
}),
}
}
pub fn deserialize_tool_call(
&self,
call: &SavedToolFunctionCall,
cx: &mut WindowContext,
) -> ToolFunctionCall {
if let Some(tool) = &self.registered_tools.get(&call.name) {
(tool.deserialize)(call, cx)
} else {
ToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
result: Some(ToolFunctionCallResult::NoSuchTool),
}
None => self
.registered_tools
.get(&tool_call.name)
.map(|tool| (tool.render_running)(cx))
.unwrap_or_else(|| div().into_any_element()),
}
}
@@ -226,82 +158,17 @@ impl ToolRegistry {
_cx: &mut WindowContext,
) -> Result<()> {
let name = tool.name();
let tool = Arc::new(tool);
let registered_tool = RegisteredTool {
type_id: TypeId::of::<T>(),
definition: tool.definition(),
enabled: AtomicBool::new(true),
deserialize: Box::new({
let tool = tool.clone();
move |tool_call: &SavedToolFunctionCall, cx: &mut WindowContext| {
let id = tool_call.id.clone();
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let Ok(input) = serde_json::from_str::<T::Input>(&tool_call.arguments) else {
return ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::ParsingFailed),
};
};
let result = match &tool_call.result {
Some(result) => match result {
SavedToolFunctionCallResult::NoSuchTool => {
Some(ToolFunctionCallResult::NoSuchTool)
}
SavedToolFunctionCallResult::ParsingFailed => {
Some(ToolFunctionCallResult::ParsingFailed)
}
SavedToolFunctionCallResult::Finished { serialized_output } => {
let output = match serialized_output {
Ok(value) => {
match serde_json::from_str::<T::Output>(value.get()) {
Ok(value) => Ok(value),
Err(_) => {
return ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(
ToolFunctionCallResult::ParsingFailed,
),
};
}
}
}
Err(e) => Err(anyhow!("{e}")),
};
let view = tool.view(input, output, cx).into();
Some(ToolFunctionCallResult::Finished {
serialized_output: serialized_output.clone(),
generate_fn: generate::<T>,
view,
})
}
},
None => None,
};
ToolFunctionCall {
id: tool_call.id.clone(),
name: name.clone(),
arguments: tool_call.arguments.clone(),
result,
}
}
}),
execute: Box::new({
let tool = tool.clone();
call: Box::new(
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
let id = tool_call.id.clone();
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let Ok(input) = serde_json::from_str::<T::Input>(&arguments) else {
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
@@ -311,33 +178,23 @@ impl ToolRegistry {
};
let result = tool.execute(&input, cx);
let tool = tool.clone();
cx.spawn(move |mut cx| async move {
let result = result.await;
let serialized_output = result
.as_ref()
.map_err(ToString::to_string)
.and_then(|output| {
Ok(RawValue::from_string(
serde_json::to_string(output).map_err(|e| e.to_string())?,
)
.unwrap())
});
let view = cx.update(|cx| tool.view(input, result, cx))?;
let result: Result<T::Output> = result.await;
let view = cx.update(|cx| T::output_view(input, result, cx))?;
Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::Finished {
serialized_output,
view: view.into(),
generate_fn: generate::<T>,
}),
})
})
}
}),
},
),
render_running: render_running::<T>,
};
@@ -348,14 +205,8 @@ impl ToolRegistry {
return Ok(());
fn render_running<T: LanguageModelTool>(
tool_call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> AnyElement {
// Attempt to parse the string arguments that are JSON as a JSON value
let maybe_arguments = serde_json::to_value(tool_call.arguments.clone()).ok();
T::render_running(&maybe_arguments, cx).into_any_element()
fn render_running<T: LanguageModelTool>(cx: &mut WindowContext) -> AnyElement {
T::render_running(cx).into_any_element()
}
fn generate<T: LanguageModelTool>(
@@ -392,7 +243,7 @@ impl ToolRegistry {
}
};
(tool.execute)(tool_call, cx)
(tool.call)(tool_call, cx)
}
}
@@ -408,9 +259,9 @@ impl ToolFunctionCallResult {
ToolFunctionCallResult::ParsingFailed => {
format!("Unable to parse arguments for {name}")
}
ToolFunctionCallResult::Finished {
generate_fn, view, ..
} => (generate_fn)(view.clone(), project, cx),
ToolFunctionCallResult::Finished { generate_fn, view } => {
(generate_fn)(view.clone(), project, cx)
}
}
}
@@ -506,8 +357,7 @@ mod test {
Task::ready(Ok(weather))
}
fn view(
&self,
fn output_view(
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,

View File

@@ -56,22 +56,16 @@ struct UpdateRequestBody {
telemetry: bool,
}
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
Checking,
Downloading,
Installing,
Updated { binary_path: PathBuf },
Updated,
Errored,
}
impl AutoUpdateStatus {
pub fn is_updated(&self) -> bool {
matches!(self, Self::Updated { .. })
}
}
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: SemanticVersion,
@@ -312,7 +306,7 @@ impl AutoUpdater {
}
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
if self.pending_poll.is_some() || self.status.is_updated() {
if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
return;
}
@@ -334,7 +328,7 @@ impl AutoUpdater {
}
pub fn status(&self) -> AutoUpdateStatus {
self.status.clone()
self.status
}
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
@@ -410,11 +404,6 @@ impl AutoUpdater {
cx.notify();
})?;
// We store the path of our current binary, before we install, since installation might
// delete it. Once deleted, it's hard to get the path to our binary on Linux.
// So we cache it here, which allows us to then restart later on.
let binary_path = cx.update(|cx| cx.app_path())??;
match OS {
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
@@ -424,7 +413,7 @@ impl AutoUpdater {
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated { binary_path };
this.status = AutoUpdateStatus::Updated;
cx.notify();
})?;

View File

@@ -16,38 +16,39 @@ doctest = false
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
util.workspace = true
release_channel.workspace = true
rpc.workspace = true
text.workspace = true
settings.workspace = true
feature_flags.workspace = true
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
futures.workspace = true
lazy_static.workspace = true
log.workspace = true
once_cell = "1.19.0"
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
release_channel.workspace = true
rpc.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
sha2.workspace = true
smol.workspace = true
sysinfo.workspace = true
telemetry_events.workspace = true
tempfile.workspace = true
text.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
url.workspace = true
util.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }

View File

@@ -30,7 +30,6 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use std::fmt;
use std::pin::Pin;
use std::{
any::TypeId,
convert::TryFrom,
@@ -66,13 +65,6 @@ impl fmt::Display for DevServerToken {
lazy_static! {
static ref ZED_SERVER_URL: Option<String> = std::env::var("ZED_SERVER_URL").ok();
static ref ZED_RPC_URL: Option<String> = std::env::var("ZED_RPC_URL").ok();
/// An environment variable whose presence indicates that the development auth
/// provider should be used.
///
/// Only works in development. Setting this environment variable in other release
/// channels is a no-op.
pub static ref ZED_DEVELOPMENT_AUTH: bool =
std::env::var("ZED_DEVELOPMENT_AUTH").map_or(false, |value| !value.is_empty());
pub static ref IMPERSONATE_LOGIN: Option<String> = std::env::var("ZED_IMPERSONATE")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
@@ -169,7 +161,6 @@ pub struct Client {
peer: Arc<Peer>,
http: Arc<HttpClientWithUrl>,
telemetry: Arc<Telemetry>,
credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static>,
state: RwLock<ClientState>,
#[allow(clippy::type_complexity)]
@@ -307,32 +298,6 @@ impl Credentials {
}
}
/// A provider for [`Credentials`].
///
/// Used to abstract over reading and writing credentials to some form of
/// persistence (like the system keychain).
trait CredentialsProvider {
/// Reads the credentials from the provider.
fn read_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>>;
/// Writes the credentials to the provider.
fn write_credentials<'a>(
&'a self,
user_id: u64,
access_token: String,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
/// Deletes the credentials from the provider.
fn delete_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
}
impl Default for ClientState {
fn default() -> Self {
Self {
@@ -478,27 +443,11 @@ impl Client {
http: Arc<HttpClientWithUrl>,
cx: &mut AppContext,
) -> Arc<Self> {
let use_zed_development_auth = match ReleaseChannel::try_global(cx) {
Some(ReleaseChannel::Dev) => *ZED_DEVELOPMENT_AUTH,
Some(ReleaseChannel::Nightly | ReleaseChannel::Preview | ReleaseChannel::Stable)
| None => false,
};
let credentials_provider: Arc<dyn CredentialsProvider + Send + Sync + 'static> =
if use_zed_development_auth {
Arc::new(DevelopmentCredentialsProvider {
path: util::paths::CONFIG_DIR.join("development_auth"),
})
} else {
Arc::new(KeychainCredentialsProvider)
};
Arc::new(Self {
id: AtomicU64::new(0),
peer: Peer::new(0),
telemetry: Telemetry::new(clock, http.clone(), cx),
http,
credentials_provider,
state: Default::default(),
#[cfg(any(test, feature = "test-support"))]
@@ -814,11 +763,8 @@ impl Client {
}
}
pub async fn has_credentials(&self, cx: &AsyncAppContext) -> bool {
self.credentials_provider
.read_credentials(cx)
.await
.is_some()
pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
read_credentials_from_keychain(cx).await.is_some()
}
pub fn set_dev_server_token(&self, token: DevServerToken) -> &Self {
@@ -829,7 +775,7 @@ impl Client {
#[async_recursion(?Send)]
pub async fn authenticate_and_connect(
self: &Arc<Self>,
try_provider: bool,
try_keychain: bool,
cx: &AsyncAppContext,
) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() {
@@ -850,13 +796,12 @@ impl Client {
self.set_status(Status::Reauthenticating, cx)
}
let mut read_from_provider = false;
let mut read_from_keychain = false;
let mut credentials = self.state.read().credentials.clone();
if credentials.is_none() && try_provider {
credentials = self.credentials_provider.read_credentials(cx).await;
read_from_provider = credentials.is_some();
if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx).await;
read_from_keychain = credentials.is_some();
}
if credentials.is_none() {
let mut status_rx = self.status();
let _ = status_rx.next().await;
@@ -893,9 +838,9 @@ impl Client {
match connection {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
if let Credentials::User{user_id, access_token} = credentials {
self.credentials_provider.write_credentials(user_id, access_token, cx).await.log_err();
write_credentials_to_keychain(user_id, access_token, cx).await.log_err();
}
}
@@ -909,8 +854,8 @@ impl Client {
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if read_from_provider {
self.credentials_provider.delete_credentials(cx).await.log_err();
if read_from_keychain {
delete_credentials_from_keychain(cx).await.log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
@@ -1319,11 +1264,8 @@ impl Client {
self.state.write().credentials = None;
self.disconnect(&cx);
if self.has_credentials(cx).await {
self.credentials_provider
.delete_credentials(cx)
.await
.log_err();
if self.has_keychain_credentials(cx).await {
delete_credentials_from_keychain(cx).await.log_err();
}
}
@@ -1523,128 +1465,41 @@ impl Client {
}
}
#[derive(Serialize, Deserialize)]
struct DevelopmentCredentials {
async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let (user_id, access_token) = cx
.update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
.log_err()?
.await
.log_err()??;
Some(Credentials::User {
user_id: user_id.parse().ok()?,
access_token: String::from_utf8(access_token).ok()?,
})
}
async fn write_credentials_to_keychain(
user_id: u64,
access_token: String,
cx: &AsyncAppContext,
) -> Result<()> {
cx.update(move |cx| {
cx.write_credentials(
&ClientSettings::get_global(cx).server_url,
&user_id.to_string(),
access_token.as_bytes(),
)
})?
.await
}
/// A credentials provider that stores credentials in a local file.
///
/// This MUST only be used in development, as this is not a secure way of storing
/// credentials on user machines.
///
/// Its existence is purely to work around the annoyance of having to constantly
/// re-allow access to the system keychain when developing Zed.
struct DevelopmentCredentialsProvider {
path: PathBuf,
}
impl CredentialsProvider for DevelopmentCredentialsProvider {
fn read_credentials<'a>(
&'a self,
_cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
async move {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let json = std::fs::read(&self.path).log_err()?;
let credentials: DevelopmentCredentials = serde_json::from_slice(&json).log_err()?;
Some(Credentials::User {
user_id: credentials.user_id,
access_token: credentials.access_token,
})
}
.boxed_local()
}
fn write_credentials<'a>(
&'a self,
user_id: u64,
access_token: String,
_cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
let json = serde_json::to_string(&DevelopmentCredentials {
user_id,
access_token,
})?;
std::fs::write(&self.path, json)?;
Ok(())
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
_cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move { Ok(std::fs::remove_file(&self.path)?) }.boxed_local()
}
}
/// A credentials provider that stores credentials in the system keychain.
struct KeychainCredentialsProvider;
impl CredentialsProvider for KeychainCredentialsProvider {
fn read_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Option<Credentials>> + 'a>> {
async move {
if IMPERSONATE_LOGIN.is_some() {
return None;
}
let (user_id, access_token) = cx
.update(|cx| cx.read_credentials(&ClientSettings::get_global(cx).server_url))
.log_err()?
.await
.log_err()??;
Some(Credentials::User {
user_id: user_id.parse().ok()?,
access_token: String::from_utf8(access_token).ok()?,
})
}
.boxed_local()
}
fn write_credentials<'a>(
&'a self,
user_id: u64,
access_token: String,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
cx.update(move |cx| {
cx.write_credentials(
&ClientSettings::get_global(cx).server_url,
&user_id.to_string(),
access_token.as_bytes(),
)
})?
.await
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
.await
}
.boxed_local()
}
async fn delete_credentials_from_keychain(cx: &AsyncAppContext) -> Result<()> {
cx.update(move |cx| cx.delete_credentials(&ClientSettings::get_global(cx).server_url))?
.await
}
/// prefix for the zed:// url scheme

View File

@@ -445,15 +445,12 @@ impl Telemetry {
installation_id: state.installation_id.as_deref().map(Into::into),
session_id: state.session_id.clone(),
is_staff: state.is_staff,
app_version: state
.app_metadata
.app_version
.unwrap_or_default()
.to_string(),
os_name: state.app_metadata.os_name.to_string(),
app_version: state.app_metadata.version.unwrap_or_default().to_string(),
os_name: state.app_metadata.os.name.to_string(),
os_version: state
.app_metadata
.os_version
.os
.version
.map(|version| version.to_string()),
architecture: state.architecture.to_string(),

View File

@@ -677,7 +677,7 @@ impl CollabTitlebarItem {
client::Status::UpgradeRequired => {
let auto_updater = auto_update::AutoUpdater::get(cx);
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
Some(AutoUpdateStatus::Installing)
| Some(AutoUpdateStatus::Downloading)
| Some(AutoUpdateStatus::Checking) => "Updating...",
@@ -691,7 +691,7 @@ impl CollabTitlebarItem {
.label_size(LabelSize::Small)
.on_click(|_, cx| {
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
if auto_updater.read(cx).status().is_updated() {
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
workspace::restart(&Default::default(), cx);
return;
}

View File

@@ -14,7 +14,7 @@ pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::{
actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task,
WindowBackgroundAppearance, WindowBounds, WindowContext, WindowKind, WindowOptions,
WindowBackgroundAppearance, WindowContext, WindowKind, WindowOptions,
};
use panel_settings::MessageEditorSettings;
pub use panel_settings::{
@@ -117,13 +117,14 @@ fn notification_window_options(
let app_id = ReleaseChannel::global(cx).app_id();
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
bounds: Some(bounds),
titlebar: None,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
display_id: Some(screen.id()),
fullscreen: false,
window_background: WindowBackgroundAppearance::default(),
app_id: Some(app_id.to_owned()),
}

View File

@@ -215,12 +215,12 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
}
}
fn active_completion_text<'a>(
&'a self,
fn active_completion_text(
&self,
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
) -> Option<&'a str> {
cx: &AppContext,
) -> Option<&str> {
let buffer_id = buffer.entity_id();
let buffer = buffer.read(cx);
let completion = self.active_completion()?;

View File

@@ -128,7 +128,8 @@ impl Render for BlameEntryTooltip {
let author_email = self.blame_entry.author_mail.clone();
let short_commit_id = self.blame_entry.sha.display_short();
let pretty_commit_id = format!("{}", self.blame_entry.sha);
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
let message = self

View File

@@ -458,7 +458,7 @@ pub struct Editor {
find_all_references_task_sources: Vec<Anchor>,
next_completion_id: CompletionId,
completion_documentation_pre_resolve_debounce: DebouncedDelay,
available_code_actions: Option<(Location, Arc<[CodeAction]>)>,
available_code_actions: Option<(Model<Buffer>, Arc<[CodeAction]>)>,
code_actions_task: Option<Task<()>>,
document_highlights_task: Option<Task<()>>,
pending_rename: Option<RenameState>,
@@ -3851,32 +3851,30 @@ impl Editor {
let spawned_test_task = this.update(&mut cx, |this, cx| {
if this.focus_handle.is_focused(cx) {
let buffer_row = action
.deployed_from_indicator
.unwrap_or_else(|| this.selections.newest::<Point>(cx).head().row);
let tasks = this.tasks.get(&buffer_row).map(|t| Arc::new(t.to_owned()));
let (location, code_actions) = this
.available_code_actions
.clone()
.and_then(|(location, code_actions)| {
let snapshot = location.buffer.read(cx).snapshot();
let point_range = location.range.to_point(&snapshot);
let point_range = point_range.start.row..=point_range.end.row;
if point_range.contains(&buffer_row) {
Some((location, code_actions))
} else {
None
}
})
.unzip();
let snapshot = this.snapshot(cx);
let display_row = action.deployed_from_indicator.unwrap_or_else(|| {
this.selections
.newest::<Point>(cx)
.head()
.to_display_point(&snapshot.display_snapshot)
.row()
});
let buffer_point =
DisplayPoint::new(display_row, 0).to_point(&snapshot.display_snapshot);
let buffer_row = snapshot
.buffer_snapshot
.buffer_line_for_row(buffer_point.row)
.map(|(_, Range { start, .. })| start);
let tasks = this.tasks.get(&display_row).map(|t| Arc::new(t.to_owned()));
let (buffer, code_actions) = this.available_code_actions.clone().unzip();
if tasks.is_none() && code_actions.is_none() {
return None;
}
let buffer = location.map(|location| location.buffer).or_else(|| {
let buffer = buffer.or_else(|| {
let snapshot = this.snapshot(cx);
let (buffer_snapshot, _) =
snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
snapshot.buffer_snapshot.buffer_line_for_row(display_row)?;
let buffer_id = buffer_snapshot.remote_id();
this.buffer().read(cx).buffer(buffer_id)
});
@@ -3887,18 +3885,22 @@ impl Editor {
this.discard_inline_completion(cx);
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|(tasks, (workspace, _))| {
let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
workspace
.update(cx, |workspace, cx| {
tasks::task_context_for_location(workspace, location, cx)
})
.ok()
.flatten()
if let Some(buffer_point) = buffer_row {
let position = Point::new(buffer_point.row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
workspace
.update(cx, |workspace, cx| {
tasks::task_context_for_location(workspace, location, cx)
})
.ok()
.flatten()
} else {
None
}
},
);
let tasks = tasks
@@ -3914,7 +3916,7 @@ impl Editor {
.map(|task| (kind.clone(), task))
})
.collect(),
position: Point::new(buffer_row, tasks.column),
position: Point::new(display_row, tasks.column),
})
});
let spawn_straight_away = tasks
@@ -3981,7 +3983,7 @@ impl Editor {
cx,
);
Some(Task::ready(Ok(())))
None
})
}
CodeActionsItem::CodeAction(action) => {
@@ -4122,13 +4124,7 @@ impl Editor {
this.available_code_actions = if actions.is_empty() {
None
} else {
Some((
Location {
buffer: start_buffer,
range: start..end,
},
actions.into(),
))
Some((start_buffer, actions.into()))
};
cx.notify();
})
@@ -4360,7 +4356,6 @@ impl Editor {
text: completion.text.to_string().into(),
});
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
self.refresh_inline_completion(true, cx);
cx.notify();
true
} else {
@@ -7722,7 +7717,12 @@ impl Editor {
}
})
.await;
let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone());
let rows = Self::refresh_runnable_display_rows(
project,
display_snapshot,
new_rows,
cx.clone(),
);
this.update(&mut cx, |this, _| {
this.clear_tasks();
@@ -7739,8 +7739,7 @@ impl Editor {
) -> Vec<(Range<usize>, Runnable)> {
snapshot.buffer_snapshot.runnable_ranges(range).collect()
}
fn runnable_rows(
fn refresh_runnable_display_rows(
project: Model<Project>,
snapshot: DisplaySnapshot,
runnable_ranges: Vec<(Range<usize>, Runnable)>,
@@ -7755,12 +7754,12 @@ impl Editor {
if tasks.is_empty() {
return None;
}
let point = multi_buffer_range.start.to_point(&snapshot.buffer_snapshot);
let point = multi_buffer_range.start.to_display_point(&snapshot);
Some((
point.row,
point.row(),
RunnableTasks {
templates: tasks,
column: point.column,
column: point.column(),
},
))
})

View File

@@ -9,7 +9,7 @@ use crate::{
JoinLines,
};
use futures::StreamExt;
use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
use gpui::{div, TestAppContext, VisualTestContext, WindowOptions};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
@@ -2826,31 +2826,6 @@ async fn test_join_lines_with_git_diff_base(
);
}
#[gpui::test]
async fn test_custom_newlines_cause_no_false_positive_diffs(
executor: BackgroundExecutor,
cx: &mut gpui::TestAppContext,
) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
cx.set_diff_base(Some("Line 0\r\nLine 1\r\nLine 2\r\nLine 3"));
executor.run_until_parked();
cx.update_editor(|editor, cx| {
assert_eq!(
editor
.buffer()
.read(cx)
.snapshot(cx)
.git_diff_hunks_in_range(0..u32::MAX)
.collect::<Vec<_>>(),
Vec::new(),
"Should not have any diffs for files with custom newlines"
);
});
}
#[gpui::test]
async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -7493,10 +7468,10 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
let follower = cx.update(|cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
bounds: Some(Bounds::from_corners(
gpui::Point::new(0.into(), 0.into()),
gpui::Point::new(10.into(), 80.into()),
))),
)),
..Default::default()
},
|cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
@@ -9051,7 +9026,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
.collect::<String>(),
cx,
);
buffer.set_diff_base(Some(sample_text), cx);
buffer.set_diff_base(Some(sample_text.into()), cx);
});
cx.executor().run_until_parked();
}
@@ -10066,17 +10041,17 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut gpui::TestAppContext)
"vvvv\nwwww\nxxxx\nyyyy\nzzzz\n@@@@\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
let buffer_1 = cx.new_model(|cx| {
let mut buffer = Buffer::local(modified_sample_text_1.to_string(), cx);
buffer.set_diff_base(Some(sample_text_1.clone()), cx);
buffer.set_diff_base(Some(sample_text_1.clone().into()), cx);
buffer
});
let buffer_2 = cx.new_model(|cx| {
let mut buffer = Buffer::local(modified_sample_text_2.to_string(), cx);
buffer.set_diff_base(Some(sample_text_2.clone()), cx);
buffer.set_diff_base(Some(sample_text_2.clone().into()), cx);
buffer
});
let buffer_3 = cx.new_model(|cx| {
let mut buffer = Buffer::local(modified_sample_text_3.to_string(), cx);
buffer.set_diff_base(Some(sample_text_3.clone()), cx);
buffer.set_diff_base(Some(sample_text_3.clone().into()), cx);
buffer
});

View File

@@ -638,11 +638,7 @@ impl EditorElement {
editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
if let Some(point) = point_for_position.as_valid() {
let anchor = position_map
.snapshot
.buffer_snapshot
.anchor_before(point.to_offset(&position_map.snapshot, Bias::Left));
hover_at(editor, Some(anchor), cx);
hover_at(editor, Some((point, &position_map.snapshot)), cx);
Self::update_visible_cursor(editor, point, position_map, cx);
} else {
hover_at(editor, None, cx);
@@ -1385,7 +1381,6 @@ impl EditorElement {
scroll_pixel_position: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
@@ -1414,12 +1409,10 @@ impl EditorElement {
*row,
cx,
);
let display_row = Point::new(*row, 0)
.to_display_point(&snapshot.display_snapshot)
.row();
let button = prepaint_gutter_button(
button,
display_row,
*row,
line_height,
gutter_dimensions,
scroll_pixel_position,
@@ -2086,10 +2079,6 @@ impl EditorElement {
crate::ContextMenuOrigin::GutterIndicator(row) => {
// Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the indicator than just a plain first column of the
// text field.
let snapshot = self.editor.update(cx, |this, cx| this.snapshot(cx));
let row = Point::new(row, 0)
.to_display_point(&snapshot.display_snapshot)
.row();
let x = -gutter_overshoot;
let y = (row + 1) as f32 * line_height - scroll_pixel_position.y;
(x, y)
@@ -3371,7 +3360,8 @@ fn render_blame_entry(
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
let short_commit_id = blame_entry.sha.display_short();
let pretty_commit_id = format!("{}", blame_entry.sha);
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
let name = util::truncate_and_trailoff(author_name, 20);
@@ -4030,13 +4020,11 @@ impl Element for EditorElement {
cx,
);
if gutter_settings.code_actions {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
let has_test_indicator = self
.editor
.read(cx)
.tasks
.contains_key(&newest_selection_point.row);
.contains_key(&newest_selection_head.row());
if !has_test_indicator {
code_actions_indicator = self.layout_code_actions_indicator(
line_height,
@@ -4056,7 +4044,6 @@ impl Element for EditorElement {
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
&snapshot,
cx,
);
@@ -4495,26 +4482,20 @@ fn layout_line(
) -> Result<ShapedLine> {
let mut line = snapshot.line(row);
let len = {
let line_len = line.len();
if line_len > MAX_LINE_LEN {
let mut len = MAX_LINE_LEN;
while !line.is_char_boundary(len) {
len -= 1;
}
line.truncate(len);
len
} else {
line_len
if line.len() > MAX_LINE_LEN {
let mut len = MAX_LINE_LEN;
while !line.is_char_boundary(len) {
len -= 1;
}
};
line.truncate(len);
}
cx.text_system().shape_line(
line.into(),
style.text.font_size.to_pixels(cx.rem_size()),
&[TextRun {
len,
len: snapshot.line_len(row) as usize,
font: style.text.font(),
color: Hsla::default(),
background_color: None,

View File

@@ -145,7 +145,8 @@ mod tests {
1.five
1.six
"
.unindent(),
.unindent()
.into(),
),
cx,
);
@@ -181,7 +182,8 @@ mod tests {
2.four
2.six
"
.unindent(),
.unindent()
.into(),
),
cx,
);

View File

@@ -10,10 +10,9 @@ use gpui::{
ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
ViewContext, WeakView,
};
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
use lsp::DiagnosticSeverity;
use multi_buffer::ToOffset;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
use smol::stream::StreamExt;
@@ -31,16 +30,21 @@ pub const HOVER_POPOVER_GAP: Pixels = px(10.);
/// Bindable action which uses the most recent selection head to trigger a hover
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
let head = editor.selections.newest_anchor().head();
show_hover(editor, head, true, cx);
let head = editor.selections.newest_display(cx).head();
let snapshot = editor.snapshot(cx);
show_hover(editor, head, &snapshot, true, cx);
}
/// The internal hover action dispatches between `show_hover` or `hide_hover`
/// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
pub fn hover_at(
editor: &mut Editor,
point: Option<(DisplayPoint, &EditorSnapshot)>,
cx: &mut ViewContext<Editor>,
) {
if EditorSettings::get_global(cx).hover_popover_enabled {
if let Some(anchor) = anchor {
show_hover(editor, anchor, false, cx);
if let Some((point, snapshot)) = point {
show_hover(editor, point, snapshot, false, cx);
} else {
hide_hover(editor, cx);
}
@@ -160,7 +164,8 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
/// Triggered by the `Hover` action when the cursor may be over a symbol.
fn show_hover(
editor: &mut Editor,
anchor: Anchor,
point: DisplayPoint,
snapshot: &EditorSnapshot,
ignore_timeout: bool,
cx: &mut ViewContext<Editor>,
) {
@@ -168,21 +173,27 @@ fn show_hover(
return;
}
let snapshot = editor.snapshot(cx);
let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
let (buffer, buffer_position) =
if let Some(output) = editor.buffer.read(cx).text_anchor_for_position(anchor, cx) {
output
} else {
return;
};
let (buffer, buffer_position) = if let Some(output) = editor
.buffer
.read(cx)
.text_anchor_for_position(multibuffer_offset, cx)
{
output
} else {
return;
};
let excerpt_id =
if let Some((excerpt_id, _, _)) = editor.buffer().read(cx).excerpt_containing(anchor, cx) {
excerpt_id
} else {
return;
};
let excerpt_id = if let Some((excerpt_id, _, _)) = editor
.buffer()
.read(cx)
.excerpt_containing(multibuffer_offset, cx)
{
excerpt_id
} else {
return;
};
let project = if let Some(project) = editor.project.clone() {
project
@@ -200,10 +211,9 @@ fn show_hover(
.as_text_range()
.map(|range| {
let hover_range = range.to_offset(&snapshot.buffer_snapshot);
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
// LSP returns a hover result for the end index of ranges that should be hovered, so we need to
// use an inclusive range here to check if we should dismiss the popover
(hover_range.start..=hover_range.end).contains(&offset)
(hover_range.start..=hover_range.end).contains(&multibuffer_offset)
})
.unwrap_or(false)
})
@@ -215,6 +225,11 @@ fn show_hover(
}
}
// Get input anchor
let anchor = snapshot
.buffer_snapshot
.anchor_at(multibuffer_offset, Bias::Left);
// Don't request again if the location is the same as the previous request
if let Some(triggered_from) = &editor.hover_state.triggered_from {
if triggered_from
@@ -224,6 +239,7 @@ fn show_hover(
return;
}
}
let snapshot = snapshot.clone();
let task = cx.spawn(|this, mut cx| {
async move {
@@ -257,7 +273,7 @@ fn show_hover(
// If there's a diagnostic, assign it on the hover state and notify
let local_diagnostic = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, usize>(anchor..anchor, false)
.diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
// Find the entry with the most specific range
.min_by_key(|entry| entry.range.end - entry.range.start)
.map(|entry| DiagnosticEntry {
@@ -625,7 +641,6 @@ mod tests {
use lsp::LanguageServerId;
use project::{HoverBlock, HoverBlockKind};
use smol::stream::StreamExt;
use text::Bias;
use unindent::Unindent;
use util::test::marked_text_ranges;
@@ -652,10 +667,7 @@ mod tests {
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let anchor = snapshot
.buffer_snapshot
.anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
hover_at(editor, Some(anchor), cx)
hover_at(editor, Some((hover_point, &snapshot)), cx)
});
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
@@ -704,10 +716,7 @@ mod tests {
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
let anchor = snapshot
.buffer_snapshot
.anchor_before(hover_point.to_offset(&snapshot, Bias::Left));
hover_at(editor, Some(anchor), cx)
hover_at(editor, Some((hover_point, &snapshot)), cx)
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));

View File

@@ -30,7 +30,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
) -> Option<&'a str>;
) -> Option<&str>;
}
pub trait InlineCompletionProviderHandle {

View File

@@ -21,6 +21,7 @@ use std::{
Arc,
},
};
use text::Rope;
use ui::Context;
use util::{
assert_set_eq,
@@ -271,7 +272,8 @@ impl EditorTestContext {
}
pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base.map(ToOwned::to_owned), cx));
let diff_base = diff_base.map(Rope::from);
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
}
/// Change the editor's text and selections using a string containing

View File

@@ -20,7 +20,7 @@ impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
let app_version = AppVersion::global(cx).to_string();
let release_channel = ReleaseChannel::global(cx);
let os_name = cx.app_metadata().os_name;
let os_name = cx.app_metadata().os.name;
let system = System::new_with_specifics(
RefreshKind::new().with_memory(MemoryRefreshKind::everything()),
);
@@ -28,7 +28,8 @@ impl SystemSpecs {
let architecture = env::consts::ARCH;
let os_version = cx
.app_metadata()
.os_version
.os
.version
.map(|os_version| os_version.to_string());
let commit_sha = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => {

View File

@@ -30,11 +30,8 @@ text.workspace = true
time.workspace = true
url.workspace = true
util.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[dev-dependencies]
unindent.workspace = true
serde_json.workspace = true

View File

@@ -37,11 +37,6 @@ impl Oid {
pub(crate) fn is_zero(&self) -> bool {
self.0.is_zero()
}
/// Returns this [`Oid`] as a short SHA.
pub fn display_short(&self) -> String {
self.to_string().chars().take(7).collect()
}
}
impl FromStr for Oid {

View File

@@ -1,7 +1,7 @@
use crate::blame::Blame;
use crate::GitHostingProviderRegistry;
use anyhow::{Context, Result};
use collections::{HashMap, HashSet};
use collections::HashMap;
use git2::{BranchType, StatusShow};
use parking_lot::Mutex;
use rope::Rope;
@@ -19,7 +19,6 @@ pub use git2::Repository as LibGitRepository;
#[derive(Clone, Debug, Hash, PartialEq)]
pub struct Branch {
pub is_head: bool,
pub name: Box<str>,
/// Timestamp of most recent commit, normalized to Unix Epoch format.
pub unix_timestamp: Option<i64>,
@@ -62,8 +61,6 @@ pub trait GitRepository: Send {
fn create_branch(&self, _: &str) -> Result<()>;
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
fn recently_changed_paths(&self, max: usize) -> Result<Vec<(SystemTime, PathBuf)>>;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -205,7 +202,6 @@ impl GitRepository for RealGitRepository {
let valid_branches = local_branches
.filter_map(|branch| {
branch.ok().and_then(|(branch, _)| {
let is_head = branch.is_head();
let name = branch.name().ok().flatten().map(Box::from)?;
let timestamp = branch.get().peel_to_commit().ok()?.time();
let unix_timestamp = timestamp.seconds();
@@ -215,7 +211,6 @@ impl GitRepository for RealGitRepository {
let unix_timestamp =
time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
Some(Branch {
is_head,
name,
unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
})
@@ -261,44 +256,6 @@ impl GitRepository for RealGitRepository {
self.hosting_provider_registry.clone(),
)
}
fn recently_changed_paths(&self, max: usize) -> Result<Vec<(SystemTime, PathBuf)>> {
let mut revwalk = self.repository.revwalk()?;
revwalk.push_head()?;
let mut included = HashSet::<PathBuf>::default();
let mut changed_paths = Vec::new();
for oid in revwalk {
let oid = oid?;
let commit = self.repository.find_commit(oid)?;
let tree = commit.tree()?;
let diff = self.repository.diff_tree_to_tree(None, Some(&tree), None)?;
diff.foreach(
&mut |delta, _| {
if let Some(path) = delta.new_file().path() {
if !included.contains(path) {
included.insert(path.into());
let time = commit.time().seconds();
let system_time =
SystemTime::UNIX_EPOCH + std::time::Duration::new(time as u64, 0);
changed_paths.push((system_time, path.to_path_buf()));
}
}
true
},
None,
None,
None,
)?;
if included.len() >= max {
break;
}
}
changed_paths.truncate(max);
Ok(changed_paths)
}
}
fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
@@ -417,10 +374,6 @@ impl GitRepository for FakeGitRepository {
.with_context(|| format!("failed to get blame for {:?}", path))
.cloned()
}
fn recently_changed_paths(&self, max: usize) -> Result<Vec<(SystemTime, PathBuf)>> {
todo!()
}
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {

View File

@@ -1,20 +0,0 @@
[package]
name = "git_ui"
version = "0.1.0"
edition = "2021"
[lib]
path = "./src/git_ui.rs"
[dependencies]
gpui.workspace = true
git.workspace = true
workspace.workspace = true
serde.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
[lints]
workspace = true

View File

@@ -1,58 +0,0 @@
use std::{
path::{Path, PathBuf},
time::{self, Instant, SystemTime},
};
use gpui::{AppContext, ViewContext};
use serde::Deserialize;
use workspace::Workspace;
#[derive(Deserialize)]
struct OpenRecentlyChanged {
count: usize,
}
gpui::impl_actions!(git, [OpenRecentlyChanged]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
workspace.register_action(open_recently_changed);
},
)
.detach();
}
fn open_recently_changed(
workspace: &mut Workspace,
action: &OpenRecentlyChanged,
cx: &mut ViewContext<Workspace>,
) {
let worktrees = workspace.project().read(cx).worktrees().collect::<Vec<_>>();
let mut repos = Vec::new();
for worktree in worktrees {
if let Some(local_worktree) = worktree.read(cx).as_local() {
if let Some(local_repo) = local_worktree
.repository_for_path(Path::new(""))
.and_then(|repo| local_worktree.get_local_repo(&repo))
{
repos.push(local_repo.repo().clone());
}
}
}
if !repos.is_empty() {
let recent_paths = cx.background_executor().spawn(async move {
let mut paths = Vec::new();
for repo in repos {
paths.extend(repo.lock().recently_changed_paths(100))?;
}
anyhow::Ok(paths)
});
cx.spawn(|this, cx| async move {
// let recent_paths =
})
}
}

View File

@@ -63,11 +63,7 @@ fn main() {
.with_assets(Assets {})
.run(|cx: &mut AppContext| {
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(300.), px(300.)),
cx,
))),
bounds: Some(Bounds::centered(None, size(px(300.), px(300.)), cx)),
..Default::default()
};
cx.open_window(options, |cx| {

View File

@@ -26,7 +26,7 @@ fn main() {
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
bounds: Some(bounds),
..Default::default()
},
|cx| {

View File

@@ -79,10 +79,10 @@ fn main() {
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(Bounds {
bounds: Some(Bounds {
size: size(px(1100.), px(600.)).into(),
origin: Point::new(DevicePixels::from(200), DevicePixels::from(200)),
})),
}),
..Default::default()
};

View File

@@ -43,7 +43,7 @@ fn main() {
WindowOptions {
// Set the bounds of the window in screen coordinates
window_bounds: Some(WindowBounds::Windowed(bounds)),
bounds: Some(bounds),
// Specify the display_id to ensure the window is created on the correct screen
display_id: Some(screen.id()),
@@ -53,6 +53,7 @@ fn main() {
show: true,
kind: WindowKind::PopUp,
is_movable: false,
fullscreen: false,
app_id: None,
}
};

View File

@@ -12,6 +12,7 @@ use std::{
use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut};
use futures::{channel::oneshot, future::LocalBoxFuture, Future};
use semantic_version::SemanticVersion;
use slotmap::SlotMap;
use smol::future::FutureExt;
use time::UtcOffset;
@@ -31,8 +32,8 @@ use crate::{
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
Keystroke, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point,
PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
Keystroke, LayoutId, Menu, OsMetadata, PathPromptOptions, Pixels, Platform, PlatformDisplay,
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext,
Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
};
@@ -131,6 +132,12 @@ impl App {
self
}
/// Assign the version of the application.
pub fn with_version(mut self, version: Option<SemanticVersion>) -> Self {
self.0.borrow_mut().app_metadata.version = version;
self
}
/// Start the application. The provided callback will be called once the
/// app is fully launched.
pub fn run<F>(self, on_finish_launching: F)
@@ -263,9 +270,11 @@ impl AppContext {
let entities = EntityMap::new();
let app_metadata = AppMetadata {
os_name: platform.os_name(),
os_version: platform.os_version().ok(),
app_version: platform.app_version().ok(),
version: None,
os: OsMetadata {
name: platform.os_name(),
version: platform.os_version().ok(),
},
};
let app = Rc::new_cyclic(|this| AppCell {
@@ -352,6 +361,11 @@ impl AppContext {
self.app_metadata.clone()
}
/// Set the version of the application.
pub fn set_app_version(&mut self, version: SemanticVersion) {
self.app_metadata.version = Some(version);
}
/// Schedules all windows in the application to be redrawn. This can be called
/// multiple times in an update cycle and still result in a single redraw.
pub fn refresh(&mut self) {
@@ -642,8 +656,8 @@ impl AppContext {
}
/// Restart the application.
pub fn restart(&self, binary_path: Option<PathBuf>) {
self.platform.restart(binary_path)
pub fn restart(&self) {
self.platform.restart()
}
/// Returns the local timezone at the platform level.

View File

@@ -4,8 +4,8 @@ use crate::{
Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model,
ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher,
TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowBounds,
WindowContext, WindowHandle, WindowOptions,
TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowContext,
WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{channel::oneshot, Stream, StreamExt};
@@ -188,7 +188,7 @@ impl TestAppContext {
let bounds = Bounds::maximized(None, &mut cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
bounds: Some(bounds),
..Default::default()
},
|cx| cx.new_view(build_window),
@@ -201,7 +201,7 @@ impl TestAppContext {
let bounds = Bounds::maximized(None, &mut cx);
let window = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
bounds: Some(bounds),
..Default::default()
},
|cx| cx.new_view(|_| Empty),
@@ -224,7 +224,7 @@ impl TestAppContext {
let bounds = Bounds::maximized(None, &mut cx);
let window = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
bounds: Some(bounds),
..Default::default()
},
|cx| cx.new_view(build_root_view),

View File

@@ -98,7 +98,7 @@ pub(crate) trait Platform: 'static {
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
fn quit(&self);
fn restart(&self, binary_path: Option<PathBuf>);
fn restart(&self);
fn activate(&self, ignoring_other_apps: bool);
fn hide(&self);
fn hide_other_apps(&self);
@@ -138,7 +138,6 @@ pub(crate) trait Platform: 'static {
fn os_name(&self) -> &'static str;
fn os_version(&self) -> Result<SemanticVersion>;
fn app_version(&self) -> Result<SemanticVersion>;
fn app_path(&self) -> Result<PathBuf>;
fn local_timezone(&self) -> UtcOffset;
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
@@ -184,7 +183,7 @@ unsafe impl Send for DisplayId {}
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn bounds(&self) -> Bounds<DevicePixels>;
fn is_maximized(&self) -> bool;
fn window_bounds(&self) -> WindowBounds;
fn is_minimized(&self) -> bool;
fn content_size(&self) -> Size<Pixels>;
fn scale_factor(&self) -> f32;
fn appearance(&self) -> WindowAppearance;
@@ -271,14 +270,20 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
/// Basic metadata about the current application and operating system.
#[derive(Clone, Debug)]
pub struct AppMetadata {
/// The current version of the application
pub version: Option<SemanticVersion>,
/// Basic metadata about the operating system
pub os: OsMetadata,
}
/// Basic metadata about the operating system.
#[derive(Clone, Debug)]
pub struct OsMetadata {
/// The name of the current operating system
pub os_name: &'static str,
pub name: &'static str,
/// The operating system's version
pub os_version: Option<SemanticVersion>,
/// The current version of the application
pub app_version: Option<SemanticVersion>,
pub version: Option<SemanticVersion>,
}
#[derive(PartialEq, Eq, Hash, Clone)]
@@ -515,10 +520,9 @@ pub trait InputHandler: 'static {
/// The variables that can be configured when creating a new window
#[derive(Debug)]
pub struct WindowOptions {
/// Specifies the state and bounds of the window in screen coordinates.
/// - `None`: Inherit the bounds.
/// - `Some(WindowBounds)`: Open a window with corresponding state and its restore size.
pub window_bounds: Option<WindowBounds>,
/// The bounds of the window in screen coordinates.
/// None -> inherit, Some(bounds) -> set bounds
pub bounds: Option<Bounds<DevicePixels>>,
/// The titlebar configuration of the window
pub titlebar: Option<TitlebarOptions>,
@@ -529,6 +533,9 @@ pub struct WindowOptions {
/// Whether the window should be shown when created
pub show: bool,
/// Whether the window should be fullscreen when created
pub fullscreen: bool,
/// The kind of window to create
pub kind: WindowKind,
@@ -569,40 +576,10 @@ pub(crate) struct WindowParams {
pub window_background: WindowBackgroundAppearance,
}
/// Represents the status of how a window should be opened.
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum WindowBounds {
/// Indicates that the window should open in a windowed state with the given bounds.
Windowed(Bounds<DevicePixels>),
/// Indicates that the window should open in a maximized state.
/// The bounds provided here represent the restore size of the window.
Maximized(Bounds<DevicePixels>),
/// Indicates that the window should open in fullscreen mode.
/// The bounds provided here represent the restore size of the window.
Fullscreen(Bounds<DevicePixels>),
}
impl Default for WindowBounds {
fn default() -> Self {
WindowBounds::Windowed(Bounds::default())
}
}
impl WindowBounds {
/// Retrieve the inner bounds
pub fn get_bounds(&self) -> Bounds<DevicePixels> {
match self {
WindowBounds::Windowed(bounds) => *bounds,
WindowBounds::Maximized(bounds) => *bounds,
WindowBounds::Fullscreen(bounds) => *bounds,
}
}
}
impl Default for WindowOptions {
fn default() -> Self {
Self {
window_bounds: None,
bounds: None,
titlebar: Some(TitlebarOptions {
title: Default::default(),
appears_transparent: Default::default(),
@@ -613,6 +590,7 @@ impl Default for WindowOptions {
kind: WindowKind::Normal,
is_movable: true,
display_id: None,
fullscreen: false,
window_background: WindowBackgroundAppearance::default(),
app_id: None,
}

View File

@@ -162,7 +162,7 @@ impl BladeAtlasState {
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
}
AtlasTextureKind::Polychrome => {
format = gpu::TextureFormat::Bgra8UnormSrgb;
format = gpu::TextureFormat::Bgra8Unorm;
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
}
AtlasTextureKind::Path => {

View File

@@ -360,7 +360,9 @@ impl BladeRenderer {
size: config.size,
usage: gpu::TextureUsage::TARGET,
display_sync: gpu::DisplaySync::Recent,
color_space: gpu::ColorSpace::Linear,
//Note: this matches the original logic of the Metal backend,
// but ultimaterly we need to switch to `Linear`.
color_space: gpu::ColorSpace::Srgb,
allow_exclusive_full_screen: false,
transparent: config.transparent,
};

View File

@@ -88,14 +88,6 @@ fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds:
return distance_from_clip_rect_impl(position, clip_bounds);
}
// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(0.04045);
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
let lower = srgb / vec3<f32>(12.92);
return select(higher, lower, cutoff);
}
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
let s = hsla.s;
@@ -105,7 +97,8 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
let x = c * (1.0 - abs(h % 2.0 - 1.0));
let m = l - c / 2.0;
var color = vec3<f32>(m);
var color = vec4<f32>(m, m, m, a);
if (h >= 0.0 && h < 1.0) {
color.r += c;
@@ -127,12 +120,7 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
color.b += x;
}
// Input colors are assumed to be in sRGB space,
// but blending and rendering needs to happen in linear space.
// The output will be converted to sRGB by either the target
// texture format or the swapchain color space.
let linear = srgb_to_linear(color);
return vec4<f32>(linear, a);
return color;
}
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
@@ -193,8 +181,7 @@ fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
// target alpha compositing mode.
fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
let alpha = color.a * alpha_factor;
let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
return vec4<f32>(color.rgb * multiplier, alpha);
return select(vec4<f32>(color.rgb, alpha), vec4<f32>(color.rgb, 1.0) * alpha, globals.premultiplied_alpha != 0u);
}
// --- quads --- //

View File

@@ -136,21 +136,17 @@ impl<P: LinuxClient + 'static> Platform for P {
self.with_common(|common| common.signal.stop());
}
fn restart(&self, binary_path: Option<PathBuf>) {
fn restart(&self) {
use std::os::unix::process::CommandExt as _;
// get the process id of the current process
let app_pid = std::process::id().to_string();
// get the path to the executable
let app_path = if let Some(path) = binary_path {
path
} else {
match self.app_path() {
Ok(path) => path,
Err(err) => {
log::error!("Failed to get app path: {:?}", err);
return;
}
let app_path = match self.app_path() {
Ok(path) => path,
Err(err) => {
log::error!("Failed to get app path: {:?}", err);
return;
}
};
@@ -351,15 +347,6 @@ impl<P: LinuxClient + 'static> Platform for P {
Ok(SemanticVersion::new(1, 0, 0))
}
fn app_version(&self) -> Result<SemanticVersion> {
const VERSION: Option<&str> = option_env!("RELEASE_VERSION");
if let Some(version) = VERSION {
version.parse()
} else {
Ok(SemanticVersion::new(1, 0, 0))
}
}
fn app_path(&self) -> Result<PathBuf> {
// get the path of the executable of the current process
let exe_path = std::env::current_exe()?;

View File

@@ -28,7 +28,7 @@ use crate::scene::Scene;
use crate::{
px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput,
Point, PromptLevel, Size, WaylandClientState, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowParams,
WindowBackgroundAppearance, WindowParams,
};
#[derive(Default)]
@@ -79,7 +79,6 @@ pub struct WaylandWindowState {
input_handler: Option<PlatformInputHandler>,
decoration_state: WaylandDecorationState,
fullscreen: bool,
restore_bounds: Bounds<DevicePixels>,
maximized: bool,
client: WaylandClientStatePtr,
callbacks: Callbacks,
@@ -152,7 +151,6 @@ impl WaylandWindowState {
input_handler: None,
decoration_state: WaylandDecorationState::Client,
fullscreen: false,
restore_bounds: Bounds::default(),
maximized: false,
callbacks: Callbacks::default(),
client,
@@ -334,15 +332,10 @@ impl WaylandWindowStatePtr {
let height = NonZeroU32::new(height as u32);
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
let mut state = self.state.borrow_mut();
state.maximized = maximized;
state.fullscreen = fullscreen;
if fullscreen || maximized {
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
}
drop(state);
self.resize(width, height);
self.set_fullscreen(fullscreen);
let mut state = self.state.borrow_mut();
state.maximized = maximized;
false
}
@@ -552,17 +545,9 @@ impl PlatformWindow for WaylandWindow {
self.borrow().maximized
}
// todo(linux)
// check if it is right
fn window_bounds(&self) -> WindowBounds {
let state = self.borrow();
if state.fullscreen {
WindowBounds::Fullscreen(state.restore_bounds)
} else if state.maximized {
WindowBounds::Maximized(state.restore_bounds)
} else {
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32)))
}
fn is_minimized(&self) -> bool {
// This cannot be determined by the client
false
}
fn content_size(&self) -> Size<Pixels> {
@@ -694,8 +679,7 @@ impl PlatformWindow for WaylandWindow {
}
fn toggle_fullscreen(&self) {
let mut state = self.borrow_mut();
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
let state = self.borrow();
if !state.fullscreen {
state.toplevel.set_fullscreen(None);
} else {

View File

@@ -5,8 +5,8 @@ use crate::{
platform::blade::{BladeRenderer, BladeSurfaceConfig},
size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, Platform, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions,
WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams,
X11Client, X11ClientState, X11ClientStatePtr,
};
use blade_graphics as gpu;
use parking_lot::Mutex;
@@ -526,9 +526,8 @@ impl PlatformWindow for X11Window {
}
// todo(linux)
fn window_bounds(&self) -> WindowBounds {
let state = self.0.state.borrow();
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p)))
fn is_minimized(&self) -> bool {
false
}
fn content_size(&self) -> Size<Pixels> {

View File

@@ -2,7 +2,7 @@ use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size,
};
use anyhow::{anyhow, Result};
use anyhow::Result;
use collections::FxHashMap;
use derive_more::{Deref, DerefMut};
use etagere::BucketedAtlasAllocator;
@@ -31,7 +31,7 @@ impl MetalAtlas {
&self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
) -> AtlasTile {
self.0.lock().allocate(size, texture_kind)
}
@@ -67,9 +67,7 @@ impl PlatformAtlas for MetalAtlas {
Ok(tile.clone())
} else {
let (size, bytes) = build()?;
let tile = lock
.allocate(size, key.texture_kind())
.ok_or_else(|| anyhow!("failed to allocate"))?;
let tile = lock.allocate(size, key.texture_kind());
let texture = lock.texture(tile.texture_id);
texture.upload(tile.bounds, &bytes);
lock.tiles_by_key.insert(key.clone(), tile.clone());
@@ -79,11 +77,7 @@ impl PlatformAtlas for MetalAtlas {
}
impl MetalAtlasState {
fn allocate(
&mut self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
@@ -94,9 +88,9 @@ impl MetalAtlasState {
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
.or_else(|| {
.unwrap_or_else(|| {
let texture = self.push_texture(size, texture_kind);
texture.allocate(size)
texture.allocate(size).unwrap()
})
}

View File

@@ -417,7 +417,7 @@ impl MetalRenderer {
let tile = self
.sprite_atlas
.allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path)?;
.allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path);
vertices_by_texture_id
.entry(tile.texture_id)
.or_insert(Vec::new())

View File

@@ -5,7 +5,7 @@ use crate::{
Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, Task,
WindowAppearance, WindowParams,
};
use anyhow::{anyhow, bail};
use anyhow::anyhow;
use block::ConcreteBlock;
use cocoa::{
appkit::{
@@ -396,7 +396,7 @@ impl Platform for MacPlatform {
}
}
fn restart(&self, _binary_path: Option<PathBuf>) {
fn restart(&self) {
use std::os::unix::process::CommandExt as _;
let app_pid = std::process::id().to_string();
@@ -693,24 +693,6 @@ impl Platform for MacPlatform {
}
}
fn app_version(&self) -> Result<SemanticVersion> {
unsafe {
let bundle: id = NSBundle::mainBundle();
if bundle.is_null() {
Err(anyhow!("app is not running inside a bundle"))
} else {
let version: id = msg_send![bundle, objectForInfoDictionaryKey: ns_string("CFBundleShortVersionString")];
if version.is_null() {
bail!("bundle does not have version");
}
let len = msg_send![version, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
let bytes = version.UTF8String() as *const u8;
let version = str::from_utf8(slice::from_raw_parts(bytes, len)).unwrap();
version.parse()
}
}
}
fn app_path(&self) -> Result<PathBuf> {
unsafe {
let bundle: id = NSBundle::mainBundle();

View File

@@ -4,8 +4,7 @@ use crate::{
DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke,
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel,
Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind,
WindowParams,
Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowKind, WindowParams,
};
use block::ConcreteBlock;
use cocoa::{
@@ -260,10 +259,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowDidChangeOcclusionState:),
window_did_change_occlusion_state as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowWillEnterFullScreen:),
window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
@@ -307,6 +302,14 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(concludeDragOperation:),
conclude_drag_operation as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidMiniaturize:),
window_did_miniaturize as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidDeminiaturize:),
window_did_deminiaturize as extern "C" fn(&Object, Sel, id),
);
decl.register()
}
@@ -345,7 +348,7 @@ struct MacWindowState {
external_files_dragged: bool,
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<DevicePixels>,
minimized: bool,
}
impl MacWindowState {
@@ -432,6 +435,10 @@ impl MacWindowState {
}
}
fn is_minimized(&self) -> bool {
self.minimized
}
fn is_fullscreen(&self) -> bool {
unsafe {
let style_mask = self.native_window.styleMask();
@@ -487,14 +494,6 @@ impl MacWindowState {
px((frame.size.height - content_layout_rect.size.height) as f32)
}
}
fn window_bounds(&self) -> WindowBounds {
if self.is_fullscreen() {
WindowBounds::Fullscreen(self.fullscreen_restore_bounds)
} else {
WindowBounds::Windowed(self.bounds())
}
}
}
unsafe impl Send for MacWindowState {}
@@ -640,7 +639,7 @@ impl MacWindow {
previous_keydown_inserted_text: None,
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
minimized: false,
})));
(*native_window).set_ivar(
@@ -776,14 +775,14 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().bounds()
}
fn window_bounds(&self) -> WindowBounds {
self.0.as_ref().lock().window_bounds()
}
fn is_maximized(&self) -> bool {
self.0.as_ref().lock().is_maximized()
}
fn is_minimized(&self) -> bool {
self.0.as_ref().lock().is_minimized()
}
fn content_size(&self) -> Size<Pixels> {
self.0.as_ref().lock().content_size()
}
@@ -1467,12 +1466,6 @@ extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
window_state.as_ref().lock().move_traffic_light();
}
extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
lock.fullscreen_restore_bounds = lock.bounds();
}
extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
@@ -1870,6 +1863,18 @@ extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) {
);
}
extern "C" fn window_did_miniaturize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
window_state.lock().minimized = true;
}
extern "C" fn window_did_deminiaturize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
window_state.lock().minimized = false;
}
async fn synthetic_drag(
window_state: Weak<Mutex<MacWindowState>>,
drag_id: usize,

View File

@@ -140,7 +140,7 @@ impl Platform for TestPlatform {
fn quit(&self) {}
fn restart(&self, _: Option<PathBuf>) {
fn restart(&self) {
unimplemented!()
}
@@ -248,10 +248,6 @@ impl Platform for TestPlatform {
Err(anyhow!("os_version called on TestPlatform"))
}
fn app_version(&self) -> Result<crate::SemanticVersion> {
Err(anyhow!("app_version called on TestPlatform"))
}
fn app_path(&self) -> Result<std::path::PathBuf> {
unimplemented!()
}

View File

@@ -2,7 +2,7 @@ use crate::{
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DevicePixels,
DispatchEventResult, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformInputHandler, PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowParams,
WindowBackgroundAppearance, WindowParams,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -112,11 +112,11 @@ impl PlatformWindow for TestWindow {
self.0.lock().bounds
}
fn window_bounds(&self) -> WindowBounds {
WindowBounds::Windowed(self.bounds())
fn is_maximized(&self) -> bool {
false
}
fn is_maximized(&self) -> bool {
fn is_minimized(&self) -> bool {
false
}

View File

@@ -1,18 +1,14 @@
mod direct_write;
mod dispatcher;
mod display;
mod events;
mod platform;
mod system_settings;
mod util;
mod window;
pub(crate) use direct_write::*;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use events::*;
pub(crate) use platform::*;
pub(crate) use system_settings::*;
pub(crate) use util::*;
pub(crate) use window::*;

View File

@@ -58,14 +58,7 @@ struct DirectWriteState {
custom_font_collection: IDWriteFontCollection1,
fonts: Vec<FontInfo>,
font_selections: HashMap<Font, FontId>,
font_id_by_identifier: HashMap<FontIdentifier, FontId>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct FontIdentifier {
postscript_name: String,
weight: i32,
style: i32,
font_id_by_postscript_name: HashMap<String, FontId>,
}
impl DirectWriteComponent {
@@ -125,7 +118,7 @@ impl DirectWriteTextSystem {
custom_font_collection,
fonts: Vec::new(),
font_selections: HashMap::default(),
font_id_by_identifier: HashMap::default(),
font_id_by_postscript_name: HashMap::default(),
})))
}
}
@@ -276,7 +269,8 @@ impl DirectWriteState {
let Some(font_face) = font_face_ref.CreateFontFace().log_err() else {
continue;
};
let Some(identifier) = get_font_identifier(&font_face, &self.components.locale) else {
let Some(postscript_name) = get_postscript_name(&font_face, &self.components.locale)
else {
continue;
};
let is_emoji = font_face.IsColorFont().as_bool();
@@ -293,7 +287,8 @@ impl DirectWriteState {
};
let font_id = FontId(self.fonts.len());
self.fonts.push(font_info);
self.font_id_by_identifier.insert(identifier, font_id);
self.font_id_by_postscript_name
.insert(postscript_name, font_id);
return Some(font_id);
}
None
@@ -950,8 +945,8 @@ impl IDWriteTextRenderer_Impl for TextRenderer {
// This `cast()` action here should never fail since we are running on Win10+, and
// `IDWriteFontFace3` requires Win10
let font_face = &font_face.cast::<IDWriteFontFace3>().unwrap();
let Some((font_identifier, font_struct, is_emoji)) =
get_font_identifier_and_font_struct(font_face, &self.locale)
let Some((postscript_name, font_struct, is_emoji)) =
get_postscript_name_and_font(font_face, &self.locale)
else {
log::error!("none postscript name found");
return Ok(());
@@ -959,8 +954,8 @@ impl IDWriteTextRenderer_Impl for TextRenderer {
let font_id = if let Some(id) = context
.text_system
.font_id_by_identifier
.get(&font_identifier)
.font_id_by_postscript_name
.get(&postscript_name)
{
*id
} else {
@@ -1126,60 +1121,39 @@ fn get_font_names_from_collection(
}
}
fn get_font_identifier_and_font_struct(
unsafe fn get_postscript_name_and_font(
font_face: &IDWriteFontFace3,
locale: &str,
) -> Option<(FontIdentifier, Font, bool)> {
) -> Option<(String, Font, bool)> {
let Some(postscript_name) = get_postscript_name(font_face, locale) else {
return None;
};
let Some(localized_family_name) = (unsafe { font_face.GetFamilyNames().log_err() }) else {
let Some(localized_family_name) = font_face.GetFamilyNames().log_err() else {
return None;
};
let Some(family_name) = get_name(localized_family_name, locale) else {
return None;
};
let weight = unsafe { font_face.GetWeight() };
let style = unsafe { font_face.GetStyle() };
let identifier = FontIdentifier {
postscript_name,
weight: weight.0,
style: style.0,
};
let font_struct = Font {
family: family_name.into(),
features: FontFeatures::default(),
weight: weight.into(),
style: style.into(),
weight: font_face.GetWeight().into(),
style: font_face.GetStyle().into(),
};
let is_emoji = unsafe { font_face.IsColorFont().as_bool() };
Some((identifier, font_struct, is_emoji))
let is_emoji = font_face.IsColorFont().as_bool();
Some((postscript_name, font_struct, is_emoji))
}
#[inline]
fn get_font_identifier(font_face: &IDWriteFontFace3, locale: &str) -> Option<FontIdentifier> {
let weight = unsafe { font_face.GetWeight().0 };
let style = unsafe { font_face.GetStyle().0 };
get_postscript_name(font_face, locale).map(|postscript_name| FontIdentifier {
postscript_name,
weight,
style,
})
}
#[inline]
fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option<String> {
let mut info = None;
unsafe fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option<String> {
let mut info = std::mem::zeroed();
let mut exists = BOOL(0);
unsafe {
font_face
.GetInformationalStrings(
DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME,
&mut info,
&mut exists,
)
.log_err();
}
font_face
.GetInformationalStrings(
DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME,
&mut info,
&mut exists,
)
.log_err();
if !exists.as_bool() || info.is_none() {
return None;
}
@@ -1188,7 +1162,7 @@ fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option<Str
}
// https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_feature_tag
fn apply_font_features(
unsafe fn apply_font_features(
direct_write_features: &IDWriteTypography,
features: &FontFeatures,
) -> Result<()> {
@@ -1217,15 +1191,11 @@ fn apply_font_features(
continue;
}
unsafe {
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
}
}
unsafe {
direct_write_features.AddFontFeature(feature_liga)?;
direct_write_features.AddFontFeature(feature_clig)?;
direct_write_features.AddFontFeature(feature_calt)?;
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
}
direct_write_features.AddFontFeature(feature_liga)?;
direct_write_features.AddFontFeature(feature_clig)?;
direct_write_features.AddFontFeature(feature_calt)?;
Ok(())
}
@@ -1261,39 +1231,32 @@ fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG {
DWRITE_FONT_FEATURE_TAG(make_open_type_tag(tag_name))
}
#[inline]
fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Option<String> {
unsafe fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Option<String> {
let mut locale_name_index = 0u32;
let mut exists = BOOL(0);
unsafe {
string
.FindLocaleName(
&HSTRING::from(locale),
&mut locale_name_index,
&mut exists as _,
)
.log_err();
if !exists.as_bool() {
string
.FindLocaleName(
&HSTRING::from(locale),
&mut locale_name_index,
DEFAULT_LOCALE_NAME,
&mut locale_name_index as _,
&mut exists as _,
)
.log_err();
}
if !exists.as_bool() {
unsafe {
string
.FindLocaleName(
DEFAULT_LOCALE_NAME,
&mut locale_name_index as _,
&mut exists as _,
)
.log_err();
}
if !exists.as_bool() {
return None;
}
}
let name_length = unsafe { string.GetStringLength(locale_name_index).unwrap() } as usize;
let name_length = string.GetStringLength(locale_name_index).unwrap() as usize;
let mut name_vec = vec![0u16; name_length + 1];
unsafe {
string.GetString(locale_name_index, &mut name_vec).unwrap();
}
string.GetString(locale_name_index, &mut name_vec).unwrap();
Some(String::from_utf16_lossy(&name_vec[..name_length]))
}
@@ -1324,8 +1287,7 @@ fn get_system_ui_font_name() -> SharedString {
// Segoe UI is the Windows font intended for user interface text strings.
"Segoe UI".into()
} else {
let font_name = String::from_utf16_lossy(&info.lfFaceName);
font_name.trim_matches(char::from(0)).to_owned().into()
String::from_utf16_lossy(&info.lfFaceName).into()
};
log::info!("Use {} as UI font.", font_family);
font_family

View File

@@ -9,7 +9,7 @@ use windows::{
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Point, Size};
#[derive(Debug, Clone, Copy)]
#[derive(Debug)]
pub(crate) struct WindowsDisplay {
pub handle: HMONITOR,
pub display_id: DisplayId,

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,8 @@
use std::{
cell::{Cell, RefCell},
ffi::{c_void, OsString},
ffi::{c_uint, c_void, OsString},
iter::once,
mem::transmute,
os::windows::ffi::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
@@ -17,7 +18,7 @@ use async_task::Runnable;
use copypasta::{ClipboardContext, ClipboardProvider};
use futures::channel::oneshot::{self, Receiver};
use itertools::Itertools;
use parking_lot::RwLock;
use parking_lot::{Mutex, RwLock};
use semantic_version::SemanticVersion;
use smallvec::SmallVec;
use time::UtcOffset;
@@ -38,26 +39,56 @@ use windows::{
use crate::*;
pub(crate) struct WindowsPlatform {
state: RefCell<WindowsPlatformState>,
raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
// The below members will never change throughout the entire lifecycle of the app.
icon: HICON,
main_receiver: flume::Receiver<Runnable>,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<dyn PlatformTextSystem>,
dispatch_event: OwnedHandle,
inner: Rc<WindowsPlatformInner>,
}
pub(crate) struct WindowsPlatformState {
callbacks: PlatformCallbacks,
pub(crate) settings: WindowsPlatformSystemSettings,
/// Windows settings pulled from SystemParametersInfo
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
#[derive(Default, Debug)]
pub(crate) struct WindowsPlatformSystemSettings {
/// SEE: SPI_GETWHEELSCROLLCHARS
pub(crate) wheel_scroll_chars: u32,
/// SEE: SPI_GETWHEELSCROLLLINES
pub(crate) wheel_scroll_lines: u32,
}
pub(crate) struct WindowsPlatformInner {
background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
main_receiver: flume::Receiver<Runnable>,
text_system: Arc<dyn PlatformTextSystem>,
callbacks: Mutex<Callbacks>,
pub raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
pub(crate) dispatch_event: OwnedHandle,
pub(crate) settings: RefCell<WindowsPlatformSystemSettings>,
pub icon: HICON,
// NOTE: standard cursor handles don't need to close.
pub(crate) current_cursor: HCURSOR,
pub(crate) current_cursor: Cell<HCURSOR>,
}
impl WindowsPlatformInner {
pub(crate) fn try_get_windows_inner_from_hwnd(
&self,
hwnd: HWND,
) -> Option<Rc<WindowsWindowInner>> {
self.raw_window_handles
.read()
.iter()
.find(|entry| *entry == &hwnd)
.and_then(|hwnd| try_get_window_inner(*hwnd))
}
#[inline]
pub fn run_foreground_tasks(&self) {
for runnable in self.main_receiver.drain() {
runnable.run();
}
}
}
#[derive(Default)]
struct PlatformCallbacks {
struct Callbacks {
open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
quit: Option<Box<dyn FnMut()>>,
reopen: Option<Box<dyn FnMut()>>,
@@ -66,16 +97,53 @@ struct PlatformCallbacks {
validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
}
impl WindowsPlatformState {
fn new() -> Self {
let callbacks = PlatformCallbacks::default();
let settings = WindowsPlatformSystemSettings::new();
let current_cursor = load_cursor(CursorStyle::Arrow);
enum WindowsMessageWaitResult {
ForegroundExecution,
WindowsMessage(MSG),
Error,
}
Self {
callbacks,
settings,
current_cursor,
impl WindowsPlatformSystemSettings {
fn new() -> Self {
let mut settings = Self::default();
settings.update_all();
settings
}
pub(crate) fn update_all(&mut self) {
self.update_wheel_scroll_lines();
self.update_wheel_scroll_chars();
}
pub(crate) fn update_wheel_scroll_lines(&mut self) {
let mut value = c_uint::default();
let result = unsafe {
SystemParametersInfoW(
SPI_GETWHEELSCROLLLINES,
0,
Some((&mut value) as *mut c_uint as *mut c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
)
};
if result.log_err() != None {
self.wheel_scroll_lines = value;
}
}
pub(crate) fn update_wheel_scroll_chars(&mut self) {
let mut value = c_uint::default();
let result = unsafe {
SystemParametersInfoW(
SPI_GETWHEELSCROLLCHARS,
0,
Some((&mut value) as *mut c_uint as *mut c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
)
};
if result.log_err() != None {
self.wheel_scroll_chars = value;
}
}
}
@@ -98,31 +166,33 @@ impl WindowsPlatform {
log::info!("Using cosmic text system.");
Arc::new(CosmicTextSystem::new()) as Arc<dyn PlatformTextSystem>
};
let icon = load_icon().unwrap_or_default();
let state = RefCell::new(WindowsPlatformState::new());
let callbacks = Mutex::new(Callbacks::default());
let raw_window_handles = RwLock::new(SmallVec::new());
Self {
state,
raw_window_handles,
icon,
main_receiver,
let settings = RefCell::new(WindowsPlatformSystemSettings::new());
let icon = load_icon().unwrap_or_default();
let current_cursor = Cell::new(load_cursor(CursorStyle::Arrow));
let inner = Rc::new(WindowsPlatformInner {
background_executor,
foreground_executor,
main_receiver,
text_system,
callbacks,
raw_window_handles,
dispatch_event,
}
settings,
icon,
current_cursor,
});
Self { inner }
}
#[inline]
fn run_foreground_tasks(&self) {
for runnable in self.main_receiver.drain() {
runnable.run();
}
self.inner.run_foreground_tasks();
}
fn redraw_all(&self) {
for handle in self.raw_window_handles.read().iter() {
for handle in self.inner.raw_window_handles.read().iter() {
unsafe {
RedrawWindow(
*handle,
@@ -133,75 +203,24 @@ impl WindowsPlatform {
}
}
}
pub fn try_get_windows_inner_from_hwnd(&self, hwnd: HWND) -> Option<Rc<WindowsWindowStatePtr>> {
self.raw_window_handles
.read()
.iter()
.find(|entry| *entry == &hwnd)
.and_then(|hwnd| try_get_window_inner(*hwnd))
}
#[inline]
fn post_message(&self, message: u32, wparam: WPARAM, lparam: LPARAM) {
self.raw_window_handles
.read()
.iter()
.for_each(|handle| unsafe {
PostMessageW(*handle, message, wparam, lparam).log_err();
});
}
fn close_one_window(&self, target_window: HWND) -> bool {
let mut lock = self.raw_window_handles.write();
let index = lock
.iter()
.position(|handle| *handle == target_window)
.unwrap();
lock.remove(index);
lock.is_empty()
}
fn update_system_settings(&self) {
let mut lock = self.state.borrow_mut();
// mouse wheel
{
let (scroll_chars, scroll_lines) = lock.settings.mouse_wheel_settings.update();
if let Some(scroll_chars) = scroll_chars {
self.post_message(
MOUSE_WHEEL_SETTINGS_CHANGED,
WPARAM(scroll_chars as usize),
LPARAM(MOUSE_WHEEL_SETTINGS_SCROLL_CHARS_CHANGED),
);
}
if let Some(scroll_lines) = scroll_lines {
self.post_message(
MOUSE_WHEEL_SETTINGS_CHANGED,
WPARAM(scroll_lines as usize),
LPARAM(MOUSE_WHEEL_SETTINGS_SCROLL_LINES_CHANGED),
);
}
}
}
}
impl Platform for WindowsPlatform {
fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone()
self.inner.background_executor.clone()
}
fn foreground_executor(&self) -> ForegroundExecutor {
self.foreground_executor.clone()
self.inner.foreground_executor.clone()
}
fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
self.text_system.clone()
self.inner.text_system.clone()
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
on_finish_launching();
let dispatch_event = self.dispatch_event.to_raw();
let dispatch_event = self.inner.dispatch_event.to_raw();
let vsync_event = create_event().unwrap();
let timer_stop_event = create_event().unwrap();
let raw_timer_stop_event = timer_stop_event.to_raw();
@@ -229,20 +248,16 @@ impl Platform for WindowsPlatform {
WAIT_EVENT(2) => {
let mut msg = MSG::default();
unsafe {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
match msg.message {
WM_QUIT => break 'a,
CLOSE_ONE_WINDOW => {
if self.close_one_window(HWND(msg.lParam.0)) {
break 'a;
}
}
WM_SETTINGCHANGE => self.update_system_settings(),
_ => {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
while PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE).as_bool() {
if msg.message == WM_QUIT {
break 'a;
}
if msg.message == WM_SETTINGCHANGE {
self.inner.settings.borrow_mut().update_all();
continue;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
@@ -257,8 +272,9 @@ impl Platform for WindowsPlatform {
}
end_vsync_timer(raw_timer_stop_event);
if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
callback();
let mut callbacks = self.inner.callbacks.lock();
if let Some(callback) = callbacks.quit.as_mut() {
callback()
}
}
@@ -268,7 +284,7 @@ impl Platform for WindowsPlatform {
.detach();
}
fn restart(&self, _: Option<PathBuf>) {
fn restart(&self) {
let pid = std::process::id();
let Some(app_path) = self.app_path().log_err() else {
return;
@@ -324,12 +340,17 @@ impl Platform for WindowsPlatform {
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
WindowsDisplay::primary_monitor().map(|display| Rc::new(display) as Rc<dyn PlatformDisplay>)
if let Some(display) = WindowsDisplay::primary_monitor() {
Some(Rc::new(display) as Rc<dyn PlatformDisplay>)
} else {
None
}
}
fn active_window(&self) -> Option<AnyWindowHandle> {
let active_window_hwnd = unsafe { GetActiveWindow() };
self.try_get_windows_inner_from_hwnd(active_window_hwnd)
self.inner
.try_get_windows_inner_from_hwnd(active_window_hwnd)
.map(|inner| inner.handle)
}
@@ -338,21 +359,7 @@ impl Platform for WindowsPlatform {
handle: AnyWindowHandle,
options: WindowParams,
) -> Box<dyn PlatformWindow> {
let lock = self.state.borrow();
let window = WindowsWindow::new(
handle,
options,
self.icon,
self.foreground_executor.clone(),
self.main_receiver.clone(),
lock.settings.mouse_wheel_settings,
lock.current_cursor,
);
drop(lock);
let handle = window.get_raw_handle();
self.raw_window_handles.write().push(handle);
Box::new(window)
Box::new(WindowsWindow::new(self.inner.clone(), handle, options))
}
// todo(windows)
@@ -372,8 +379,9 @@ impl Platform for WindowsPlatform {
.detach();
}
// todo(windows)
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
self.state.borrow_mut().callbacks.open_urls = Some(callback);
self.inner.callbacks.lock().open_urls = Some(callback);
}
fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
@@ -493,26 +501,26 @@ impl Platform for WindowsPlatform {
}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
self.state.borrow_mut().callbacks.quit = Some(callback);
self.inner.callbacks.lock().quit = Some(callback);
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
self.state.borrow_mut().callbacks.reopen = Some(callback);
self.inner.callbacks.lock().reopen = Some(callback);
}
// todo(windows)
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.state.borrow_mut().callbacks.app_menu_action = Some(callback);
self.inner.callbacks.lock().app_menu_action = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
self.state.borrow_mut().callbacks.will_open_app_menu = Some(callback);
self.inner.callbacks.lock().will_open_app_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
self.state.borrow_mut().callbacks.validate_app_menu_command = Some(callback);
self.inner.callbacks.lock().validate_app_menu_command = Some(callback);
}
fn os_name(&self) -> &'static str {
@@ -536,100 +544,6 @@ impl Platform for WindowsPlatform {
}
}
fn app_version(&self) -> Result<SemanticVersion> {
let mut file_name_buffer = vec![0u16; MAX_PATH as usize];
let file_name = {
let mut file_name_buffer_capacity = MAX_PATH as usize;
let mut file_name_length;
loop {
file_name_length =
unsafe { GetModuleFileNameW(None, &mut file_name_buffer) } as usize;
if file_name_length < file_name_buffer_capacity {
break;
}
// buffer too small
file_name_buffer_capacity *= 2;
file_name_buffer = vec![0u16; file_name_buffer_capacity];
}
PCWSTR::from_raw(file_name_buffer[0..(file_name_length + 1)].as_ptr())
};
let version_info_block = {
let mut version_handle = 0;
let version_info_size =
unsafe { GetFileVersionInfoSizeW(file_name, Some(&mut version_handle)) } as usize;
if version_info_size == 0 {
log::error!(
"unable to get version info size: {}",
std::io::Error::last_os_error()
);
return Err(anyhow!("unable to get version info size"));
}
let mut version_data = vec![0u8; version_info_size + 2];
unsafe {
GetFileVersionInfoW(
file_name,
version_handle,
version_info_size as u32,
version_data.as_mut_ptr() as _,
)
}
.inspect_err(|_| {
log::error!(
"unable to retrieve version info: {}",
std::io::Error::last_os_error()
)
})?;
version_data
};
let version_info_raw = {
let mut buffer = unsafe { std::mem::zeroed() };
let mut size = 0;
let entry = "\\".encode_utf16().chain(Some(0)).collect_vec();
if !unsafe {
VerQueryValueW(
version_info_block.as_ptr() as _,
PCWSTR::from_raw(entry.as_ptr()),
&mut buffer,
&mut size,
)
}
.as_bool()
{
log::error!(
"unable to query version info data: {}",
std::io::Error::last_os_error()
);
return Err(anyhow!("the specified resource is not valid"));
}
if size == 0 {
log::error!(
"unable to query version info data: {}",
std::io::Error::last_os_error()
);
return Err(anyhow!("no value is available for the specified name"));
}
buffer
};
let version_info = unsafe { &*(version_info_raw as *mut VS_FIXEDFILEINFO) };
// https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
if version_info.dwSignature == 0xFEEF04BD {
return Ok(SemanticVersion::new(
((version_info.dwProductVersionMS >> 16) & 0xFFFF) as usize,
(version_info.dwProductVersionMS & 0xFFFF) as usize,
((version_info.dwProductVersionLS >> 16) & 0xFFFF) as usize,
));
} else {
log::error!(
"no version info present: {}",
std::io::Error::last_os_error()
);
return Err(anyhow!("no version info present"));
}
}
fn app_path(&self) -> Result<PathBuf> {
Ok(std::env::current_exe()?)
}
@@ -659,9 +573,7 @@ impl Platform for WindowsPlatform {
}
fn set_cursor_style(&self, style: CursorStyle) {
let hcursor = load_cursor(style);
self.post_message(CURSOR_STYLE_CHANGED, WPARAM(0), LPARAM(hcursor.0));
self.state.borrow_mut().current_cursor = hcursor;
self.inner.current_cursor.set(load_cursor(style));
}
// todo(windows)
@@ -684,7 +596,7 @@ impl Platform for WindowsPlatform {
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
let mut ctx = ClipboardContext::new().unwrap();
let content = ctx.get_contents().ok()?;
let content = ctx.get_contents().unwrap();
Some(ClipboardItem {
text: content,
metadata: None,
@@ -693,10 +605,10 @@ impl Platform for WindowsPlatform {
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
let mut password = password.to_vec();
let mut username = username.encode_utf16().chain(Some(0)).collect_vec();
let mut username = username.encode_utf16().chain(once(0)).collect_vec();
let mut target_name = windows_credentials_target_name(url)
.encode_utf16()
.chain(Some(0))
.chain(once(0))
.collect_vec();
self.foreground_executor().spawn(async move {
let credentials = CREDENTIALW {
@@ -718,7 +630,7 @@ impl Platform for WindowsPlatform {
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
let mut target_name = windows_credentials_target_name(url)
.encode_utf16()
.chain(Some(0))
.chain(once(0))
.collect_vec();
self.foreground_executor().spawn(async move {
let mut credentials: *mut CREDENTIALW = std::ptr::null_mut();
@@ -751,7 +663,7 @@ impl Platform for WindowsPlatform {
fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
let mut target_name = windows_credentials_target_name(url)
.encode_utf16()
.chain(Some(0))
.chain(once(0))
.collect_vec();
self.foreground_executor().spawn(async move {
unsafe { CredDeleteW(PCWSTR::from_raw(target_name.as_ptr()), CRED_TYPE_GENERIC, 0)? };

View File

@@ -1,81 +0,0 @@
use std::ffi::{c_uint, c_void};
use util::ResultExt;
use windows::Win32::UI::WindowsAndMessaging::{
SystemParametersInfoW, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES,
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
};
/// Windows settings pulled from SystemParametersInfo
/// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
#[derive(Default, Debug)]
pub(crate) struct WindowsPlatformSystemSettings {
pub(crate) mouse_wheel_settings: MouseWheelSettings,
}
#[derive(Default, Debug, Clone, Copy)]
pub(crate) struct MouseWheelSettings {
/// SEE: SPI_GETWHEELSCROLLCHARS
pub(crate) wheel_scroll_chars: u32,
/// SEE: SPI_GETWHEELSCROLLLINES
pub(crate) wheel_scroll_lines: u32,
}
impl WindowsPlatformSystemSettings {
pub(crate) fn new() -> Self {
let mut settings = Self::default();
settings.init();
settings
}
fn init(&mut self) {
self.mouse_wheel_settings.update();
}
}
impl MouseWheelSettings {
pub(crate) fn update(&mut self) -> (Option<u32>, Option<u32>) {
(
self.update_wheel_scroll_chars(),
self.update_wheel_scroll_lines(),
)
}
fn update_wheel_scroll_chars(&mut self) -> Option<u32> {
let mut value = c_uint::default();
let result = unsafe {
SystemParametersInfoW(
SPI_GETWHEELSCROLLCHARS,
0,
Some((&mut value) as *mut c_uint as *mut c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
)
};
if result.log_err() != None && self.wheel_scroll_chars != value {
self.wheel_scroll_chars = value;
Some(value)
} else {
None
}
}
fn update_wheel_scroll_lines(&mut self) -> Option<u32> {
let mut value = c_uint::default();
let result = unsafe {
SystemParametersInfoW(
SPI_GETWHEELSCROLLLINES,
0,
Some((&mut value) as *mut c_uint as *mut c_void),
SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS::default(),
)
};
if result.log_err() != None && self.wheel_scroll_lines != value {
self.wheel_scroll_lines = value;
Some(value)
} else {
None
}
}
}

View File

@@ -74,7 +74,6 @@ pub(crate) unsafe fn set_window_long(
}
}
#[derive(Debug, Clone)]
pub(crate) struct OwnedHandle(HANDLE);
impl OwnedHandle {
@@ -118,14 +117,12 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => (&IBEAM, IDC_IBEAM),
CursorStyle::Crosshair => (&CROSS, IDC_CROSS),
CursorStyle::PointingHand | CursorStyle::DragLink => (&HAND, IDC_HAND),
CursorStyle::ResizeLeft
| CursorStyle::ResizeRight
| CursorStyle::ResizeLeftRight
| CursorStyle::ResizeColumn => (&SIZEWE, IDC_SIZEWE),
CursorStyle::ResizeUp
| CursorStyle::ResizeDown
| CursorStyle::ResizeUpDown
| CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS),
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
(&SIZEWE, IDC_SIZEWE)
}
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
(&SIZENS, IDC_SIZENS)
}
CursorStyle::OperationNotAllowed => (&NO, IDC_NO),
_ => (&ARROW, IDC_ARROW),
};
@@ -138,19 +135,3 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
)
})
}
#[inline]
pub(crate) fn logical_size(physical_size: Size<DevicePixels>, scale_factor: f32) -> Size<Pixels> {
Size {
width: px(physical_size.width.0 as f32 / scale_factor),
height: px(physical_size.height.0 as f32 / scale_factor),
}
}
#[inline]
pub(crate) fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point<Pixels> {
Point {
x: px(x / scale_factor),
y: px(y / scale_factor),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,8 @@ use crate::{
RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams,
WindowTextSystem, SUBPIXEL_VARIANTS,
WindowAppearance, WindowBackgroundAppearance, WindowOptions, WindowParams, WindowTextSystem,
SUBPIXEL_VARIANTS,
};
use anyhow::{anyhow, Context as _, Result};
use collections::{FxHashMap, FxHashSet};
@@ -565,7 +565,7 @@ fn default_bounds(display_id: Option<DisplayId>, cx: &mut AppContext) -> Bounds<
const DEFAULT_WINDOW_OFFSET: Point<DevicePixels> = point(DevicePixels(0), DevicePixels(35));
cx.active_window()
.and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok())
.and_then(|w| w.update(cx, |_, cx| cx.window_bounds()).ok())
.map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET))
.unwrap_or_else(|| {
let display = display_id
@@ -592,20 +592,19 @@ impl Window {
cx: &mut AppContext,
) -> Self {
let WindowOptions {
window_bounds,
bounds,
titlebar,
focus,
show,
kind,
is_movable,
display_id,
fullscreen,
window_background,
app_id,
} = options;
let bounds = window_bounds
.map(|bounds| bounds.get_bounds())
.unwrap_or_else(|| default_bounds(display_id, cx));
let bounds = bounds.unwrap_or_else(|| default_bounds(display_id, cx));
let mut platform_window = cx.platform.open_window(
handle,
WindowParams {
@@ -633,12 +632,8 @@ impl Window {
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
if let Some(ref window_open_state) = window_bounds {
match window_open_state {
WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(),
WindowBounds::Maximized(_) => platform_window.zoom(),
WindowBounds::Windowed(_) => {}
}
if fullscreen {
platform_window.toggle_fullscreen();
}
platform_window.on_close(Box::new({
@@ -696,7 +691,7 @@ impl Window {
let mut cx = cx.to_async();
move |_, _| {
handle
.update(&mut cx, |_, cx| cx.bounds_changed())
.update(&mut cx, |_, cx| cx.window_bounds_changed())
.log_err();
}
}));
@@ -704,7 +699,7 @@ impl Window {
let mut cx = cx.to_async();
move || {
handle
.update(&mut cx, |_, cx| cx.bounds_changed())
.update(&mut cx, |_, cx| cx.window_bounds_changed())
.log_err();
}
}));
@@ -948,10 +943,10 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.is_maximized()
}
/// Return the `WindowBounds` to indicate that how a window should be opened
/// after it has been closed
pub fn window_bounds(&self) -> WindowBounds {
self.window.platform_window.window_bounds()
/// Check if the platform window is minimized
/// On some platforms (namely Windows) the position is incorrect when minimized
pub fn is_minimized(&self) -> bool {
self.window.platform_window.is_minimized()
}
/// Dispatch the given action on the currently focused element.
@@ -1080,7 +1075,7 @@ impl<'a> WindowContext<'a> {
.spawn(|app| f(AsyncWindowContext::new(app, self.window.handle)))
}
fn bounds_changed(&mut self) {
fn window_bounds_changed(&mut self) {
self.window.scale_factor = self.window.platform_window.scale_factor();
self.window.viewport_size = self.window.platform_window.content_size();
self.window.display_id = self.window.platform_window.display().id();
@@ -1093,7 +1088,7 @@ impl<'a> WindowContext<'a> {
}
/// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays.
pub fn bounds(&self) -> Bounds<DevicePixels> {
pub fn window_bounds(&self) -> Bounds<DevicePixels> {
self.window.platform_window.bounds()
}

View File

@@ -564,7 +564,12 @@ impl Buffer {
let buffer_id = BufferId::new(message.id)
.with_context(|| anyhow!("Could not deserialize buffer_id"))?;
let buffer = TextBuffer::new(replica_id, buffer_id, message.base_text);
let mut this = Self::build(buffer, message.diff_base, file, capability);
let mut this = Self::build(
buffer,
message.diff_base.map(|text| text.into()),
file,
capability,
);
this.text.set_line_ending(proto::deserialize_line_ending(
rpc::proto::LineEnding::from_i32(message.line_ending)
.ok_or_else(|| anyhow!("missing line_ending"))?,
@@ -653,7 +658,7 @@ impl Buffer {
/// Builds a [Buffer] with the given underlying [TextBuffer], diff base, [File] and [Capability].
pub fn build(
buffer: TextBuffer,
diff_base: Option<String>,
diff_base: Option<Rope>,
file: Option<Arc<dyn File>>,
capability: Capability,
) -> Self {
@@ -666,12 +671,7 @@ impl Buffer {
transaction_depth: 0,
was_dirty_before_starting_transaction: None,
text: buffer,
diff_base: diff_base
.map(|mut raw_diff_base| {
LineEnding::normalize(&mut raw_diff_base);
raw_diff_base
})
.map(Rope::from),
diff_base,
diff_base_version: 0,
git_diff: git::diff::BufferDiff::new(),
file,
@@ -900,13 +900,8 @@ impl Buffer {
/// Sets the text that will be used to compute a Git diff
/// against the buffer text.
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
self.diff_base = diff_base
.map(|mut raw_diff_base| {
LineEnding::normalize(&mut raw_diff_base);
raw_diff_base
})
.map(Rope::from);
pub fn set_diff_base(&mut self, diff_base: Option<Rope>, cx: &mut ModelContext<Self>) {
self.diff_base = diff_base;
self.diff_base_version += 1;
if let Some(recalc_task) = self.git_diff_recalc(cx) {
cx.spawn(|buffer, mut cx| async move {
@@ -928,7 +923,7 @@ impl Buffer {
/// Recomputes the Git diff status.
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
let diff_base = self.diff_base.clone()?;
let diff_base = self.diff_base.clone()?; // TODO: Make this an Arc
let snapshot = self.snapshot();
let mut diff = self.git_diff.clone();

View File

@@ -35,27 +35,14 @@ struct ProjectState {
}
struct LanguageServerState {
kind: LanguageServerKind,
name: LanguageServerName,
log_messages: VecDeque<String>,
rpc_state: Option<LanguageServerRpcState>,
project: Option<WeakModel<Project>>,
_io_logs_subscription: Option<lsp::Subscription>,
_lsp_logs_subscription: Option<lsp::Subscription>,
}
enum LanguageServerKind {
Local { project: WeakModel<Project> },
Global { name: LanguageServerName },
}
impl LanguageServerKind {
fn project(&self) -> Option<&WeakModel<Project>> {
match self {
Self::Local { project } => Some(project),
Self::Global { .. } => None,
}
}
}
struct LanguageServerRpcState {
rpc_messages: VecDeque<String>,
last_message_kind: Option<MessageKind>,
@@ -151,9 +138,8 @@ impl LogStore {
},
));
this.add_language_server(
LanguageServerKind::Global {
name: LanguageServerName(Arc::from("copilot")),
},
None,
LanguageServerName(Arc::from("copilot")),
server.clone(),
cx,
);
@@ -194,16 +180,18 @@ impl LogStore {
cx.observe_release(project, move |this, _, _| {
this.projects.remove(&weak_project);
this.language_servers
.retain(|_, state| state.kind.project() != Some(&weak_project));
.retain(|_, state| state.project.as_ref() != Some(&weak_project));
}),
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::LanguageServerAdded(id) => {
let read_project = project.read(cx);
if let Some(server) = read_project.language_server_for_id(*id) {
if let Some((server, adapter)) = read_project
.language_server_for_id(*id)
.zip(read_project.language_server_adapter_for_id(*id))
{
this.add_language_server(
LanguageServerKind::Local {
project: project.downgrade(),
},
Some(&project.downgrade()),
adapter.name.clone(),
server,
cx,
);
@@ -231,7 +219,8 @@ impl LogStore {
fn add_language_server(
&mut self,
kind: LanguageServerKind,
project: Option<&WeakModel<Project>>,
name: LanguageServerName,
server: Arc<LanguageServer>,
cx: &mut ModelContext<Self>,
) -> Option<&mut LanguageServerState> {
@@ -241,8 +230,9 @@ impl LogStore {
.or_insert_with(|| {
cx.notify();
LanguageServerState {
kind,
name,
rpc_state: None,
project: project.cloned(),
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
_io_logs_subscription: None,
_lsp_logs_subscription: None,
@@ -310,20 +300,23 @@ impl LogStore {
fn server_ids_for_project<'a>(
&'a self,
lookup_project: &'a WeakModel<Project>,
project: &'a WeakModel<Project>,
) -> impl Iterator<Item = LanguageServerId> + 'a {
self.language_servers
.iter()
.filter_map(move |(id, state)| match &state.kind {
LanguageServerKind::Local { project } => {
if project == lookup_project {
Some(*id)
} else {
None
}
[].into_iter()
.chain(self.language_servers.iter().filter_map(|(id, state)| {
if state.project.as_ref() == Some(project) {
return Some(*id);
} else {
None
}
LanguageServerKind::Global { .. } => Some(*id),
})
}))
.chain(self.language_servers.iter().filter_map(|(id, state)| {
if state.project.is_none() {
return Some(*id);
} else {
None
}
}))
}
fn enable_rpc_trace_for_language_server(
@@ -415,7 +408,7 @@ impl LspLogView {
.read(cx)
.language_servers
.iter()
.find(|(_, server)| server.kind.project() == Some(&project.downgrade()))
.find(|(_, server)| server.project == Some(project.downgrade()))
.map(|(id, _)| *id);
let weak_project = project.downgrade();
@@ -569,18 +562,21 @@ impl LspLogView {
log_store
.language_servers
.iter()
.filter_map(|(server_id, state)| match &state.kind {
LanguageServerKind::Global { name } => Some(LogMenuItem {
server_id: *server_id,
server_name: name.clone(),
worktree_root_name: "supplementary".to_string(),
rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace
&& self.current_server_id == Some(*server_id),
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(*server_id),
}),
_ => None,
.filter_map(|(server_id, state)| {
if state.project.is_none() {
Some(LogMenuItem {
server_id: *server_id,
server_name: state.name.clone(),
worktree_root_name: "supplementary".to_string(),
rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace
&& self.current_server_id == Some(*server_id),
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(*server_id),
})
} else {
None
}
}),
)
.collect::<Vec<_>>();

View File

@@ -1,13 +0,0 @@
; Refer to https://github.com/nvim-treesitter/nvim-treesitter/blob/master/queries/go/injections.scm#L4C1-L16C41
(call_expression
(selector_expression) @_function
(#any-of? @_function
"regexp.Match" "regexp.MatchReader" "regexp.MatchString" "regexp.Compile" "regexp.CompilePOSIX"
"regexp.MustCompile" "regexp.MustCompilePOSIX")
(argument_list
.
[
(raw_string_literal)
(interpreted_string_literal)
] @content
(#set! "language" "regex")))

View File

@@ -97,7 +97,7 @@ use std::{
};
use task::static_source::{StaticSource, TrackedFile};
use terminals::Terminals;
use text::{Anchor, BufferId, LineEnding};
use text::{Anchor, BufferId, LineEnding, Rope};
use util::{
debug_panic, defer,
http::{HttpClient, Url},
@@ -7559,7 +7559,11 @@ impl Project {
None
} else {
let relative_path = path.strip_prefix(&work_directory).ok()?;
repo_entry.repo().lock().load_index_text(relative_path)
repo_entry
.repo()
.lock()
.load_index_text(relative_path)
.map(Rope::from)
};
Some((buffer, base_text))
}
@@ -7587,7 +7591,7 @@ impl Project {
.send(proto::UpdateDiffBase {
project_id,
buffer_id,
diff_base,
diff_base: diff_base.map(|rope| rope.to_string()),
})
.log_err();
}
@@ -7864,18 +7868,6 @@ impl Project {
})
}
pub fn project_path_for_absolute_path(
&self,
abs_path: &Path,
cx: &AppContext,
) -> Option<ProjectPath> {
self.find_local_worktree(abs_path, cx)
.map(|(worktree, relative_path)| ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
})
}
pub fn get_workspace_root(
&self,
project_path: &ProjectPath,
@@ -8674,15 +8666,14 @@ impl Project {
this.update(&mut cx, |this, cx| {
let buffer_id = envelope.payload.buffer_id;
let buffer_id = BufferId::new(buffer_id)?;
let diff_base = envelope.payload.diff_base.map(Rope::from);
if let Some(buffer) = this
.opened_buffers
.get_mut(&buffer_id)
.and_then(|b| b.upgrade())
.or_else(|| this.incomplete_remote_buffers.get(&buffer_id).cloned())
{
buffer.update(cx, |buffer, cx| {
buffer.set_diff_base(envelope.payload.diff_base, cx)
});
buffer.update(cx, |buffer, cx| buffer.set_diff_base(diff_base, cx));
}
Ok(())
})?

View File

@@ -250,7 +250,6 @@ impl SearchQuery {
}
}
}
pub async fn search(
&self,
buffer: &BufferSnapshot,

View File

@@ -1,7 +1,7 @@
use crate::Project;
use collections::HashMap;
use gpui::{AnyWindowHandle, Context, Entity, Model, ModelContext, WeakModel};
use settings::{Settings, SettingsLocation};
use settings::Settings;
use smol::channel::bounded;
use std::path::{Path, PathBuf};
use task::SpawnInTerminal;
@@ -31,25 +31,8 @@ impl Project {
"creating terminals as a guest is not supported yet"
);
// used only for TerminalSettings::get
let worktree = {
let terminal_cwd = working_directory.as_deref();
let task_cwd = spawn_task
.as_ref()
.and_then(|spawn_task| spawn_task.cwd.as_deref());
terminal_cwd
.and_then(|terminal_cwd| self.find_local_worktree(terminal_cwd, cx))
.or_else(|| task_cwd.and_then(|spawn_cwd| self.find_local_worktree(spawn_cwd, cx)))
};
let settings_location = worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id().to_usize(),
path,
});
let is_terminal = spawn_task.is_none();
let settings = TerminalSettings::get(settings_location, cx);
let settings = TerminalSettings::get_global(cx);
let python_settings = settings.detect_venv.clone();
let (completion_tx, completion_rx) = bounded(1);

View File

@@ -63,7 +63,7 @@ impl AppVersion {
from_env.parse().expect("invalid ZED_APP_VERSION")
} else {
cx.app_metadata()
.app_version
.version
.unwrap_or_else(|| pkg_version.parse().expect("invalid version in Cargo.toml"))
};
cx.set_global(GlobalAppVersion(version))

View File

@@ -450,7 +450,7 @@ pub struct WorktreeSearchResult {
pub score: f32,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Status {
Idle,
Loading,

View File

@@ -7,8 +7,7 @@ mod story_selector;
use clap::Parser;
use dialoguer::FuzzySelect;
use gpui::{
div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowBounds,
WindowOptions,
div, px, size, AnyView, AppContext, Bounds, Render, ViewContext, VisualContext, WindowOptions,
};
use log::LevelFilter;
use project::Project;
@@ -86,7 +85,7 @@ fn main() {
let bounds = Bounds::centered(None, size, cx);
let _window = cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
bounds: Some(bounds),
..Default::default()
},
move |cx| {

View File

@@ -10,9 +10,7 @@ use collections::BTreeMap;
use futures::{channel::mpsc, io::BufReader, AsyncBufReadExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, EntityId, Global, Model, ModelContext, Task, WeakModel};
use language::{
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, ToOffset,
};
use language::{language_settings::all_language_settings, Anchor, Buffer, ToOffset};
use messages::*;
use postage::watch;
use serde::{Deserialize, Serialize};
@@ -21,7 +19,7 @@ use smol::{
io::AsyncWriteExt,
process::{Child, ChildStdin, ChildStdout, Command},
};
use std::{path::PathBuf, process::Stdio, sync::Arc};
use std::{ops::Range, path::PathBuf, process::Stdio, sync::Arc};
use ui::prelude::*;
use util::ResultExt;
@@ -130,9 +128,9 @@ impl Supermaven {
state_id,
SupermavenCompletionState {
buffer_id,
prefix_anchor: cursor_position,
range: cursor_position.bias_left(buffer)..cursor_position.bias_right(buffer),
completion: Vec::new(),
text: String::new(),
dedent: String::new(),
updates_tx,
},
);
@@ -160,64 +158,16 @@ impl Supermaven {
pub fn completion(
&self,
buffer: &Model<Buffer>,
cursor_position: Anchor,
cx: &AppContext,
) -> Option<&str> {
id: SupermavenCompletionStateId,
) -> Option<&SupermavenCompletionState> {
if let Self::Spawned(agent) = self {
find_relevant_completion(
&agent.states,
buffer.entity_id(),
&buffer.read(cx).snapshot(),
cursor_position,
)
agent.states.get(&id)
} else {
None
}
}
}
fn find_relevant_completion<'a>(
states: &'a BTreeMap<SupermavenCompletionStateId, SupermavenCompletionState>,
buffer_id: EntityId,
buffer: &BufferSnapshot,
cursor_position: Anchor,
) -> Option<&'a str> {
let mut best_completion: Option<&str> = None;
'completions: for state in states.values() {
if state.buffer_id != buffer_id {
continue;
}
let Some(state_completion) = state.text.strip_prefix(&state.dedent) else {
continue;
};
let current_cursor_offset = cursor_position.to_offset(buffer);
let original_cursor_offset = state.prefix_anchor.to_offset(buffer);
if current_cursor_offset < original_cursor_offset {
continue;
}
let text_inserted_since_completion_request =
buffer.text_for_range(original_cursor_offset..current_cursor_offset);
let mut trimmed_completion = state_completion;
for chunk in text_inserted_since_completion_request {
if let Some(suffix) = trimmed_completion.strip_prefix(chunk) {
trimmed_completion = suffix;
} else {
continue 'completions;
}
}
if best_completion.map_or(false, |best| best.len() > trimmed_completion.len()) {
continue;
}
best_completion = Some(trimmed_completion);
}
best_completion
}
pub struct SupermavenAgent {
_process: Child,
next_state_id: SupermavenCompletionStateId,
@@ -361,12 +311,11 @@ impl SupermavenAgent {
let state_id = SupermavenCompletionStateId(response.state_id.parse().unwrap());
if let Some(state) = self.states.get_mut(&state_id) {
for item in &response.items {
match item {
ResponseItem::Text { text } => state.text.push_str(text),
ResponseItem::Dedent { text } => state.dedent.push_str(text),
_ => {}
if let ResponseItem::Text { text } = item {
state.text.push_str(text);
}
}
state.completion.extend(response.items);
*state.updates_tx.borrow_mut() = ();
}
}
@@ -384,9 +333,9 @@ pub struct SupermavenCompletionStateId(usize);
#[allow(dead_code)]
pub struct SupermavenCompletionState {
buffer_id: EntityId,
prefix_anchor: Anchor,
range: Range<Anchor>,
completion: Vec<ResponseItem>,
text: String,
dedent: String,
updates_tx: watch::Sender<()>,
}

View File

@@ -3,7 +3,9 @@ use anyhow::Result;
use editor::{Direction, InlineCompletionProvider};
use futures::StreamExt as _;
use gpui::{AppContext, Model, ModelContext, Task};
use language::{language_settings::all_language_settings, Anchor, Buffer};
use language::{
language_settings::all_language_settings, Anchor, Buffer, OffsetRangeExt as _, ToOffset,
};
use std::time::Duration;
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
@@ -90,16 +92,29 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
cursor_position: Anchor,
cx: &'a AppContext,
) -> Option<&'a str> {
let completion_text = self
.supermaven
.read(cx)
.completion(buffer, cursor_position, cx)?;
let completion_id = self.completion_id?;
let buffer = buffer.read(cx);
let cursor_offset = cursor_position.to_offset(buffer);
let completion = self.supermaven.read(cx).completion(completion_id)?;
let completion_text = trim_to_end_of_line_unless_leading_newline(completion_text);
let mut completion_range = completion.range.to_offset(buffer);
let completion_text = completion_text.trim_end();
let prefix_len = common_prefix(
buffer.chars_for_range(completion_range.clone()),
completion.text.chars(),
);
completion_range.start += prefix_len;
let suffix_len = common_prefix(
buffer.reversed_chars_for_range(completion_range.clone()),
completion.text[prefix_len..].chars().rev(),
);
completion_range.end = completion_range.end.saturating_sub(suffix_len);
if !completion_text.trim().is_empty() {
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
if completion_range.is_empty()
&& completion_range.start == cursor_offset
&& !completion_text.trim().is_empty()
{
Some(completion_text)
} else {
None
@@ -107,24 +122,9 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
}
}
fn trim_to_end_of_line_unless_leading_newline(text: &str) -> &str {
if has_leading_newline(&text) {
text
} else if let Some(i) = text.find('\n') {
&text[..i]
} else {
text
}
}
fn has_leading_newline(text: &str) -> bool {
for c in text.chars() {
if c == '\n' {
return true;
}
if !c.is_whitespace() {
return false;
}
}
false
fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
a.zip(b)
.take_while(|(a, b)| a == b)
.map(|(a, _)| a.len_utf8())
.sum()
}

View File

@@ -74,7 +74,6 @@ impl TerminalPanel {
pane.set_can_split(false, cx);
pane.set_can_navigate(false, cx);
pane.display_nav_history_buttons(None);
pane.set_should_display_tab_bar(|_| true);
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
h_flex()
.gap_2()
@@ -597,7 +596,6 @@ impl TerminalPanel {
.workspace
.update(cx, |workspace, _| workspace.project().clone())
.ok()?;
let reveal = spawn_task.reveal;
let window = cx.window_handle();
let new_terminal = project.update(cx, |project, cx| {

View File

@@ -41,17 +41,6 @@ impl RenderOnce for AnyIcon {
}
}
/// The decoration for an icon.
///
/// For example, this can show an indicator, an "x",
/// or a diagonal strkethrough to indicate something is disabled.
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
pub enum IconDecoration {
Strikethrough,
IndicatorDot,
X,
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum IconSize {
Indicator,
@@ -130,8 +119,6 @@ pub enum IconName {
FolderX,
Github,
Hash,
Indicator,
IndicatorX,
InlayHint,
Link,
MagicWand,
@@ -172,7 +159,6 @@ pub enum IconName {
SupermavenDisabled,
SupermavenError,
SupermavenInit,
Strikethrough,
Tab,
Terminal,
Trash,
@@ -243,8 +229,6 @@ impl IconName {
IconName::FolderX => "icons/stop_sharing.svg",
IconName::Github => "icons/github.svg",
IconName::Hash => "icons/hash.svg",
IconName::Indicator => "icons/indicator.svg",
IconName::IndicatorX => "icons/indicator_x.svg",
IconName::InlayHint => "icons/inlay_hint.svg",
IconName::Link => "icons/link.svg",
IconName::MagicWand => "icons/magic_wand.svg",
@@ -285,7 +269,6 @@ impl IconName {
IconName::SupermavenDisabled => "icons/supermaven_disabled.svg",
IconName::SupermavenError => "icons/supermaven_error.svg",
IconName::SupermavenInit => "icons/supermaven_init.svg",
IconName::Strikethrough => "icons/strikethrough.svg",
IconName::Tab => "icons/tab.svg",
IconName::Terminal => "icons/terminal.svg",
IconName::Trash => "icons/trash.svg",
@@ -361,80 +344,6 @@ impl RenderOnce for Icon {
}
}
#[derive(IntoElement)]
pub struct DecoratedIcon {
icon: Icon,
decoration: IconDecoration,
decoration_color: Color,
parent_background: Option<Hsla>,
}
impl DecoratedIcon {
pub fn new(icon: Icon, decoration: IconDecoration) -> Self {
Self {
icon,
decoration,
decoration_color: Color::Default,
parent_background: None,
}
}
pub fn decoration_color(mut self, color: Color) -> Self {
self.decoration_color = color;
self
}
pub fn parent_background(mut self, background: Option<Hsla>) -> Self {
self.parent_background = background;
self
}
}
impl RenderOnce for DecoratedIcon {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let background = self
.parent_background
.unwrap_or(cx.theme().colors().background);
let size = self.icon.size;
let decoration_icon = match self.decoration {
IconDecoration::Strikethrough => IconName::Strikethrough,
IconDecoration::IndicatorDot => IconName::Indicator,
IconDecoration::X => IconName::IndicatorX,
};
let decoration_svg = |icon: IconName| {
svg()
.absolute()
.top_0()
.left_0()
.path(icon.path())
.size(size)
.flex_none()
.text_color(self.decoration_color.color(cx))
};
let decoration_knockout = |icon: IconName| {
svg()
.absolute()
.top(-rems_from_px(2.))
.left(-rems_from_px(3.))
.path(icon.path())
.size(size + rems_from_px(2.))
.flex_none()
.text_color(background)
};
div()
.relative()
.size(self.icon.size)
.child(self.icon)
.child(decoration_knockout(decoration_icon))
.child(decoration_svg(decoration_icon))
}
}
#[derive(IntoElement)]
pub struct IconWithIndicator {
icon: Icon,

View File

@@ -2,7 +2,7 @@ use gpui::Render;
use story::Story;
use strum::IntoEnumIterator;
use crate::{prelude::*, DecoratedIcon, IconDecoration};
use crate::prelude::*;
use crate::{Icon, IconName};
pub struct IconStory;
@@ -13,23 +13,6 @@ impl Render for IconStory {
Story::container()
.child(Story::title_for::<Icon>())
.child(Story::label("DecoratedIcon"))
.child(DecoratedIcon::new(
Icon::new(IconName::Bell).color(Color::Muted),
IconDecoration::IndicatorDot,
))
.child(
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot)
.decoration_color(Color::Accent),
)
.child(DecoratedIcon::new(
Icon::new(IconName::Bell).color(Color::Muted),
IconDecoration::Strikethrough,
))
.child(
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::X)
.decoration_color(Color::Error),
)
.child(Story::label("All Icons"))
.child(div().flex().gap_3().children(icons.map(Icon::new)))
}

View File

@@ -27,13 +27,7 @@ pub enum Spacing {
///
/// Relative to the user's `ui_font_size` and [UiDensity] setting.
Large,
/// Extra Large spacing - @16px/rem: `8px`|`12px`|`16px`
///
/// Relative to the user's `ui_font_size` and [UiDensity] setting.
XLarge,
/// 2X Large spacing - @16px/rem: `12px`|`16px`|`20px`
///
/// Relative to the user's `ui_font_size` and [UiDensity] setting.
XXLarge,
}

View File

@@ -159,20 +159,14 @@ impl PickerDelegate for BranchListDelegate {
let candidates = picker.update(&mut cx, |view, _| {
const RECENT_BRANCHES_COUNT: usize = 10;
let mut branches = view.delegate.all_branches.clone();
if query.is_empty() {
if branches.len() > RECENT_BRANCHES_COUNT {
// Truncate list of recent branches
// Do a partial sort to show recent-ish branches first.
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
rhs.is_head
.cmp(&lhs.is_head)
.then(rhs.unix_timestamp.cmp(&lhs.unix_timestamp))
});
branches.truncate(RECENT_BRANCHES_COUNT);
}
branches.sort_unstable_by(|lhs, rhs| {
rhs.is_head.cmp(&lhs.is_head).then(lhs.name.cmp(&rhs.name))
if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
// Truncate list of recent branches
// Do a partial sort to show recent-ish branches first.
branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
});
branches.truncate(RECENT_BRANCHES_COUNT);
branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
}
branches
.into_iter()

View File

@@ -69,7 +69,7 @@ impl ClosePosition {
pub struct ItemSettingsContent {
/// Whether to show the Git file status on a tab item.
///
/// Default: false
/// Default: true
git_status: Option<bool>,
/// Position of the close button in a tab.
///

View File

@@ -203,7 +203,6 @@ pub struct Pane {
custom_drop_handle:
Option<Arc<dyn Fn(&mut Pane, &dyn Any, &mut ViewContext<Pane>) -> ControlFlow<(), ()>>>,
can_split: bool,
should_display_tab_bar: Rc<dyn Fn(&ViewContext<Pane>) -> bool>,
render_tab_bar_buttons: Rc<dyn Fn(&mut Pane, &mut ViewContext<Pane>) -> AnyElement>,
_subscriptions: Vec<Subscription>,
tab_bar_scroll_handle: ScrollHandle,
@@ -313,7 +312,6 @@ impl Pane {
can_drop_predicate,
custom_drop_handle: None,
can_split: true,
should_display_tab_bar: Rc::new(|cx| TabBarSettings::get_global(cx).show),
render_tab_bar_buttons: Rc::new(move |pane, cx| {
// Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
// `end_slot`, but due to needing a view here that isn't possible.
@@ -470,13 +468,6 @@ impl Pane {
&self.activation_history
}
pub fn set_should_display_tab_bar<F>(&mut self, should_display_tab_bar: F)
where
F: 'static + Fn(&ViewContext<Pane>) -> bool,
{
self.should_display_tab_bar = Rc::new(should_display_tab_bar);
}
pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext<Self>) {
self.can_split = can_split;
cx.notify();
@@ -1972,9 +1963,6 @@ impl Render for Pane {
key_context.add("EmptyPane");
}
let should_display_tab_bar = self.should_display_tab_bar.clone();
let display_tab_bar = should_display_tab_bar(cx);
v_flex()
.key_context(key_context)
.track_focus(&self.focus_handle)
@@ -2073,7 +2061,7 @@ impl Render for Pane {
}
}),
)
.when(self.active_item().is_some() && display_tab_bar, |pane| {
.when(self.active_item().is_some(), |pane| {
pane.child(self.render_tab_bar(cx))
})
.child({

View File

@@ -5,7 +5,7 @@ use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use client::DevServerProjectId;
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::{point, size, Axis, Bounds, WindowBounds};
use gpui::{point, size, Axis, Bounds};
use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
@@ -59,99 +59,50 @@ impl sqlez::bindable::Column for SerializedAxis {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct SerializedWindowsBounds(pub(crate) Bounds<gpui::DevicePixels>);
impl StaticColumnCount for SerializedWindowBounds {
impl StaticColumnCount for SerializedWindowsBounds {
fn column_count() -> usize {
5
}
}
impl Bind for SerializedWindowBounds {
impl Bind for SerializedWindowsBounds {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
match self.0 {
WindowBounds::Windowed(bounds) => {
let next_index = statement.bind(&"Windowed", start_index)?;
statement.bind(
&(
SerializedDevicePixels(bounds.origin.x),
SerializedDevicePixels(bounds.origin.y),
SerializedDevicePixels(bounds.size.width),
SerializedDevicePixels(bounds.size.height),
),
next_index,
)
}
WindowBounds::Maximized(bounds) => {
let next_index = statement.bind(&"Maximized", start_index)?;
statement.bind(
&(
SerializedDevicePixels(bounds.origin.x),
SerializedDevicePixels(bounds.origin.y),
SerializedDevicePixels(bounds.size.width),
SerializedDevicePixels(bounds.size.height),
),
next_index,
)
}
WindowBounds::Fullscreen(bounds) => {
let next_index = statement.bind(&"FullScreen", start_index)?;
statement.bind(
&(
SerializedDevicePixels(bounds.origin.x),
SerializedDevicePixels(bounds.origin.y),
SerializedDevicePixels(bounds.size.width),
SerializedDevicePixels(bounds.size.height),
),
next_index,
)
}
}
let next_index = statement.bind(&"Fixed", start_index)?;
statement.bind(
&(
SerializedDevicePixels(self.0.origin.x),
SerializedDevicePixels(self.0.origin.y),
SerializedDevicePixels(self.0.size.width),
SerializedDevicePixels(self.0.size.height),
),
next_index,
)
}
}
impl Column for SerializedWindowBounds {
impl Column for SerializedWindowsBounds {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (window_state, next_index) = String::column(statement, start_index)?;
let status = match window_state.as_str() {
"Windowed" | "Fixed" => {
let bounds = match window_state.as_str() {
"Fixed" => {
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
let x: i32 = x;
let y: i32 = y;
let width: i32 = width;
let height: i32 = height;
SerializedWindowBounds(WindowBounds::Windowed(Bounds {
SerializedWindowsBounds(Bounds {
origin: point(x.into(), y.into()),
size: size(width.into(), height.into()),
}))
}
"Maximized" => {
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
let x: i32 = x;
let y: i32 = y;
let width: i32 = width;
let height: i32 = height;
SerializedWindowBounds(WindowBounds::Maximized(Bounds {
origin: point(x.into(), y.into()),
size: size(width.into(), height.into()),
}))
}
"FullScreen" => {
let ((x, y, width, height), _) = Column::column(statement, next_index)?;
let x: i32 = x;
let y: i32 = y;
let width: i32 = width;
let height: i32 = height;
SerializedWindowBounds(WindowBounds::Fullscreen(Bounds {
origin: point(x.into(), y.into()),
size: size(width.into(), height.into()),
}))
})
}
_ => bail!("Window State did not have a valid string"),
};
Ok((status, next_index + 4))
Ok((bounds, next_index + 4))
}
}
@@ -328,8 +279,6 @@ define_connection! {
ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
),
// Add fullscreen field to workspace
// Deprecated, `WindowBounds` holds the fullscreen state now.
// Preserving so users can downgrade Zed.
sql!(
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
),
@@ -379,17 +328,19 @@ impl WorkspaceDb {
workspace_id,
local_paths,
dev_server_project_id,
window_bounds,
bounds,
display,
fullscreen,
centered_layout,
docks,
): (
WorkspaceId,
Option<LocalPaths>,
Option<u64>,
Option<SerializedWindowBounds>,
Option<SerializedWindowsBounds>,
Option<Uuid>,
Option<bool>,
Option<bool>,
DockStructure,
) = self
.select_row_bound(sql! {
@@ -403,6 +354,7 @@ impl WorkspaceDb {
window_width,
window_height,
display,
fullscreen,
centered_layout,
left_dock_visible,
left_dock_active_panel,
@@ -446,7 +398,8 @@ impl WorkspaceDb {
.get_center_pane_group(workspace_id)
.context("Getting center group")
.log_err()?,
window_bounds,
bounds: bounds.map(|bounds| bounds.0),
fullscreen: fullscreen.unwrap_or(false),
centered_layout: centered_layout.unwrap_or(false),
display,
docks,
@@ -596,12 +549,13 @@ impl WorkspaceDb {
pub(crate) fn last_window(
&self,
) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
let mut prepared_query =
self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
self.select::<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)>(sql!(
SELECT
display,
window_state, window_x, window_y, window_width, window_height
window_state, window_x, window_y, window_width, window_height,
fullscreen
FROM workspaces
WHERE local_paths
IS NOT NULL
@@ -609,7 +563,10 @@ impl WorkspaceDb {
LIMIT 1
))?;
let result = prepared_query()?;
Ok(result.into_iter().next().unwrap_or_else(|| (None, None)))
Ok(result
.into_iter()
.next()
.unwrap_or_else(|| (None, None, None)))
}
query! {
@@ -872,7 +829,7 @@ impl WorkspaceDb {
}
query! {
pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
pub(crate) async fn set_window_bounds(workspace_id: WorkspaceId, bounds: SerializedWindowsBounds, display: Uuid) -> Result<()> {
UPDATE workspaces
SET window_state = ?2,
window_x = ?3,
@@ -884,6 +841,14 @@ impl WorkspaceDb {
}
}
query! {
pub(crate) async fn set_fullscreen(workspace_id: WorkspaceId, fullscreen: bool) -> Result<()> {
UPDATE workspaces
SET fullscreen = ?2
WHERE workspace_id = ?1
}
}
query! {
pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
UPDATE workspaces
@@ -973,9 +938,10 @@ mod tests {
id: WorkspaceId(1),
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
window_bounds: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
fullscreen: false,
centered_layout: false,
};
@@ -983,9 +949,10 @@ mod tests {
id: WorkspaceId(2),
location: LocalPaths::new(["/tmp"]).into(),
center_group: Default::default(),
window_bounds: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
fullscreen: false,
centered_layout: false,
};
@@ -1082,9 +1049,10 @@ mod tests {
id: WorkspaceId(5),
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
center_group,
window_bounds: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
fullscreen: false,
centered_layout: false,
};
@@ -1111,9 +1079,10 @@ mod tests {
id: WorkspaceId(1),
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
window_bounds: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
fullscreen: false,
centered_layout: false,
};
@@ -1121,9 +1090,10 @@ mod tests {
id: WorkspaceId(2),
location: LocalPaths::new(["/tmp"]).into(),
center_group: Default::default(),
window_bounds: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
fullscreen: false,
centered_layout: false,
};
@@ -1158,9 +1128,10 @@ mod tests {
id: WorkspaceId(3),
location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
window_bounds: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
fullscreen: false,
centered_layout: false,
};
@@ -1192,9 +1163,10 @@ mod tests {
id: WorkspaceId(4),
location: LocalPaths::new(workspace_id).into(),
center_group: center_group.clone(),
window_bounds: Default::default(),
bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
fullscreen: false,
centered_layout: false,
}
}

View File

@@ -1,4 +1,4 @@
use super::{SerializedAxis, SerializedWindowBounds};
use super::SerializedAxis;
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
use anyhow::{Context, Result};
use async_recursion::async_recursion;
@@ -7,7 +7,7 @@ use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{AsyncWindowContext, Model, Task, View, WeakView};
use gpui::{AsyncWindowContext, Bounds, DevicePixels, Model, Task, View, WeakView};
use project::Project;
use serde::{Deserialize, Serialize};
use std::{
@@ -110,7 +110,8 @@ pub(crate) struct SerializedWorkspace {
pub(crate) id: WorkspaceId,
pub(crate) location: SerializedWorkspaceLocation,
pub(crate) center_group: SerializedPaneGroup,
pub(crate) window_bounds: Option<SerializedWindowBounds>,
pub(crate) bounds: Option<Bounds<DevicePixels>>,
pub(crate) fullscreen: bool,
pub(crate) centered_layout: bool,
pub(crate) display: Option<Uuid>,
pub(crate) docks: DockStructure,

View File

@@ -32,7 +32,7 @@ use gpui::{
ElementId, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
GlobalElementId, KeyContext, Keystroke, LayoutId, ManagedView, Model, ModelContext,
PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView,
WindowBounds, WindowHandle, WindowOptions,
WindowHandle, WindowOptions,
};
use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@@ -46,7 +46,7 @@ use node_runtime::NodeRuntime;
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
pub use pane::*;
pub use pane_group::*;
use persistence::{model::SerializedWorkspace, SerializedWindowBounds, DB};
use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
pub use persistence::{
model::{ItemId, LocalPaths, SerializedDevServerProject, SerializedWorkspaceLocation},
WorkspaceDb, DB as WORKSPACE_DB,
@@ -130,6 +130,7 @@ actions!(
NewCenterTerminal,
NewSearch,
Feedback,
Restart,
Welcome,
ToggleZoom,
ToggleLeftDock,
@@ -184,11 +185,6 @@ pub struct CloseInactiveTabsAndPanes {
#[derive(Clone, Deserialize, PartialEq)]
pub struct SendKeystrokes(pub String);
#[derive(Clone, Deserialize, PartialEq, Default)]
pub struct Restart {
pub binary_path: Option<PathBuf>,
}
impl_actions!(
workspace,
[
@@ -198,7 +194,6 @@ impl_actions!(
CloseInactiveTabsAndPanes,
NewFileInDirection,
OpenTerminal,
Restart,
Save,
SaveAll,
SwapPaneInDirection,
@@ -790,15 +785,29 @@ impl Workspace {
.await;
this.update(&mut cx, |this, cx| {
if let Some(display) = cx.display() {
let window_bounds = cx.window_bounds();
let fullscreen = cx.is_fullscreen();
if let Some(display_uuid) = display.uuid().log_err() {
let window_bounds = cx.window_bounds();
cx.background_executor()
.spawn(DB.set_window_open_status(
workspace_id,
SerializedWindowBounds(window_bounds),
display_uuid,
))
.detach_and_log_err(cx);
// Only update the window bounds when not full screen,
// so we can remember the last non-fullscreen bounds
// across restarts
if fullscreen {
cx.background_executor()
.spawn(DB.set_fullscreen(workspace_id, true))
.detach_and_log_err(cx);
} else if !cx.is_minimized() {
cx.background_executor()
.spawn(DB.set_fullscreen(workspace_id, false))
.detach_and_log_err(cx);
cx.background_executor()
.spawn(DB.set_window_bounds(
workspace_id,
SerializedWindowsBounds(window_bounds),
display_uuid,
))
.detach_and_log_err(cx);
}
}
}
this.bounds_save_task_queued.take();
@@ -938,27 +947,30 @@ impl Workspace {
} else {
let window_bounds_override = window_bounds_env_override();
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(WindowBounds::Windowed(bounds)), None)
let (bounds, display, fullscreen) = if let Some(bounds) = window_bounds_override {
(Some(bounds), None, false)
} else {
let restorable_bounds = serialized_workspace
.as_ref()
.and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
.and_then(|workspace| {
Some((workspace.display?, workspace.bounds?, workspace.fullscreen))
})
.or_else(|| {
let (display, window_bounds) = DB.last_window().log_err()?;
Some((display?, window_bounds?))
let (display, bounds, fullscreen) = DB.last_window().log_err()?;
Some((display?, bounds?.0, fullscreen.unwrap_or(false)))
});
if let Some((serialized_display, serialized_status)) = restorable_bounds {
(Some(serialized_status.0), Some(serialized_display))
if let Some((serialized_display, bounds, fullscreen)) = restorable_bounds {
(Some(bounds), Some(serialized_display), fullscreen)
} else {
(None, None)
(None, None, false)
}
};
// Use the serialized workspace to construct the new window
let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
options.window_bounds = window_bounds;
options.bounds = bounds;
options.fullscreen = fullscreen;
let centered_layout = serialized_workspace
.as_ref()
.map(|w| w.centered_layout)
@@ -3655,14 +3667,14 @@ impl Workspace {
if let Some(location) = location {
let center_group = build_serialized_pane_group(&self.center.root, cx);
let docks = build_serialized_docks(self, cx);
let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
let serialized_workspace = SerializedWorkspace {
id: self.database_id,
location,
center_group,
window_bounds,
bounds: Default::default(),
display: Default::default(),
docks,
fullscreen: cx.is_fullscreen(),
centered_layout: self.centered_layout,
};
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
@@ -4855,8 +4867,7 @@ pub fn join_hosted_project(
let window_bounds_override = window_bounds_env_override();
cx.update(|cx| {
let mut options = (app_state.build_window_options)(None, cx);
options.window_bounds =
window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
options.bounds = window_bounds_override;
cx.open_window(options, |cx| {
cx.new_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
@@ -4920,8 +4931,7 @@ pub fn join_dev_server_project(
let window_bounds_override = window_bounds_env_override();
cx.update(|cx| {
let mut options = (app_state.build_window_options)(None, cx);
options.window_bounds =
window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
options.bounds = window_bounds_override;
cx.open_window(options, |cx| {
cx.new_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
@@ -4983,8 +4993,7 @@ pub fn join_in_room_project(
let window_bounds_override = window_bounds_env_override();
cx.update(|cx| {
let mut options = (app_state.build_window_options)(None, cx);
options.window_bounds =
window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
options.bounds = window_bounds_override;
cx.open_window(options, |cx| {
cx.new_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
@@ -5025,7 +5034,7 @@ pub fn join_in_room_project(
})
}
pub fn restart(restart: &Restart, cx: &mut AppContext) {
pub fn restart(_: &Restart, cx: &mut AppContext) {
let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
let mut workspace_windows = cx
.windows()
@@ -5051,7 +5060,6 @@ pub fn restart(restart: &Restart, cx: &mut AppContext) {
.ok();
}
let binary_path = restart.binary_path.clone();
cx.spawn(|mut cx| async move {
if let Some(prompt) = prompt {
let answer = prompt.await?;
@@ -5071,7 +5079,7 @@ pub fn restart(restart: &Restart, cx: &mut AppContext) {
}
}
cx.update(|cx| cx.restart(binary_path))
cx.update(|cx| cx.restart())
})
.detach_and_log_err(cx);
}

Some files were not shown because too many files have changed in this diff Show More