Compare commits
1 Commits
v0.156.0
...
proto_refo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a2665b940 |
@@ -12,6 +12,9 @@
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"proto": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2,
|
||||
"preferred_line_length": 100,
|
||||
|
||||
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -10500,20 +10500,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snippets_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"paths",
|
||||
"picker",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.10"
|
||||
@@ -14482,7 +14468,6 @@ dependencies = [
|
||||
"simplelog",
|
||||
"smol",
|
||||
"snippet_provider",
|
||||
"snippets_ui",
|
||||
"supermaven",
|
||||
"sysinfo",
|
||||
"tab_switcher",
|
||||
|
||||
@@ -99,7 +99,6 @@ members = [
|
||||
"crates/settings_ui",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/story",
|
||||
@@ -276,7 +275,6 @@ settings = { path = "crates/settings" }
|
||||
settings_ui = { path = "crates/settings_ui" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
snippet_provider = { path = "crates/snippet_provider" }
|
||||
snippets_ui = { path = "crates/snippets_ui" }
|
||||
sqlez = { path = "crates/sqlez" }
|
||||
sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
story = { path = "crates/story" }
|
||||
|
||||
@@ -50,9 +50,6 @@ And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
{{#if diagnostic_errors}}
|
||||
{{#each diagnostic_errors}}
|
||||
|
||||
Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to.
|
||||
|
||||
<diagnostic_error>
|
||||
<line_number>{{line_number}}</line_number>
|
||||
<error_message>{{error_message}}</error_message>
|
||||
|
||||
@@ -356,19 +356,9 @@
|
||||
/// Scrollbar-related settings
|
||||
"scrollbar": {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
/// This setting can take four values:
|
||||
///
|
||||
/// 1. null (default): Inherit editor settings
|
||||
/// 2. Show the scrollbar if there's important information or
|
||||
/// follow the system's configured behavior (default):
|
||||
/// "auto"
|
||||
/// 3. Match the system's configured behavior:
|
||||
/// "system"
|
||||
/// 4. Always show the scrollbar:
|
||||
/// "always"
|
||||
/// 5. Never show the scrollbar:
|
||||
/// "never"
|
||||
"show": null
|
||||
/// Default: always
|
||||
"show": "always"
|
||||
}
|
||||
},
|
||||
"outline_panel": {
|
||||
|
||||
@@ -31,11 +31,11 @@ impl SlashCommand for AutoCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Automatically infer what context to add".into()
|
||||
"Automatically infer what context to add, based on your prompt".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Automatically Infer Context".into()
|
||||
}
|
||||
|
||||
fn label(&self, cx: &AppContext) -> CodeLabel {
|
||||
|
||||
@@ -19,11 +19,11 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Re-insert changed files".into()
|
||||
"re-insert changed files".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Re-insert Changed Files".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -95,7 +95,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert Diagnostics".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -104,11 +104,11 @@ impl SlashCommand for FetchSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert fetched URL contents".into()
|
||||
"insert URL contents".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert fetched URL contents".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -110,11 +110,11 @@ impl SlashCommand for FileSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert file".into()
|
||||
"insert file".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert File".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -19,11 +19,11 @@ impl SlashCommand for NowSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert current date and time".into()
|
||||
"insert the current date and time".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert Current Date and Time".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -47,11 +47,11 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Generate a semantic search based on context".into()
|
||||
"Generate semantic searches based on the current context".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Project Context".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -16,11 +16,11 @@ impl SlashCommand for PromptSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert prompt from library".into()
|
||||
"insert prompt from library".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert Prompt from Library".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -34,11 +34,11 @@ impl SlashCommand for SearchSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Search your project semantically".into()
|
||||
"semantic search".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Semantic Search".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -17,11 +17,11 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert symbols for active tab".into()
|
||||
"insert symbols for active tab".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert Symbols for Active Tab".into()
|
||||
}
|
||||
|
||||
fn complete_argument(
|
||||
|
||||
@@ -24,11 +24,11 @@ impl SlashCommand for TabSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert open tabs (active tab by default)".to_owned()
|
||||
"insert open tabs (active tab by default)".to_owned()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert Open Tabs".to_owned()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -29,11 +29,11 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert terminal output".into()
|
||||
"insert terminal output".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert Terminal Output".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -29,11 +29,11 @@ impl SlashCommand for WorkflowSlashCommand {
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Insert prompt to opt into the edit workflow".into()
|
||||
"insert a prompt that opts into the edit workflow".into()
|
||||
}
|
||||
|
||||
fn menu_text(&self) -> String {
|
||||
self.description()
|
||||
"Insert Workflow Prompt".into()
|
||||
}
|
||||
|
||||
fn requires_argument(&self) -> bool {
|
||||
|
||||
@@ -184,7 +184,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
h_flex()
|
||||
.group(format!("command-entry-label-{ix}"))
|
||||
.w_full()
|
||||
.min_w(px(250.))
|
||||
.min_w(px(220.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
@@ -203,9 +203,7 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
div()
|
||||
.font_buffer(cx)
|
||||
.child(
|
||||
Label::new(args)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
Label::new(args).size(LabelSize::Small),
|
||||
)
|
||||
.visible_on_hover(format!(
|
||||
"command-entry-label-{ix}"
|
||||
|
||||
@@ -379,75 +379,51 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().finish_waiting();
|
||||
|
||||
// Open the buffer on the host.
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "fn main() { a. }")
|
||||
});
|
||||
|
||||
// Confirm a completion on the guest.
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
|
||||
assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
|
||||
});
|
||||
|
||||
// Return a resolved completion from the host's language server.
|
||||
// The resolved completion has an additional text edit.
|
||||
fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
|
||||
|params, _| async move {
|
||||
Ok(match params.label.as_str() {
|
||||
"first_method(…)" => lsp::CompletionItem {
|
||||
label: "first_method(…)".into(),
|
||||
detail: Some("fn(&mut self, B) -> C".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "first_method($1)".to_string(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 14),
|
||||
lsp::Position::new(0, 14),
|
||||
),
|
||||
})),
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
new_text: "use d::SomeTrait;\n".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
}]),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
..Default::default()
|
||||
},
|
||||
"second_method(…)" => lsp::CompletionItem {
|
||||
label: "second_method(…)".into(),
|
||||
detail: Some("fn(&mut self, C) -> D<E>".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "second_method()".to_string(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 14),
|
||||
lsp::Position::new(0, 14),
|
||||
),
|
||||
})),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
new_text: "use d::SomeTrait;\n".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
}]),
|
||||
..Default::default()
|
||||
},
|
||||
_ => panic!("unexpected completion label: {:?}", params.label),
|
||||
assert_eq!(params.label, "first_method(…)");
|
||||
Ok(lsp::CompletionItem {
|
||||
label: "first_method(…)".into(),
|
||||
detail: Some("fn(&mut self, B) -> C".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "first_method($1)".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
|
||||
})),
|
||||
additional_text_edits: Some(vec![lsp::TextEdit {
|
||||
new_text: "use d::SomeTrait;\n".to_string(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
}]),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
..Default::default()
|
||||
})
|
||||
},
|
||||
);
|
||||
cx_a.executor().finish_waiting();
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
// Confirm a completion on the guest.
|
||||
editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// The additional edit is applied.
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
@@ -540,15 +516,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// When accepting the completion, the snippet is insert.
|
||||
editor_b
|
||||
.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
|
||||
|
||||
@@ -4,7 +4,7 @@ use fs::{FakeFs, Fs as _};
|
||||
use gpui::{Context as _, TestAppContext};
|
||||
use language::language_settings::all_language_settings;
|
||||
use project::ProjectPath;
|
||||
use remote::SshRemoteClient;
|
||||
use remote::SshSession;
|
||||
use remote_server::HeadlessProject;
|
||||
use serde_json::json;
|
||||
use std::{path::Path, sync::Arc};
|
||||
@@ -24,7 +24,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (client_ssh, server_ssh) = SshRemoteClient::fake(cx_a, server_cx);
|
||||
let (client_ssh, server_ssh) = SshSession::fake(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
|
||||
@@ -25,7 +25,7 @@ use node_runtime::NodeRuntime;
|
||||
use notifications::NotificationStore;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use remote::SshRemoteClient;
|
||||
use remote::SshSession;
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
@@ -835,7 +835,7 @@ impl TestClient {
|
||||
pub async fn build_ssh_project(
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Arc<SshRemoteClient>,
|
||||
ssh: Arc<SshSession>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Model<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
|
||||
@@ -363,12 +363,10 @@ mod tests {
|
||||
|
||||
// Confirming a completion inserts it and hides the context menu, without showing
|
||||
// the copilot suggestion afterwards.
|
||||
editor.confirm_completion(&Default::default(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor
|
||||
.confirm_completion(&Default::default(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
assert!(!editor.context_menu_visible());
|
||||
assert!(!editor.has_active_inline_completion(cx));
|
||||
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
|
||||
|
||||
@@ -193,7 +193,6 @@ gpui::actions!(
|
||||
AcceptPartialInlineCompletion,
|
||||
AddSelectionAbove,
|
||||
AddSelectionBelow,
|
||||
ApplyDiffHunk,
|
||||
Backspace,
|
||||
Cancel,
|
||||
CancelLanguageServerWork,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{ops::ControlFlow, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use gpui::{Task, ViewContext};
|
||||
@@ -7,7 +7,7 @@ use crate::Editor;
|
||||
|
||||
pub struct DebouncedDelay {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<ControlFlow<()>>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl DebouncedDelay {
|
||||
@@ -23,22 +23,17 @@ impl DebouncedDelay {
|
||||
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
|
||||
{
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
channel.send(ControlFlow::Break(())).ok();
|
||||
_ = channel.send(());
|
||||
}
|
||||
|
||||
let (sender, mut receiver) = oneshot::channel::<ControlFlow<()>>();
|
||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||
self.cancel_channel = Some(sender);
|
||||
|
||||
drop(self.task.take());
|
||||
self.task = Some(cx.spawn(move |model, mut cx| async move {
|
||||
let mut timer = cx.background_executor().timer(delay).fuse();
|
||||
futures::select_biased! {
|
||||
interrupt = receiver => {
|
||||
match interrupt {
|
||||
Ok(ControlFlow::Break(())) | Err(_) => return,
|
||||
Ok(ControlFlow::Continue(())) => {},
|
||||
}
|
||||
}
|
||||
_ = receiver => return,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
@@ -47,11 +42,4 @@ impl DebouncedDelay {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn start_now(&mut self) -> Option<Task<()>> {
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
channel.send(ControlFlow::Continue(())).ok();
|
||||
}
|
||||
self.task.take()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ use debounced_delay::DebouncedDelay;
|
||||
use display_map::*;
|
||||
pub use display_map::{DisplayPoint, FoldPlaceholder};
|
||||
pub use editor_settings::{
|
||||
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
|
||||
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings,
|
||||
};
|
||||
pub use editor_settings_controls::*;
|
||||
use element::LineWithInvisibles;
|
||||
@@ -3059,7 +3059,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
if self.clear_expanded_diff_hunks(cx) {
|
||||
if self.clear_clicked_diff_hunks(cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -4422,49 +4422,16 @@ impl Editor {
|
||||
&mut self,
|
||||
item_ix: Option<usize>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<anyhow::Result<()>>> {
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
||||
menu
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut resolve_task_store = completions_menu
|
||||
.selected_completion_documentation_resolve_debounce
|
||||
.lock();
|
||||
let selected_completion_resolve = resolve_task_store.start_now();
|
||||
let menu_pre_resolve = self
|
||||
.completion_documentation_pre_resolve_debounce
|
||||
.start_now();
|
||||
drop(resolve_task_store);
|
||||
|
||||
Some(cx.spawn(|editor, mut cx| async move {
|
||||
match (selected_completion_resolve, menu_pre_resolve) {
|
||||
(None, None) => {}
|
||||
(Some(resolve), None) | (None, Some(resolve)) => resolve.await,
|
||||
(Some(resolve_1), Some(resolve_2)) => {
|
||||
futures::join!(resolve_1, resolve_2);
|
||||
}
|
||||
}
|
||||
if let Some(apply_edits_task) = editor.update(&mut cx, |editor, cx| {
|
||||
editor.apply_resolved_completion(completions_menu, item_ix, intent, cx)
|
||||
})? {
|
||||
apply_edits_task.await?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn apply_resolved_completion(
|
||||
&mut self,
|
||||
completions_menu: CompletionsMenu,
|
||||
item_ix: Option<usize>,
|
||||
intent: CompletionIntent,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<Task<anyhow::Result<Option<language::Transaction>>>> {
|
||||
use language::ToOffset as _;
|
||||
|
||||
let mat = completions_menu
|
||||
.matches
|
||||
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
||||
@@ -4623,7 +4590,11 @@ impl Editor {
|
||||
// so we should automatically call signature_help
|
||||
self.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
Some(apply_edits)
|
||||
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
apply_edits.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
|
||||
@@ -6234,20 +6205,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_selected_diff_hunks(&mut self, _: &ApplyDiffHunk, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let hunks = hunks_for_selections(&snapshot, &self.selections.disjoint_anchors());
|
||||
self.transact(cx, |editor, cx| {
|
||||
for hunk in hunks {
|
||||
if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_into_base(Some(hunk.buffer_range.to_offset(buffer)), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_active_item_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
|
||||
if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| {
|
||||
let project_path = buffer.read(cx).project_path(cx)?;
|
||||
@@ -11286,32 +11243,30 @@ impl Editor {
|
||||
None
|
||||
}
|
||||
|
||||
fn target_file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn language::LocalFile> {
|
||||
self.active_excerpt(cx)?
|
||||
.1
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|f| f.as_local())
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
|
||||
if let Some(target) = self.target_file(cx) {
|
||||
cx.reveal_path(&target.abs_path(cx));
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
cx.reveal_path(&file.abs_path(cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
|
||||
if let Some(file) = self.target_file(cx) {
|
||||
if let Some(path) = file.abs_path(cx).to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(path) = file.abs_path(cx).to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
|
||||
if let Some(file) = self.target_file(cx) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(path.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11522,10 +11477,12 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext<Self>) {
|
||||
if let Some(file) = self.target_file(cx) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
let selection = self.selections.newest::<Point>(cx).start.row + 1;
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
if let Some(path) = file.path().to_str() {
|
||||
let selection = self.selections.newest::<Point>(cx).start.row + 1;
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12286,9 +12243,12 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let mut new_selections_by_buffer = HashMap::default();
|
||||
for selection in self.selections.all::<usize>(cx) {
|
||||
for (buffer, range, _) in
|
||||
for (buffer, mut range, _) in
|
||||
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
|
||||
{
|
||||
if selection.reversed {
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
}
|
||||
let mut range = range.to_point(buffer.read(cx));
|
||||
range.start.column = 0;
|
||||
range.end.column = buffer.read(cx).line_len(range.end.row);
|
||||
|
||||
@@ -7996,7 +7996,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.ˇ
|
||||
one.second_completionˇ
|
||||
two
|
||||
three
|
||||
"});
|
||||
@@ -8029,9 +8029,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completionˇ
|
||||
two
|
||||
thoverlapping additional editree
|
||||
|
||||
additional edit"});
|
||||
three
|
||||
additional edit
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
one.second_completion
|
||||
@@ -8091,8 +8091,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
two siˇ
|
||||
three siˇ
|
||||
two sixth_completionˇ
|
||||
three sixth_completionˇ
|
||||
additional edit
|
||||
"});
|
||||
|
||||
@@ -8133,11 +8133,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state("editor.cloˇ");
|
||||
cx.assert_editor_state("editor.closeˇ");
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
editor.closeˇ"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -10142,7 +10140,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
|
||||
cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let task_completion_item = completion_item.clone();
|
||||
|
||||
@@ -436,7 +436,6 @@ impl EditorElement {
|
||||
register_action(view, cx, Editor::accept_inline_completion);
|
||||
register_action(view, cx, Editor::revert_file);
|
||||
register_action(view, cx, Editor::revert_selected_hunks);
|
||||
register_action(view, cx, Editor::apply_selected_diff_hunks);
|
||||
register_action(view, cx, Editor::open_active_item_in_terminal)
|
||||
}
|
||||
|
||||
|
||||
@@ -821,7 +821,7 @@ mod tests {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(false),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -913,15 +913,12 @@ mod tests {
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
|
||||
//apply a completion and check it was successfully applied
|
||||
let () = cx
|
||||
.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&Default::default(), cx);
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _apply_additional_edits = cx.update_editor(|editor, cx| {
|
||||
editor.context_menu_next(&Default::default(), cx);
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completionˇ
|
||||
two
|
||||
|
||||
@@ -14,9 +14,9 @@ use ui::{
|
||||
use util::RangeExt;
|
||||
|
||||
use crate::{
|
||||
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
|
||||
BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
|
||||
DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
|
||||
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, BlockDisposition,
|
||||
BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow, DisplaySnapshot,
|
||||
Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
|
||||
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
|
||||
};
|
||||
|
||||
@@ -32,7 +32,6 @@ pub(super) struct ExpandedHunks {
|
||||
pub(crate) hunks: Vec<ExpandedHunk>,
|
||||
diff_base: HashMap<BufferId, DiffBaseBuffer>,
|
||||
hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
|
||||
expand_all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -73,10 +72,6 @@ impl ExpandedHunks {
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn set_expand_all_diff_hunks(&mut self) {
|
||||
self.expanded_hunks.expand_all = true;
|
||||
}
|
||||
|
||||
pub(super) fn toggle_hovered_hunk(
|
||||
&mut self,
|
||||
hovered_hunk: &HoveredHunk,
|
||||
@@ -138,10 +133,6 @@ impl Editor {
|
||||
hunks_to_toggle: Vec<MultiBufferDiffHunk>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.expanded_hunks.expand_all {
|
||||
return;
|
||||
}
|
||||
|
||||
let previous_toggle_task = self.expanded_hunks.hunk_update_tasks.remove(&None);
|
||||
let new_toggle_task = cx.spawn(move |editor, mut cx| async move {
|
||||
if let Some(task) = previous_toggle_task {
|
||||
@@ -247,14 +238,19 @@ impl Editor {
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<()> {
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let hunk_range = hunk.multi_buffer_range.clone();
|
||||
let hunk_point_range = hunk_range.to_point(&multi_buffer_snapshot);
|
||||
let multi_buffer_row_range = hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
.to_point(&multi_buffer_snapshot)
|
||||
..hunk.multi_buffer_range.end.to_point(&multi_buffer_snapshot);
|
||||
let hunk_start = hunk.multi_buffer_range.start;
|
||||
let hunk_end = hunk.multi_buffer_range.end;
|
||||
|
||||
let buffer = self.buffer().clone();
|
||||
let snapshot = self.snapshot(cx);
|
||||
let (diff_base_buffer, deleted_text_lines) = buffer.update(cx, |buffer, cx| {
|
||||
let hunk = buffer_diff_hunk(&snapshot.buffer_snapshot, hunk_point_range.clone())?;
|
||||
let mut buffer_ranges = buffer.range_to_buffer_ranges(hunk_point_range, cx);
|
||||
let hunk = buffer_diff_hunk(&snapshot.buffer_snapshot, multi_buffer_row_range.clone())?;
|
||||
let mut buffer_ranges = buffer.range_to_buffer_ranges(multi_buffer_row_range, cx);
|
||||
if buffer_ranges.len() == 1 {
|
||||
let (buffer, _, _) = buffer_ranges.pop()?;
|
||||
let diff_base_buffer = diff_base_buffer
|
||||
@@ -279,7 +275,7 @@ impl Editor {
|
||||
probe
|
||||
.hunk_range
|
||||
.start
|
||||
.cmp(&hunk_range.start, &multi_buffer_snapshot)
|
||||
.cmp(&hunk_start, &multi_buffer_snapshot)
|
||||
}) {
|
||||
Ok(_already_present) => return None,
|
||||
Err(ix) => ix,
|
||||
@@ -299,7 +295,7 @@ impl Editor {
|
||||
}
|
||||
DiffHunkStatus::Added => {
|
||||
self.highlight_rows::<DiffRowHighlight>(
|
||||
hunk_range.clone(),
|
||||
hunk_start..hunk_end,
|
||||
added_hunk_color(cx),
|
||||
false,
|
||||
cx,
|
||||
@@ -308,7 +304,7 @@ impl Editor {
|
||||
}
|
||||
DiffHunkStatus::Modified => {
|
||||
self.highlight_rows::<DiffRowHighlight>(
|
||||
hunk_range.clone(),
|
||||
hunk_start..hunk_end,
|
||||
added_hunk_color(cx),
|
||||
false,
|
||||
cx,
|
||||
@@ -327,7 +323,7 @@ impl Editor {
|
||||
block_insert_index,
|
||||
ExpandedHunk {
|
||||
blocks,
|
||||
hunk_range,
|
||||
hunk_range: hunk_start..hunk_end,
|
||||
status: hunk.status,
|
||||
folded: false,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
@@ -337,47 +333,11 @@ impl Editor {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn apply_changes_in_range(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> Option<()> {
|
||||
let (buffer, range, _) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.range_to_buffer_ranges(range, cx)
|
||||
.into_iter()
|
||||
.next()?;
|
||||
|
||||
buffer.update(cx, |branch_buffer, cx| {
|
||||
branch_buffer.merge_into_base(Some(range), cx);
|
||||
});
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
|
||||
let buffers = self.buffer.read(cx).all_buffers();
|
||||
for branch_buffer in buffers {
|
||||
branch_buffer.update(cx, |branch_buffer, cx| {
|
||||
branch_buffer.merge_into_base(None, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn hunk_header_block(
|
||||
&self,
|
||||
hunk: &HoveredHunk,
|
||||
cx: &mut ViewContext<'_, Editor>,
|
||||
) -> BlockProperties<Anchor> {
|
||||
let is_branch_buffer = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.point_to_buffer_offset(hunk.multi_buffer_range.start, cx)
|
||||
.map_or(false, |(buffer, _, _)| {
|
||||
buffer.read(cx).diff_base_buffer().is_some()
|
||||
});
|
||||
|
||||
let border_color = cx.theme().colors().border_variant;
|
||||
let gutter_color = match hunk.status {
|
||||
DiffHunkStatus::Added => cx.theme().status().created,
|
||||
@@ -428,71 +388,131 @@ impl Editor {
|
||||
.pr_6()
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.border_t_1()
|
||||
.pl_6()
|
||||
.pr_6()
|
||||
.border_color(border_color)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(!is_branch_buffer, |row| {
|
||||
row.child(
|
||||
IconButton::new("next-hunk", IconName::ArrowDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Next Hunk",
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_subsequent_hunk(
|
||||
hunk.multi_buffer_range.end,
|
||||
.child(
|
||||
IconButton::new("next-hunk", IconName::ArrowDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Next Hunk",
|
||||
&GoToHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let position = hunk
|
||||
.multi_buffer_range
|
||||
.end
|
||||
.to_point(
|
||||
&snapshot.buffer_snapshot,
|
||||
);
|
||||
if let Some(hunk) = editor
|
||||
.go_to_hunk_after_position(
|
||||
&snapshot, position, cx,
|
||||
)
|
||||
{
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(
|
||||
hunk.row_range.start.0,
|
||||
0,
|
||||
));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(
|
||||
hunk.row_range.end.0,
|
||||
0,
|
||||
));
|
||||
editor.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range:
|
||||
multi_buffer_start
|
||||
..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk
|
||||
.diff_base_byte_range,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("prev-hunk", IconName::ArrowUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPrevHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.go_to_preceding_hunk(
|
||||
hunk.multi_buffer_range.start,
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("prev-hunk", IconName::ArrowUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle = editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Previous Hunk",
|
||||
&GoToPrevHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let position = hunk
|
||||
.multi_buffer_range
|
||||
.start
|
||||
.to_point(
|
||||
&snapshot.buffer_snapshot,
|
||||
);
|
||||
let hunk = editor
|
||||
.go_to_hunk_before_position(
|
||||
&snapshot, position, cx,
|
||||
);
|
||||
if let Some(hunk) = hunk {
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(
|
||||
hunk.row_range.start.0,
|
||||
0,
|
||||
));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(
|
||||
hunk.row_range.end.0,
|
||||
0,
|
||||
));
|
||||
editor.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range:
|
||||
multi_buffer_start
|
||||
..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk
|
||||
.diff_base_byte_range,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("discard", IconName::Undo)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -538,90 +558,46 @@ impl Editor {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if is_branch_buffer {
|
||||
this.child(
|
||||
IconButton::new("apply", IconName::Check)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
let focus_handle =
|
||||
editor.focus_handle(cx);
|
||||
move |cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Apply Hunk",
|
||||
&ApplyDiffHunk,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let hunk = hunk.clone();
|
||||
move |_event, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.apply_changes_in_range(
|
||||
hunk.multi_buffer_range
|
||||
.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
.child({
|
||||
let focus = editor.focus_handle(cx);
|
||||
PopoverMenu::new("hunk-controls-dropdown")
|
||||
.trigger(
|
||||
IconButton::new(
|
||||
"toggle_editor_selections_icon",
|
||||
IconName::EllipsisVertical,
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected(
|
||||
hunk_controls_menu_handle.is_deployed(),
|
||||
)
|
||||
.when(
|
||||
!hunk_controls_menu_handle.is_deployed(),
|
||||
|this| {
|
||||
this.tooltip(|cx| {
|
||||
Tooltip::text("Hunk Controls", cx)
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child({
|
||||
let focus = editor.focus_handle(cx);
|
||||
PopoverMenu::new("hunk-controls-dropdown")
|
||||
.trigger(
|
||||
IconButton::new(
|
||||
"toggle_editor_selections_icon",
|
||||
IconName::EllipsisVertical,
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.with_handle(hunk_controls_menu_handle)
|
||||
.menu(move |cx| {
|
||||
let focus = focus.clone();
|
||||
let menu =
|
||||
ContextMenu::build(cx, move |menu, _| {
|
||||
menu.context(focus.clone()).action(
|
||||
"Discard All",
|
||||
RevertFile.boxed_clone(),
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected(
|
||||
hunk_controls_menu_handle
|
||||
.is_deployed(),
|
||||
)
|
||||
.when(
|
||||
!hunk_controls_menu_handle
|
||||
.is_deployed(),
|
||||
|this| {
|
||||
this.tooltip(|cx| {
|
||||
Tooltip::text(
|
||||
"Hunk Controls",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.with_handle(hunk_controls_menu_handle)
|
||||
.menu(move |cx| {
|
||||
let focus = focus.clone();
|
||||
let menu = ContextMenu::build(
|
||||
cx,
|
||||
move |menu, _| {
|
||||
menu.context(focus.clone())
|
||||
.action(
|
||||
"Discard All",
|
||||
RevertFile
|
||||
.boxed_clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
Some(menu)
|
||||
})
|
||||
});
|
||||
Some(menu)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(!is_branch_buffer, |div| {
|
||||
div.child(
|
||||
.child(
|
||||
div().child(
|
||||
IconButton::new("collapse", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -645,8 +621,8 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -721,10 +697,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
|
||||
if self.expanded_hunks.expand_all {
|
||||
return false;
|
||||
}
|
||||
pub(super) fn clear_clicked_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
|
||||
self.expanded_hunks.hunk_update_tasks.clear();
|
||||
self.clear_row_highlights::<DiffRowHighlight>();
|
||||
let to_remove = self
|
||||
@@ -828,43 +801,33 @@ impl Editor {
|
||||
status,
|
||||
} => {
|
||||
let hunk_display_range = display_row_range;
|
||||
|
||||
if expanded_hunk_display_range.start
|
||||
> hunk_display_range.end
|
||||
{
|
||||
recalculated_hunks.next();
|
||||
if editor.expanded_hunks.expand_all {
|
||||
continue;
|
||||
} else if expanded_hunk_display_range.end
|
||||
< hunk_display_range.start
|
||||
{
|
||||
break;
|
||||
} else {
|
||||
if !expanded_hunk.folded
|
||||
&& expanded_hunk_display_range == hunk_display_range
|
||||
&& expanded_hunk.status == hunk_status(buffer_hunk)
|
||||
&& expanded_hunk.diff_base_byte_range
|
||||
== buffer_hunk.diff_base_byte_range
|
||||
{
|
||||
recalculated_hunks.next();
|
||||
retain = true;
|
||||
} else {
|
||||
hunks_to_reexpand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if expanded_hunk_display_range.end
|
||||
< hunk_display_range.start
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if !expanded_hunk.folded
|
||||
&& expanded_hunk_display_range == hunk_display_range
|
||||
&& expanded_hunk.status == hunk_status(buffer_hunk)
|
||||
&& expanded_hunk.diff_base_byte_range
|
||||
== buffer_hunk.diff_base_byte_range
|
||||
{
|
||||
recalculated_hunks.next();
|
||||
retain = true;
|
||||
} else {
|
||||
hunks_to_reexpand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -876,26 +839,6 @@ impl Editor {
|
||||
retain
|
||||
});
|
||||
|
||||
if editor.expanded_hunks.expand_all {
|
||||
for hunk in recalculated_hunks {
|
||||
match diff_hunk_to_display(&hunk, &snapshot) {
|
||||
DisplayDiffHunk::Folded { .. } => {}
|
||||
DisplayDiffHunk::Unfolded {
|
||||
diff_base_byte_range,
|
||||
multi_buffer_range,
|
||||
status,
|
||||
..
|
||||
} => {
|
||||
hunks_to_reexpand.push(HoveredHunk {
|
||||
status,
|
||||
multi_buffer_range,
|
||||
diff_base_byte_range,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.remove_highlighted_rows::<DiffRowHighlight>(highlights_to_remove, cx);
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
|
||||
@@ -933,51 +876,6 @@ impl Editor {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn go_to_subsequent_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let position = position.to_point(&snapshot.buffer_snapshot);
|
||||
if let Some(hunk) = self.go_to_hunk_after_position(&snapshot, position, cx) {
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(hunk.row_range.start.0, 0));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(hunk.row_range.end.0, 0));
|
||||
self.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range: multi_buffer_start..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn go_to_preceding_hunk(&mut self, position: Anchor, cx: &mut ViewContext<Self>) {
|
||||
let snapshot = self.snapshot(cx);
|
||||
let position = position.to_point(&snapshot.buffer_snapshot);
|
||||
let hunk = self.go_to_hunk_before_position(&snapshot, position, cx);
|
||||
if let Some(hunk) = hunk {
|
||||
let multi_buffer_start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(hunk.row_range.start.0, 0));
|
||||
let multi_buffer_end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_after(Point::new(hunk.row_range.end.0, 0));
|
||||
self.expand_diff_hunk(
|
||||
None,
|
||||
&HoveredHunk {
|
||||
multi_buffer_range: multi_buffer_start..multi_buffer_end,
|
||||
status: hunk_status(&hunk),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_diff_hunk(
|
||||
@@ -1060,15 +958,13 @@ fn editor_with_deleted_text(
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
|
||||
enum DeletedBlockRowHighlight {}
|
||||
editor.highlight_rows::<DeletedBlockRowHighlight>(
|
||||
editor.highlight_rows::<DiffRowHighlight>(
|
||||
Anchor::min()..Anchor::max(),
|
||||
deleted_color,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); //
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor
|
||||
._subscriptions
|
||||
.extend([cx.on_blur(&editor.focus_handle, |editor, cx| {
|
||||
@@ -1077,41 +973,37 @@ fn editor_with_deleted_text(
|
||||
});
|
||||
})]);
|
||||
|
||||
let parent_editor_for_reverts = parent_editor.clone();
|
||||
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
|
||||
let diff_base_range = hunk.diff_base_byte_range.clone();
|
||||
editor
|
||||
.register_action::<RevertSelectedHunks>({
|
||||
let parent_editor = parent_editor.clone();
|
||||
move |_, cx| {
|
||||
parent_editor
|
||||
.update(cx, |editor, cx| {
|
||||
let Some((buffer, original_text)) =
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let (_, buffer, _) = buffer.excerpt_containing(
|
||||
original_multi_buffer_range.start,
|
||||
cx,
|
||||
)?;
|
||||
let original_text =
|
||||
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
|
||||
Some((buffer, Arc::from(original_text.to_string())))
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
Some((
|
||||
original_multi_buffer_range.start.text_anchor
|
||||
..original_multi_buffer_range.end.text_anchor,
|
||||
original_text,
|
||||
)),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
.register_action::<RevertSelectedHunks>(move |_, cx| {
|
||||
parent_editor_for_reverts
|
||||
.update(cx, |editor, cx| {
|
||||
let Some((buffer, original_text)) =
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let (_, buffer, _) = buffer
|
||||
.excerpt_containing(original_multi_buffer_range.start, cx)?;
|
||||
let original_text =
|
||||
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
|
||||
Some((buffer, Arc::from(original_text.to_string())))
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
Some((
|
||||
original_multi_buffer_range.start.text_anchor
|
||||
..original_multi_buffer_range.end.text_anchor,
|
||||
original_text,
|
||||
)),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
let hunk = hunk.clone();
|
||||
|
||||
@@ -158,12 +158,6 @@ pub fn deploy_context_menu(
|
||||
}
|
||||
|
||||
let focus = cx.focused();
|
||||
let has_reveal_target = editor.target_file(cx).is_some();
|
||||
let reveal_in_finder_label = if cfg!(target_os = "macos") {
|
||||
"Reveal in Finder"
|
||||
} else {
|
||||
"Reveal in File Manager"
|
||||
};
|
||||
ui::ContextMenu::build(cx, |menu, _cx| {
|
||||
let builder = menu
|
||||
.on_blur_subscription(Subscription::new(|| {}))
|
||||
@@ -186,13 +180,11 @@ pub fn deploy_context_menu(
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.separator()
|
||||
.map(|builder| {
|
||||
if has_reveal_target {
|
||||
builder.action(reveal_in_finder_label, Box::new(RevealInFileManager))
|
||||
} else {
|
||||
builder
|
||||
.disabled_action(reveal_in_finder_label, Box::new(RevealInFileManager))
|
||||
}
|
||||
.when(cfg!(target_os = "macos"), |builder| {
|
||||
builder.action("Reveal in Finder", Box::new(RevealInFileManager))
|
||||
})
|
||||
.when(cfg!(not(target_os = "macos")), |builder| {
|
||||
builder.action("Reveal in File Manager", Box::new(RevealInFileManager))
|
||||
})
|
||||
.action("Open in Terminal", Box::new(OpenInTerminal))
|
||||
.action("Copy Permalink", Box::new(CopyPermalinkToLine));
|
||||
|
||||
@@ -11,14 +11,14 @@ use text::ToOffset;
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
|
||||
ToolbarItemView, Workspace,
|
||||
ToolbarItemView,
|
||||
};
|
||||
|
||||
pub struct ProposedChangesEditor {
|
||||
editor: View<Editor>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_recalculate_diffs_task: Task<Option<()>>,
|
||||
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
|
||||
recalculate_diffs_tx: mpsc::UnboundedSender<Model<Buffer>>,
|
||||
}
|
||||
|
||||
pub struct ProposedChangesBuffer<T> {
|
||||
@@ -30,11 +30,6 @@ pub struct ProposedChangesEditorToolbar {
|
||||
current_editor: Option<View<ProposedChangesEditor>>,
|
||||
}
|
||||
|
||||
struct RecalculateDiff {
|
||||
buffer: Model<Buffer>,
|
||||
debounce: bool,
|
||||
}
|
||||
|
||||
impl ProposedChangesEditor {
|
||||
pub fn new<T: ToOffset>(
|
||||
buffers: Vec<ProposedChangesBuffer<T>>,
|
||||
@@ -63,26 +58,21 @@ impl ProposedChangesEditor {
|
||||
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
|
||||
|
||||
Self {
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
|
||||
editor.set_expand_all_diff_hunks();
|
||||
editor
|
||||
}),
|
||||
editor: cx
|
||||
.new_view(|cx| Editor::for_multibuffer(multibuffer.clone(), project, true, cx)),
|
||||
recalculate_diffs_tx,
|
||||
_recalculate_diffs_task: cx.spawn(|_, mut cx| async move {
|
||||
let mut buffers_to_diff = HashSet::default();
|
||||
while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
|
||||
buffers_to_diff.insert(recalculate_diff.buffer);
|
||||
while let Some(buffer) = recalculate_diffs_rx.next().await {
|
||||
buffers_to_diff.insert(buffer);
|
||||
|
||||
while recalculate_diff.debounce {
|
||||
loop {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(250))
|
||||
.await;
|
||||
let mut had_further_changes = false;
|
||||
while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
|
||||
let next_recalculate_diff = next_recalculate_diff?;
|
||||
recalculate_diff.debounce &= next_recalculate_diff.debounce;
|
||||
buffers_to_diff.insert(next_recalculate_diff.buffer);
|
||||
while let Ok(next_buffer) = recalculate_diffs_rx.try_next() {
|
||||
buffers_to_diff.insert(next_buffer?);
|
||||
had_further_changes = true;
|
||||
}
|
||||
if !had_further_changes {
|
||||
@@ -109,24 +99,19 @@ impl ProposedChangesEditor {
|
||||
event: &BufferEvent,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
BufferEvent::Operation { .. } => {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer,
|
||||
debounce: true,
|
||||
})
|
||||
.ok();
|
||||
if let BufferEvent::Edited = event {
|
||||
self.recalculate_diffs_tx.unbounded_send(buffer).ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_all_changes(&self, cx: &mut ViewContext<Self>) {
|
||||
let buffers = self.editor.read(cx).buffer.read(cx).all_buffers();
|
||||
for branch_buffer in buffers {
|
||||
if let Some(base_buffer) = branch_buffer.read(cx).diff_base_buffer() {
|
||||
base_buffer.update(cx, |base_buffer, cx| {
|
||||
base_buffer.merge(&branch_buffer, None, cx)
|
||||
});
|
||||
}
|
||||
BufferEvent::DiffBaseChanged => {
|
||||
self.recalculate_diffs_tx
|
||||
.unbounded_send(RecalculateDiff {
|
||||
buffer,
|
||||
debounce: false,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,31 +159,6 @@ impl Item for ProposedChangesEditor {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::added_to_workspace(editor, workspace, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, Item::deactivated);
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::navigate(editor, data, cx))
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: workspace::ItemNavHistory,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
Item::set_nav_history(editor, nav_history, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ProposedChangesEditorToolbar {
|
||||
@@ -223,9 +183,7 @@ impl Render for ProposedChangesEditorToolbar {
|
||||
Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
|
||||
if let Some(editor) = &editor {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.editor.update(cx, |editor, cx| {
|
||||
editor.apply_all_changes(cx);
|
||||
})
|
||||
editor.apply_all_changes(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use multi_buffer::{ExcerptRange, ToPoint};
|
||||
use parking_lot::RwLock;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{FakeFs, Project};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
|
||||
@@ -587,54 +587,38 @@ impl Fs for RealFs {
|
||||
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
|
||||
let root_path = path.to_path_buf();
|
||||
|
||||
// Check if root path is a symlink
|
||||
let target_path = self.read_link(&path).await.ok();
|
||||
|
||||
watcher::global({
|
||||
let target_path = target_path.clone();
|
||||
|g| {
|
||||
let tx = tx.clone();
|
||||
let pending_paths = pending_paths.clone();
|
||||
g.add(move |event: ¬ify::Event| {
|
||||
let kind = match event.kind {
|
||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
||||
_ => None,
|
||||
};
|
||||
let mut paths = event
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
if let Some(target) = target_path.clone() {
|
||||
if path.starts_with(target) {
|
||||
return Some(PathEvent {
|
||||
path: path.clone(),
|
||||
kind,
|
||||
});
|
||||
}
|
||||
} else if path.starts_with(&root_path) {
|
||||
return Some(PathEvent {
|
||||
path: path.clone(),
|
||||
kind,
|
||||
});
|
||||
}
|
||||
None
|
||||
watcher::global(|g| {
|
||||
let tx = tx.clone();
|
||||
let pending_paths = pending_paths.clone();
|
||||
g.add(move |event: ¬ify::Event| {
|
||||
let kind = match event.kind {
|
||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
||||
_ => None,
|
||||
};
|
||||
let mut paths = event
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|path| {
|
||||
path.starts_with(&root_path).then(|| PathEvent {
|
||||
path: path.clone(),
|
||||
kind,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !paths.is_empty() {
|
||||
paths.sort();
|
||||
let mut pending_paths = pending_paths.lock();
|
||||
if pending_paths.is_empty() {
|
||||
tx.try_send(()).ok();
|
||||
}
|
||||
util::extend_sorted(&mut *pending_paths, paths, usize::MAX, |a, b| {
|
||||
a.path.cmp(&b.path)
|
||||
});
|
||||
if !paths.is_empty() {
|
||||
paths.sort();
|
||||
let mut pending_paths = pending_paths.lock();
|
||||
if pending_paths.is_empty() {
|
||||
tx.try_send(()).ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
util::extend_sorted(&mut *pending_paths, paths, usize::MAX, |a, b| {
|
||||
a.path.cmp(&b.path)
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -642,14 +626,6 @@ impl Fs for RealFs {
|
||||
|
||||
watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
|
||||
|
||||
// Check if path is a symlink and follow the target parent
|
||||
if let Some(target) = target_path {
|
||||
watcher.add(&target).ok();
|
||||
if let Some(parent) = target.parent() {
|
||||
watcher.add(parent).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
// watch the parent dir so we can tell when settings.json is created
|
||||
if let Some(parent) = path.parent() {
|
||||
watcher.add(parent).log_err();
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder as _;
|
||||
|
||||
struct SubWindow {
|
||||
custom_titlebar: bool,
|
||||
}
|
||||
|
||||
fn button(text: &str, on_click: impl Fn(&mut WindowContext) + 'static) -> impl IntoElement {
|
||||
div()
|
||||
.id(SharedString::from(text.to_string()))
|
||||
.flex_none()
|
||||
.px_2()
|
||||
.bg(rgb(0xf7f7f7))
|
||||
.active(|this| this.opacity(0.85))
|
||||
.border_1()
|
||||
.border_color(rgb(0xe0e0e0))
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.child(text.to_string())
|
||||
.on_click(move |_, cx| on_click(cx))
|
||||
}
|
||||
|
||||
impl Render for SubWindow {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.bg(rgb(0xffffff))
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.when(self.custom_titlebar, |cx| {
|
||||
cx.child(
|
||||
div()
|
||||
.flex()
|
||||
.h(px(32.))
|
||||
.px_4()
|
||||
.bg(gpui::blue())
|
||||
.text_color(gpui::white())
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.child("Custom Titlebar"),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.p_8()
|
||||
.gap_2()
|
||||
.child("SubWindow")
|
||||
.child(button("Close", |cx| {
|
||||
cx.remove_window();
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct WindowDemo {}
|
||||
|
||||
impl Render for WindowDemo {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let window_bounds =
|
||||
WindowBounds::Windowed(Bounds::centered(None, size(px(300.0), px(300.0)), cx));
|
||||
|
||||
div()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.bg(rgb(0xffffff))
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(button("Normal", move |cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|_cx| SubWindow {
|
||||
custom_titlebar: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Popup", move |cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(window_bounds),
|
||||
kind: WindowKind::PopUp,
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|_cx| SubWindow {
|
||||
custom_titlebar: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Custom Titlebar", move |cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
titlebar: None,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|_cx| SubWindow {
|
||||
custom_titlebar: true,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Invisible", move |cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
show: false,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|_cx| SubWindow {
|
||||
custom_titlebar: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Unmovable", move |cx| {
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
is_movable: false,
|
||||
titlebar: None,
|
||||
window_bounds: Some(window_bounds),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|_cx| SubWindow {
|
||||
custom_titlebar: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}))
|
||||
.child(button("Hide Application", |cx| {
|
||||
cx.hide();
|
||||
|
||||
// Restore the application after 3 seconds
|
||||
cx.spawn(|mut cx| async move {
|
||||
Timer::after(std::time::Duration::from_secs(3)).await;
|
||||
cx.update(|cx| {
|
||||
cx.activate(false);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|_cx| WindowDemo {}),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
@@ -2057,7 +2057,6 @@ impl Interactivity {
|
||||
fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut WindowContext) {
|
||||
if let Some(scroll_offset) = self.scroll_offset.clone() {
|
||||
let overflow = style.overflow;
|
||||
let allow_concurrent_scroll = style.allow_concurrent_scroll;
|
||||
let line_height = cx.line_height();
|
||||
let hitbox = hitbox.clone();
|
||||
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
|
||||
@@ -2066,31 +2065,27 @@ impl Interactivity {
|
||||
let old_scroll_offset = *scroll_offset;
|
||||
let delta = event.delta.pixel_delta(line_height);
|
||||
|
||||
let mut delta_x = Pixels::ZERO;
|
||||
if overflow.x == Overflow::Scroll {
|
||||
let mut delta_x = Pixels::ZERO;
|
||||
if !delta.x.is_zero() {
|
||||
delta_x = delta.x;
|
||||
} else if overflow.y != Overflow::Scroll {
|
||||
delta_x = delta.y;
|
||||
}
|
||||
|
||||
scroll_offset.x += delta_x;
|
||||
}
|
||||
let mut delta_y = Pixels::ZERO;
|
||||
|
||||
if overflow.y == Overflow::Scroll {
|
||||
let mut delta_y = Pixels::ZERO;
|
||||
if !delta.y.is_zero() {
|
||||
delta_y = delta.y;
|
||||
} else if overflow.x != Overflow::Scroll {
|
||||
delta_y = delta.x;
|
||||
}
|
||||
|
||||
scroll_offset.y += delta_y;
|
||||
}
|
||||
if !allow_concurrent_scroll && !delta_x.is_zero() && !delta_y.is_zero() {
|
||||
if delta_x.abs() > delta_y.abs() {
|
||||
delta_y = Pixels::ZERO;
|
||||
} else {
|
||||
delta_x = Pixels::ZERO;
|
||||
}
|
||||
}
|
||||
scroll_offset.y += delta_y;
|
||||
scroll_offset.x += delta_x;
|
||||
|
||||
cx.stop_propagation();
|
||||
if *scroll_offset != old_scroll_offset {
|
||||
|
||||
@@ -89,16 +89,6 @@ pub enum ListSizingBehavior {
|
||||
Auto,
|
||||
}
|
||||
|
||||
/// The horizontal sizing behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ListHorizontalSizingBehavior {
|
||||
/// List items' width can never exceed the width of the list.
|
||||
#[default]
|
||||
FitList,
|
||||
/// List items' width may go over the width of the list, if any item is wider.
|
||||
Unconstrained,
|
||||
}
|
||||
|
||||
struct LayoutItemsResponse {
|
||||
max_item_width: Pixels,
|
||||
scroll_top: ListOffset,
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! elements with uniform height.
|
||||
|
||||
use crate::{
|
||||
point, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
|
||||
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
|
||||
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||
ListSizingBehavior, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
@@ -14,8 +14,6 @@ use smallvec::SmallVec;
|
||||
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
use super::ListHorizontalSizingBehavior;
|
||||
|
||||
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
|
||||
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
|
||||
/// uniform_list will only render the visible subset of items.
|
||||
@@ -59,7 +57,6 @@ where
|
||||
},
|
||||
scroll_handle: None,
|
||||
sizing_behavior: ListSizingBehavior::default(),
|
||||
horizontal_sizing_behavior: ListHorizontalSizingBehavior::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +69,11 @@ pub struct UniformList {
|
||||
interactivity: Interactivity,
|
||||
scroll_handle: Option<UniformListScrollHandle>,
|
||||
sizing_behavior: ListSizingBehavior,
|
||||
horizontal_sizing_behavior: ListHorizontalSizingBehavior,
|
||||
}
|
||||
|
||||
/// Frame state used by the [UniformList].
|
||||
pub struct UniformListFrameState {
|
||||
item_size: Size<Pixels>,
|
||||
items: SmallVec<[AnyElement; 32]>,
|
||||
}
|
||||
|
||||
@@ -90,18 +87,7 @@ pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
|
||||
pub struct UniformListScrollState {
|
||||
pub base_handle: ScrollHandle,
|
||||
pub deferred_scroll_to_item: Option<usize>,
|
||||
/// Size of the item, captured during last layout.
|
||||
pub last_item_size: Option<ItemSize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
/// The size of the item and its contents.
|
||||
pub struct ItemSize {
|
||||
/// The size of the item.
|
||||
pub item: Size<Pixels>,
|
||||
/// The size of the item's contents, which may be larger than the item itself,
|
||||
/// if the item was bounded by a parent element.
|
||||
pub contents: Size<Pixels>,
|
||||
pub last_item_height: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl UniformListScrollHandle {
|
||||
@@ -110,7 +96,7 @@ impl UniformListScrollHandle {
|
||||
Self(Rc::new(RefCell::new(UniformListScrollState {
|
||||
base_handle: ScrollHandle::new(),
|
||||
deferred_scroll_to_item: None,
|
||||
last_item_size: None,
|
||||
last_item_height: None,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -184,6 +170,7 @@ impl Element for UniformList {
|
||||
(
|
||||
layout_id,
|
||||
UniformListFrameState {
|
||||
item_size,
|
||||
items: SmallVec::new(),
|
||||
},
|
||||
)
|
||||
@@ -206,30 +193,17 @@ impl Element for UniformList {
|
||||
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||
);
|
||||
|
||||
let can_scroll_horizontally = matches!(
|
||||
self.horizontal_sizing_behavior,
|
||||
ListHorizontalSizingBehavior::Unconstrained
|
||||
);
|
||||
|
||||
let longest_item_size = self.measure_item(None, cx);
|
||||
let content_width = if can_scroll_horizontally {
|
||||
padded_bounds.size.width.max(longest_item_size.width)
|
||||
} else {
|
||||
padded_bounds.size.width
|
||||
};
|
||||
let content_size = Size {
|
||||
width: content_width,
|
||||
height: longest_item_size.height * self.item_count + padding.top + padding.bottom,
|
||||
width: padded_bounds.size.width,
|
||||
height: frame_state.item_size.height * self.item_count + padding.top + padding.bottom,
|
||||
};
|
||||
|
||||
let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
|
||||
let item_height = longest_item_size.height;
|
||||
|
||||
let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
|
||||
let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
|
||||
let mut handle = handle.0.borrow_mut();
|
||||
handle.last_item_size = Some(ItemSize {
|
||||
item: padded_bounds.size,
|
||||
contents: content_size,
|
||||
});
|
||||
handle.last_item_height = Some(item_height);
|
||||
handle.deferred_scroll_to_item.take()
|
||||
});
|
||||
|
||||
@@ -254,19 +228,12 @@ impl Element for UniformList {
|
||||
if self.item_count > 0 {
|
||||
let content_height =
|
||||
item_height * self.item_count + padding.top + padding.bottom;
|
||||
let is_scrolled_vertically = !scroll_offset.y.is_zero();
|
||||
let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
|
||||
if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
|
||||
shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
|
||||
scroll_offset.y = min_vertical_scroll_offset;
|
||||
}
|
||||
let min_scroll_offset = padded_bounds.size.height - content_height;
|
||||
let is_scrolled = scroll_offset.y != px(0.);
|
||||
|
||||
let content_width = content_size.width + padding.left + padding.right;
|
||||
let is_scrolled_horizontally =
|
||||
can_scroll_horizontally && !scroll_offset.x.is_zero();
|
||||
if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
|
||||
shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
|
||||
scroll_offset.x = Pixels::ZERO;
|
||||
if is_scrolled && scroll_offset.y < min_scroll_offset {
|
||||
shared_scroll_offset.borrow_mut().y = min_scroll_offset;
|
||||
scroll_offset.y = min_scroll_offset;
|
||||
}
|
||||
|
||||
if let Some(ix) = shared_scroll_to_item {
|
||||
@@ -296,21 +263,9 @@ impl Element for UniformList {
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (mut item, ix) in items.into_iter().zip(visible_range) {
|
||||
let item_origin = padded_bounds.origin
|
||||
+ point(
|
||||
if can_scroll_horizontally {
|
||||
scroll_offset.x + padding.left
|
||||
} else {
|
||||
scroll_offset.x
|
||||
},
|
||||
item_height * ix + scroll_offset.y + padding.top,
|
||||
);
|
||||
let available_width = if can_scroll_horizontally {
|
||||
padded_bounds.size.width + scroll_offset.x.abs()
|
||||
} else {
|
||||
padded_bounds.size.width
|
||||
};
|
||||
+ point(px(0.), item_height * ix + scroll_offset.y + padding.top);
|
||||
let available_space = size(
|
||||
AvailableSpace::Definite(available_width),
|
||||
AvailableSpace::Definite(padded_bounds.size.width),
|
||||
AvailableSpace::Definite(item_height),
|
||||
);
|
||||
item.layout_as_root(available_space, cx);
|
||||
@@ -363,25 +318,6 @@ impl UniformList {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal sizing behavior, controlling the way list items laid out horizontally.
|
||||
/// With [`ListHorizontalSizingBehavior::Unconstrained`] behavior, every item and the list itself will
|
||||
/// have the size of the widest item and lay out pushing the `end_slot` to the right end.
|
||||
pub fn with_horizontal_sizing_behavior(
|
||||
mut self,
|
||||
behavior: ListHorizontalSizingBehavior,
|
||||
) -> Self {
|
||||
self.horizontal_sizing_behavior = behavior;
|
||||
match behavior {
|
||||
ListHorizontalSizingBehavior::FitList => {
|
||||
self.interactivity.base_style.overflow.x = None;
|
||||
}
|
||||
ListHorizontalSizingBehavior::Unconstrained => {
|
||||
self.interactivity.base_style.overflow.x = Some(Overflow::Scroll);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
|
||||
if self.item_count == 0 {
|
||||
return Size::default();
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::{
|
||||
|
||||
use super::x11::X11Client;
|
||||
|
||||
pub(crate) const SCROLL_LINES: f32 = 3.0;
|
||||
pub(crate) const SCROLL_LINES: f64 = 3.0;
|
||||
|
||||
// Values match the defaults on GTK.
|
||||
// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
|
||||
|
||||
@@ -1634,10 +1634,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES;
|
||||
scroll_delta.y += discrete as f32 * axis_modifier * SCROLL_LINES as f32;
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES;
|
||||
scroll_delta.x += discrete as f32 * axis_modifier * SCROLL_LINES as f32;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@@ -1662,10 +1662,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
let wheel_percent = value120 as f32 / 120.0;
|
||||
match axis {
|
||||
wl_pointer::Axis::VerticalScroll => {
|
||||
scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES;
|
||||
scroll_delta.y += wheel_percent * axis_modifier * SCROLL_LINES as f32;
|
||||
}
|
||||
wl_pointer::Axis::HorizontalScroll => {
|
||||
scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES;
|
||||
scroll_delta.x += wheel_percent * axis_modifier * SCROLL_LINES as f32;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use core::str;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
@@ -42,10 +42,7 @@ use crate::{
|
||||
WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
button_or_scroll_from_event_detail, get_valuator_axis_index, modifiers_from_state,
|
||||
pressed_button_from_mask, ButtonOrScroll, ScrollDirection,
|
||||
};
|
||||
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
|
||||
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
|
||||
@@ -54,15 +51,7 @@ use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
|
||||
};
|
||||
|
||||
/// Value for DeviceId parameters which selects all devices.
|
||||
pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0;
|
||||
|
||||
/// Value for DeviceId parameters which selects all device groups. Events that
|
||||
/// occur within the group are emitted by the group itself.
|
||||
///
|
||||
/// In XInput 2's interface, these are referred to as "master devices", but that
|
||||
/// terminology is both archaic and unclear.
|
||||
pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1;
|
||||
pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11WindowStatePtr,
|
||||
@@ -128,26 +117,6 @@ pub struct Xdnd {
|
||||
position: Point<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PointerDeviceState {
|
||||
horizontal: ScrollAxisState,
|
||||
vertical: ScrollAxisState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ScrollAxisState {
|
||||
/// Valuator number for looking up this axis's scroll value.
|
||||
valuator_number: Option<u16>,
|
||||
/// Conversion factor from scroll units to lines.
|
||||
multiplier: f32,
|
||||
/// Last scroll value for calculating scroll delta.
|
||||
///
|
||||
/// This gets set to `None` whenever it might be invalid - when devices change or when window focus changes.
|
||||
/// The logic errs on the side of invalidating this, since the consequence is just skipping the delta of one scroll event.
|
||||
/// The consequence of not invalidating it can be large invalid deltas, which are much more user visible.
|
||||
scroll_value: Option<f32>,
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
@@ -183,7 +152,9 @@ pub struct X11ClientState {
|
||||
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
|
||||
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
|
||||
|
||||
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
pub(crate) scroll_class_data: Vec<xinput::DeviceClassDataScroll>,
|
||||
pub(crate) scroll_x: Option<f32>,
|
||||
pub(crate) scroll_y: Option<f32>,
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||
@@ -295,21 +266,31 @@ impl X11Client {
|
||||
.prefetch_extension_information(xinput::X11_EXTENSION_NAME)
|
||||
.unwrap();
|
||||
|
||||
// Announce to X server that XInput up to 2.1 is supported. To increase this to 2.2 and
|
||||
// beyond, support for touch events would need to be added.
|
||||
let xinput_version = xcb_connection
|
||||
.xinput_xi_query_version(2, 1)
|
||||
.xinput_xi_query_version(2, 0)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
// XInput 1.x is not supported.
|
||||
assert!(
|
||||
xinput_version.major_version >= 2,
|
||||
"XInput version >= 2 required."
|
||||
"XInput Extension v2 not supported."
|
||||
);
|
||||
|
||||
let pointer_device_states =
|
||||
get_new_pointer_device_states(&xcb_connection, &BTreeMap::new());
|
||||
let master_device_query = xcb_connection
|
||||
.xinput_xi_query_device(XINPUT_MASTER_DEVICE)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
let scroll_class_data = master_device_query
|
||||
.infos
|
||||
.iter()
|
||||
.find(|info| info.type_ == xinput::DeviceType::MASTER_POINTER)
|
||||
.unwrap()
|
||||
.classes
|
||||
.iter()
|
||||
.filter_map(|class| class.data.as_scroll())
|
||||
.map(|class| *class)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
|
||||
|
||||
@@ -453,7 +434,9 @@ impl X11Client {
|
||||
cursor_styles: HashMap::default(),
|
||||
cursor_cache: HashMap::default(),
|
||||
|
||||
pointer_device_states,
|
||||
scroll_class_data,
|
||||
scroll_x: None,
|
||||
scroll_y: None,
|
||||
|
||||
clipboard,
|
||||
clipboard_item: None,
|
||||
@@ -967,56 +950,35 @@ impl X11Client {
|
||||
window.handle_ime_commit(text);
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
match button_or_scroll_from_event_detail(event.detail) {
|
||||
Some(ButtonOrScroll::Button(button)) => {
|
||||
let click_elapsed = state.last_click.elapsed();
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
&& state
|
||||
.last_mouse_button
|
||||
.is_some_and(|prev_button| prev_button == button)
|
||||
&& is_within_click_distance(state.last_location, position)
|
||||
{
|
||||
state.current_count += 1;
|
||||
} else {
|
||||
state.current_count = 1;
|
||||
}
|
||||
if let Some(button) = button_of_key(event.detail.try_into().unwrap()) {
|
||||
let click_elapsed = state.last_click.elapsed();
|
||||
|
||||
state.last_click = Instant::now();
|
||||
state.last_mouse_button = Some(button);
|
||||
state.last_location = position;
|
||||
let current_count = state.current_count;
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
&& state
|
||||
.last_mouse_button
|
||||
.is_some_and(|prev_button| prev_button == button)
|
||||
&& is_within_click_distance(state.last_location, position)
|
||||
{
|
||||
state.current_count += 1;
|
||||
} else {
|
||||
state.current_count = 1;
|
||||
}
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count: current_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
}
|
||||
Some(ButtonOrScroll::Scroll(direction)) => {
|
||||
drop(state);
|
||||
// Emulated scroll button presses are sent simultaneously with smooth scrolling XinputMotion events.
|
||||
// Since handling those events does the scrolling, they are skipped here.
|
||||
if !event
|
||||
.flags
|
||||
.contains(xinput::PointerEventFlags::POINTER_EMULATED)
|
||||
{
|
||||
let scroll_delta = match direction {
|
||||
ScrollDirection::Up => Point::new(0.0, SCROLL_LINES),
|
||||
ScrollDirection::Down => Point::new(0.0, -SCROLL_LINES),
|
||||
ScrollDirection::Left => Point::new(SCROLL_LINES, 0.0),
|
||||
ScrollDirection::Right => Point::new(-SCROLL_LINES, 0.0),
|
||||
};
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
make_scroll_wheel_event(position, scroll_delta, modifiers),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::error!("Unknown x11 button: {}", event.detail);
|
||||
}
|
||||
state.last_click = Instant::now();
|
||||
state.last_mouse_button = Some(button);
|
||||
state.last_location = position;
|
||||
let current_count = state.current_count;
|
||||
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count: current_count,
|
||||
first_mouse: false,
|
||||
}));
|
||||
} else {
|
||||
log::warn!("Unknown button press: {event:?}");
|
||||
}
|
||||
}
|
||||
Event::XinputButtonRelease(event) => {
|
||||
@@ -1029,19 +991,15 @@ impl X11Client {
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
);
|
||||
match button_or_scroll_from_event_detail(event.detail) {
|
||||
Some(ButtonOrScroll::Button(button)) => {
|
||||
let click_count = state.current_count;
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count,
|
||||
}));
|
||||
}
|
||||
Some(ButtonOrScroll::Scroll(_)) => {}
|
||||
None => {}
|
||||
if let Some(button) = button_of_key(event.detail.try_into().unwrap()) {
|
||||
let click_count = state.current_count;
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
|
||||
button,
|
||||
position,
|
||||
modifiers,
|
||||
click_count,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Event::XinputMotion(event) => {
|
||||
@@ -1056,6 +1014,12 @@ impl X11Client {
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
let axisvalues = event
|
||||
.axisvalues
|
||||
.iter()
|
||||
.map(|axisvalue| fp3232_to_f32(*axisvalue))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if event.valuator_mask[0] & 3 != 0 {
|
||||
window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
|
||||
position,
|
||||
@@ -1064,17 +1028,64 @@ impl X11Client {
|
||||
}));
|
||||
}
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
|
||||
let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event);
|
||||
drop(state);
|
||||
if let Some(scroll_delta) = scroll_delta {
|
||||
window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event(
|
||||
position,
|
||||
scroll_delta,
|
||||
modifiers,
|
||||
)));
|
||||
let mut valuator_idx = 0;
|
||||
let scroll_class_data = self.0.borrow().scroll_class_data.clone();
|
||||
for shift in 0..32 {
|
||||
if (event.valuator_mask[0] >> shift) & 1 == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for scroll_class in &scroll_class_data {
|
||||
if scroll_class.scroll_type == xinput::ScrollType::HORIZONTAL
|
||||
&& scroll_class.number == shift
|
||||
{
|
||||
let new_scroll = axisvalues[valuator_idx]
|
||||
/ fp3232_to_f32(scroll_class.increment)
|
||||
* SCROLL_LINES as f32;
|
||||
let old_scroll = self.0.borrow().scroll_x;
|
||||
self.0.borrow_mut().scroll_x = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(delta_scroll, 0.0)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
));
|
||||
}
|
||||
} else if scroll_class.scroll_type == xinput::ScrollType::VERTICAL
|
||||
&& scroll_class.number == shift
|
||||
{
|
||||
// the `increment` is the valuator delta equivalent to one positive unit of scrolling. Here that means SCROLL_LINES lines.
|
||||
let new_scroll = axisvalues[valuator_idx]
|
||||
/ fp3232_to_f32(scroll_class.increment)
|
||||
* SCROLL_LINES as f32;
|
||||
let old_scroll = self.0.borrow().scroll_y;
|
||||
self.0.borrow_mut().scroll_y = Some(new_scroll);
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
let (x, y) = if !modifiers.shift {
|
||||
(0.0, delta_scroll)
|
||||
} else {
|
||||
(delta_scroll, 0.0)
|
||||
};
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(x, y)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
valuator_idx += 1;
|
||||
}
|
||||
}
|
||||
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
@@ -1084,10 +1095,10 @@ impl X11Client {
|
||||
state.mouse_focused_window = Some(event.event);
|
||||
}
|
||||
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
self.0.borrow_mut().scroll_y = None;
|
||||
|
||||
// Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.mouse_focused_window = None;
|
||||
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
||||
let position = point(
|
||||
@@ -1106,26 +1117,6 @@ impl X11Client {
|
||||
}));
|
||||
window.set_hovered(false);
|
||||
}
|
||||
Event::XinputHierarchy(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
// Temporarily use `state.pointer_device_states` to only store pointers that still have valid scroll values.
|
||||
// Any change to a device invalidates its scroll values.
|
||||
for info in event.infos {
|
||||
if is_pointer_device(info.type_) {
|
||||
state.pointer_device_states.remove(&info.deviceid);
|
||||
}
|
||||
}
|
||||
state.pointer_device_states = get_new_pointer_device_states(
|
||||
&state.xcb_connection,
|
||||
&state.pointer_device_states,
|
||||
);
|
||||
}
|
||||
Event::XinputDeviceChanged(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
|
||||
reset_pointer_device_scroll_positions(&mut pointer);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
@@ -1751,142 +1742,3 @@ fn xdnd_send_status(
|
||||
.send_event(false, target, EventMask::default(), message)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Recomputes `pointer_device_states` by querying all pointer devices.
|
||||
/// When a device is present in `scroll_values_to_preserve`, its value for `ScrollAxisState.scroll_value` is used.
|
||||
fn get_new_pointer_device_states(
|
||||
xcb_connection: &XCBConnection,
|
||||
scroll_values_to_preserve: &BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
) -> BTreeMap<xinput::DeviceId, PointerDeviceState> {
|
||||
let devices_query_result = xcb_connection
|
||||
.xinput_xi_query_device(XINPUT_ALL_DEVICES)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
let mut pointer_device_states = BTreeMap::new();
|
||||
pointer_device_states.extend(
|
||||
devices_query_result
|
||||
.infos
|
||||
.iter()
|
||||
.filter(|info| is_pointer_device(info.type_))
|
||||
.filter_map(|info| {
|
||||
let scroll_data = info
|
||||
.classes
|
||||
.iter()
|
||||
.filter_map(|class| class.data.as_scroll())
|
||||
.map(|class| *class)
|
||||
.rev()
|
||||
.collect::<Vec<_>>();
|
||||
let old_state = scroll_values_to_preserve.get(&info.deviceid);
|
||||
let old_horizontal = old_state.map(|state| &state.horizontal);
|
||||
let old_vertical = old_state.map(|state| &state.vertical);
|
||||
let horizontal = scroll_data
|
||||
.iter()
|
||||
.find(|data| data.scroll_type == xinput::ScrollType::HORIZONTAL)
|
||||
.map(|data| scroll_data_to_axis_state(data, old_horizontal));
|
||||
let vertical = scroll_data
|
||||
.iter()
|
||||
.find(|data| data.scroll_type == xinput::ScrollType::VERTICAL)
|
||||
.map(|data| scroll_data_to_axis_state(data, old_vertical));
|
||||
if horizontal.is_none() && vertical.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
info.deviceid,
|
||||
PointerDeviceState {
|
||||
horizontal: horizontal.unwrap_or_else(Default::default),
|
||||
vertical: vertical.unwrap_or_else(Default::default),
|
||||
},
|
||||
))
|
||||
}
|
||||
}),
|
||||
);
|
||||
if pointer_device_states.is_empty() {
|
||||
log::error!("Found no xinput mouse pointers.");
|
||||
}
|
||||
return pointer_device_states;
|
||||
}
|
||||
|
||||
/// Returns true if the device is a pointer device. Does not include pointer device groups.
|
||||
fn is_pointer_device(type_: xinput::DeviceType) -> bool {
|
||||
type_ == xinput::DeviceType::SLAVE_POINTER
|
||||
}
|
||||
|
||||
fn scroll_data_to_axis_state(
|
||||
data: &xinput::DeviceClassDataScroll,
|
||||
old_axis_state_with_valid_scroll_value: Option<&ScrollAxisState>,
|
||||
) -> ScrollAxisState {
|
||||
ScrollAxisState {
|
||||
valuator_number: Some(data.number),
|
||||
multiplier: SCROLL_LINES / fp3232_to_f32(data.increment),
|
||||
scroll_value: old_axis_state_with_valid_scroll_value.and_then(|state| state.scroll_value),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_all_pointer_device_scroll_positions(
|
||||
pointer_device_states: &mut BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
) {
|
||||
pointer_device_states
|
||||
.iter_mut()
|
||||
.for_each(|(_, device_state)| reset_pointer_device_scroll_positions(device_state));
|
||||
}
|
||||
|
||||
fn reset_pointer_device_scroll_positions(pointer: &mut PointerDeviceState) {
|
||||
pointer.horizontal.scroll_value = None;
|
||||
pointer.vertical.scroll_value = None;
|
||||
}
|
||||
|
||||
/// Returns the scroll delta for a smooth scrolling motion event, or `None` if no scroll data is present.
|
||||
fn get_scroll_delta_and_update_state(
|
||||
pointer: &mut PointerDeviceState,
|
||||
event: &xinput::MotionEvent,
|
||||
) -> Option<Point<f32>> {
|
||||
let delta_x = get_axis_scroll_delta_and_update_state(event, &mut pointer.horizontal);
|
||||
let delta_y = get_axis_scroll_delta_and_update_state(event, &mut pointer.vertical);
|
||||
if delta_x.is_some() || delta_y.is_some() {
|
||||
Some(Point::new(delta_x.unwrap_or(0.0), delta_y.unwrap_or(0.0)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_axis_scroll_delta_and_update_state(
|
||||
event: &xinput::MotionEvent,
|
||||
axis: &mut ScrollAxisState,
|
||||
) -> Option<f32> {
|
||||
let axis_index = get_valuator_axis_index(&event.valuator_mask, axis.valuator_number?)?;
|
||||
if let Some(axis_value) = event.axisvalues.get(axis_index) {
|
||||
let new_scroll = fp3232_to_f32(*axis_value);
|
||||
let delta_scroll = axis
|
||||
.scroll_value
|
||||
.map(|old_scroll| (old_scroll - new_scroll) * axis.multiplier);
|
||||
axis.scroll_value = Some(new_scroll);
|
||||
delta_scroll
|
||||
} else {
|
||||
log::error!("Encountered invalid XInput valuator_mask, scrolling may not work properly.");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn make_scroll_wheel_event(
|
||||
position: Point<Pixels>,
|
||||
scroll_delta: Point<f32>,
|
||||
modifiers: Modifiers,
|
||||
) -> crate::ScrollWheelEvent {
|
||||
// When shift is held down, vertical scrolling turns into horizontal scrolling.
|
||||
let delta = if modifiers.shift {
|
||||
Point {
|
||||
x: scroll_delta.y,
|
||||
y: 0.0,
|
||||
}
|
||||
} else {
|
||||
scroll_delta
|
||||
};
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(delta),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,13 @@ use x11rb::protocol::{
|
||||
|
||||
use crate::{Modifiers, MouseButton, NavigationDirection};
|
||||
|
||||
pub(crate) enum ButtonOrScroll {
|
||||
Button(MouseButton),
|
||||
Scroll(ScrollDirection),
|
||||
}
|
||||
|
||||
pub(crate) enum ScrollDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option<ButtonOrScroll> {
|
||||
pub(crate) fn button_of_key(detail: xproto::Button) -> Option<MouseButton> {
|
||||
Some(match detail {
|
||||
1 => ButtonOrScroll::Button(MouseButton::Left),
|
||||
2 => ButtonOrScroll::Button(MouseButton::Middle),
|
||||
3 => ButtonOrScroll::Button(MouseButton::Right),
|
||||
4 => ButtonOrScroll::Scroll(ScrollDirection::Up),
|
||||
5 => ButtonOrScroll::Scroll(ScrollDirection::Down),
|
||||
6 => ButtonOrScroll::Scroll(ScrollDirection::Left),
|
||||
7 => ButtonOrScroll::Scroll(ScrollDirection::Right),
|
||||
8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)),
|
||||
9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)),
|
||||
1 => MouseButton::Left,
|
||||
2 => MouseButton::Middle,
|
||||
3 => MouseButton::Right,
|
||||
8 => MouseButton::Navigate(NavigationDirection::Back),
|
||||
9 => MouseButton::Navigate(NavigationDirection::Forward),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -64,91 +48,3 @@ pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option<MouseButton>
|
||||
return None;
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_valuator_axis_index(
|
||||
valuator_mask: &Vec<u32>,
|
||||
valuator_number: u16,
|
||||
) -> Option<usize> {
|
||||
// XInput valuator masks have a 1 at the bit indexes corresponding to each
|
||||
// valuator present in this event's axisvalues. Axisvalues is ordered from
|
||||
// lowest valuator number to highest, so counting bits before the 1 bit for
|
||||
// this valuator yields the index in axisvalues.
|
||||
if bit_is_set_in_vec(&valuator_mask, valuator_number) {
|
||||
Some(popcount_upto_bit_index(&valuator_mask, valuator_number) as usize)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`.
|
||||
fn popcount_upto_bit_index(bit_vec: &Vec<u32>, bit_index: u16) -> u32 {
|
||||
let array_index = bit_index as usize / 32;
|
||||
let popcount: u32 = bit_vec
|
||||
.get(array_index)
|
||||
.map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones());
|
||||
if array_index == 0 {
|
||||
popcount
|
||||
} else {
|
||||
// Valuator numbers over 32 probably never occur for scroll position, but may as well
|
||||
// support it.
|
||||
let leading_popcount: u32 = bit_vec
|
||||
.iter()
|
||||
.take(array_index)
|
||||
.map(|bits| bits.count_ones())
|
||||
.sum();
|
||||
popcount + leading_popcount
|
||||
}
|
||||
}
|
||||
|
||||
fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool {
|
||||
let array_index = bit_index as usize / 32;
|
||||
bit_vec
|
||||
.get(array_index)
|
||||
.map_or(false, |bits| bit_is_set(*bits, bit_index % 32))
|
||||
}
|
||||
|
||||
fn bit_is_set(bits: u32, bit_index: u16) -> bool {
|
||||
bits & (1 << bit_index) != 0
|
||||
}
|
||||
|
||||
/// Sets every bit with `i >= bit_index` to 0.
|
||||
fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 {
|
||||
if bit_index == 0 {
|
||||
0
|
||||
} else if bit_index >= 32 {
|
||||
u32::MAX
|
||||
} else {
|
||||
bits & ((1 << bit_index) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_valuator_axis_index() {
|
||||
assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1));
|
||||
assert!(get_valuator_axis_index(&vec![0b11], 2) == None);
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 0) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 1) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b100], 3) == None);
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1));
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None);
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2));
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None);
|
||||
|
||||
assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use super::{X11Display, XINPUT_ALL_DEVICES, XINPUT_ALL_DEVICE_GROUPS};
|
||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
||||
x11rb::atom_manager! {
|
||||
pub XcbAtoms: AtomsCookie {
|
||||
XA_ATOM,
|
||||
@@ -475,7 +475,7 @@ impl X11WindowState {
|
||||
.xinput_xi_select_events(
|
||||
x_window,
|
||||
&[xinput::EventMask {
|
||||
deviceid: XINPUT_ALL_DEVICE_GROUPS,
|
||||
deviceid: XINPUT_MASTER_DEVICE,
|
||||
mask: vec![
|
||||
xinput::XIEventMask::MOTION
|
||||
| xinput::XIEventMask::BUTTON_PRESS
|
||||
@@ -487,19 +487,6 @@ impl X11WindowState {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
xcb_connection
|
||||
.xinput_xi_select_events(
|
||||
x_window,
|
||||
&[xinput::EventMask {
|
||||
deviceid: XINPUT_ALL_DEVICES,
|
||||
mask: vec![
|
||||
xinput::XIEventMask::HIERARCHY,
|
||||
xinput::XIEventMask::DEVICE_CHANGED,
|
||||
],
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
xcb_connection.flush().unwrap();
|
||||
|
||||
let raw = RawWindow {
|
||||
@@ -1266,7 +1253,7 @@ impl PlatformWindow for X11Window {
|
||||
self.0.x_window,
|
||||
state.atoms._GTK_SHOW_WINDOW_MENU,
|
||||
[
|
||||
XINPUT_ALL_DEVICE_GROUPS as u32,
|
||||
XINPUT_MASTER_DEVICE as u32,
|
||||
coords.dst_x as u32,
|
||||
coords.dst_y as u32,
|
||||
0,
|
||||
|
||||
@@ -707,7 +707,7 @@ impl MacWindow {
|
||||
}
|
||||
}
|
||||
|
||||
if focus && show {
|
||||
if focus {
|
||||
native_window.makeKeyAndOrderFront_(nil);
|
||||
} else if show {
|
||||
native_window.orderFront_(nil);
|
||||
|
||||
@@ -295,9 +295,13 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
fn activate(&self, _ignoring_other_apps: bool) {}
|
||||
|
||||
fn hide(&self) {}
|
||||
// todo(windows)
|
||||
fn hide(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
fn hide_other_apps(&self) {
|
||||
|
||||
@@ -287,7 +287,7 @@ impl WindowsWindow {
|
||||
.map(|title| title.as_ref())
|
||||
.unwrap_or(""),
|
||||
);
|
||||
let (dwexstyle, mut dwstyle) = if params.kind == WindowKind::PopUp {
|
||||
let (dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
|
||||
(WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
|
||||
} else {
|
||||
(
|
||||
@@ -295,10 +295,6 @@ impl WindowsWindow {
|
||||
WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
|
||||
)
|
||||
};
|
||||
if !params.show {
|
||||
dwstyle |= WS_MINIMIZE;
|
||||
}
|
||||
|
||||
let hinstance = get_module_handle();
|
||||
let display = if let Some(display_id) = params.display_id {
|
||||
// if we obtain a display_id, then this ID must be valid.
|
||||
@@ -361,12 +357,7 @@ impl WindowsWindow {
|
||||
drop(lock);
|
||||
SetWindowPlacement(raw_hwnd, &placement)?;
|
||||
}
|
||||
|
||||
if params.show {
|
||||
unsafe { ShowWindow(raw_hwnd, SW_SHOW).ok()? };
|
||||
} else {
|
||||
unsafe { ShowWindow(raw_hwnd, SW_HIDE).ok()? };
|
||||
}
|
||||
unsafe { ShowWindow(raw_hwnd, SW_SHOW).ok()? };
|
||||
|
||||
Ok(Self(state_ptr))
|
||||
}
|
||||
|
||||
@@ -156,8 +156,6 @@ pub struct Style {
|
||||
pub overflow: Point<Overflow>,
|
||||
/// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
|
||||
pub scrollbar_width: f32,
|
||||
/// Whether both x and y axis should be scrollable at the same time.
|
||||
pub allow_concurrent_scroll: bool,
|
||||
|
||||
// Position properties
|
||||
/// What should the `position` value of this struct use as a base offset?
|
||||
@@ -669,7 +667,6 @@ impl Default for Style {
|
||||
x: Overflow::Visible,
|
||||
y: Overflow::Visible,
|
||||
},
|
||||
allow_concurrent_scroll: false,
|
||||
scrollbar_width: 0.0,
|
||||
position: Position::Relative,
|
||||
inset: Edges::auto(),
|
||||
|
||||
@@ -62,7 +62,7 @@ pub use text::{
|
||||
use theme::SyntaxTheme;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
use util::{debug_panic, RangeExt};
|
||||
use util::RangeExt;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||
@@ -823,41 +823,40 @@ impl Buffer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies all of the changes in this buffer that intersect the given `range`
|
||||
/// to its base buffer. This buffer must be a branch buffer to call this method.
|
||||
pub fn merge_into_base(&mut self, range: Option<Range<usize>>, cx: &mut ModelContext<Self>) {
|
||||
let Some(base_buffer) = self.diff_base_buffer() else {
|
||||
debug_panic!("not a branch buffer");
|
||||
return;
|
||||
};
|
||||
|
||||
base_buffer.update(cx, |base_buffer, cx| {
|
||||
let edits = self
|
||||
.edits_since::<usize>(&base_buffer.version)
|
||||
.filter_map(|edit| {
|
||||
if range
|
||||
.as_ref()
|
||||
.map_or(true, |range| range.overlaps(&edit.new))
|
||||
{
|
||||
Some((edit.old, self.text_for_range(edit.new).collect::<String>()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
/// Applies all of the changes in `branch` buffer that intersect the given `range`
|
||||
/// to this buffer.
|
||||
pub fn merge(
|
||||
&mut self,
|
||||
branch: &Model<Self>,
|
||||
range: Option<Range<Anchor>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let edits = branch.read_with(cx, |branch, _| {
|
||||
branch
|
||||
.edits_since_in_range::<usize>(
|
||||
&self.version,
|
||||
range.unwrap_or(Anchor::MIN..Anchor::MAX),
|
||||
)
|
||||
.map(|edit| {
|
||||
(
|
||||
edit.old,
|
||||
branch.text_for_range(edit.new).collect::<String>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let operation = self.edit(edits, None, cx);
|
||||
|
||||
let operation = base_buffer.edit(edits, None, cx);
|
||||
|
||||
// Prevent this operation from being reapplied to the branch.
|
||||
// Prevent this operation from being reapplied to the branch.
|
||||
branch.update(cx, |branch, cx| {
|
||||
if let Some(BufferDiffBase::PastBufferVersion {
|
||||
operations_to_ignore,
|
||||
..
|
||||
}) = &mut self.diff_base
|
||||
}) = &mut branch.diff_base
|
||||
{
|
||||
operations_to_ignore.extend(operation);
|
||||
}
|
||||
|
||||
cx.emit(BufferEvent::DiffBaseChanged);
|
||||
cx.emit(BufferEvent::Edited)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2471,8 +2471,8 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Merging the branch applies all of its changes to the base.
|
||||
branch_buffer.update(cx, |branch_buffer, cx| {
|
||||
branch_buffer.merge_into_base(None, cx);
|
||||
base_buffer.update(cx, |base_buffer, cx| {
|
||||
base_buffer.merge(&branch_buffer, None, cx);
|
||||
});
|
||||
|
||||
branch_buffer.update(cx, |branch_buffer, cx| {
|
||||
@@ -2484,18 +2484,6 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_merge_into_base(cx: &mut AppContext) {
|
||||
init_settings(cx, |_| {});
|
||||
let base = cx.new_model(|cx| Buffer::local("abcdefghijk", cx));
|
||||
let branch = base.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
branch.update(cx, |branch, cx| {
|
||||
branch.edit([(0..3, "ABC"), (7..9, "HI")], None, cx);
|
||||
branch.merge_into_base(Some(5..8), cx);
|
||||
});
|
||||
assert_eq!(base.read(cx).text(), "abcdefgHIjk");
|
||||
}
|
||||
|
||||
fn start_recalculating_diff(buffer: &Model<Buffer>, cx: &mut TestAppContext) {
|
||||
buffer
|
||||
.update(cx, |buffer, cx| buffer.recalculate_diff(cx).unwrap())
|
||||
|
||||
@@ -381,7 +381,7 @@ pub struct FeaturesContent {
|
||||
pub enum SoftWrap {
|
||||
/// Prefer a single line generally, unless an overly long line is encountered.
|
||||
None,
|
||||
/// Deprecated: use None instead. Left to avoid breaking existing users' configs.
|
||||
/// Deprecated: use None instead. Left to avoid breakin existing users' configs.
|
||||
/// Prefer a single line generally, unless an overly long line is encountered.
|
||||
PreferLine,
|
||||
/// Soft wrap lines that exceed the editor width.
|
||||
|
||||
@@ -46,7 +46,6 @@ lsp.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
protols-tree-sitter-proto = { workspace = true, optional = true }
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rust-embed.workspace = true
|
||||
@@ -56,24 +55,26 @@ settings.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
toml.workspace = true
|
||||
tree-sitter = { workspace = true, optional = true }
|
||||
tree-sitter-bash = { workspace = true, optional = true }
|
||||
tree-sitter-c = { workspace = true, optional = true }
|
||||
tree-sitter-cpp = { workspace = true, optional = true }
|
||||
tree-sitter-css = { workspace = true, optional = true }
|
||||
tree-sitter-go = { workspace = true, optional = true }
|
||||
tree-sitter-go-mod = { workspace = true, optional = true }
|
||||
tree-sitter-gowork = { workspace = true, optional = true }
|
||||
tree-sitter-jsdoc = { workspace = true, optional = true }
|
||||
tree-sitter-json = { workspace = true, optional = true }
|
||||
tree-sitter-md = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
tree-sitter-regex = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter-yaml = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
|
||||
tree-sitter-bash = {workspace = true, optional = true}
|
||||
tree-sitter-c = {workspace = true, optional = true}
|
||||
tree-sitter-cpp = {workspace = true, optional = true}
|
||||
tree-sitter-css = {workspace = true, optional = true}
|
||||
tree-sitter-go = {workspace = true, optional = true}
|
||||
tree-sitter-go-mod = {workspace = true, optional = true}
|
||||
tree-sitter-gowork = {workspace = true, optional = true}
|
||||
tree-sitter-jsdoc = {workspace = true, optional = true}
|
||||
tree-sitter-json = {workspace = true, optional = true}
|
||||
tree-sitter-md = {workspace = true, optional = true}
|
||||
protols-tree-sitter-proto = {workspace = true, optional = true}
|
||||
tree-sitter-python = {workspace = true, optional = true}
|
||||
tree-sitter-regex = {workspace = true, optional = true}
|
||||
tree-sitter-rust = {workspace = true, optional = true}
|
||||
tree-sitter-typescript = {workspace = true, optional = true}
|
||||
tree-sitter-yaml = {workspace = true, optional = true}
|
||||
tree-sitter = {workspace = true, optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
text.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name = "Proto"
|
||||
name = "proto"
|
||||
grammar = "proto"
|
||||
path_suffixes = ["proto"]
|
||||
line_comments = ["// "]
|
||||
|
||||
@@ -54,7 +54,7 @@ use parking_lot::{Mutex, RwLock};
|
||||
use paths::{local_tasks_file_relative_path, local_vscode_tasks_file_relative_path};
|
||||
pub use prettier_store::PrettierStore;
|
||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||
use remote::SshRemoteClient;
|
||||
use remote::SshSession;
|
||||
use rpc::{proto::SSH_PROJECT_ID, AnyProtoClient, ErrorCode};
|
||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||
use search_history::SearchHistory;
|
||||
@@ -138,7 +138,7 @@ pub struct Project {
|
||||
join_project_response_message_id: u32,
|
||||
user_store: Model<UserStore>,
|
||||
fs: Arc<dyn Fs>,
|
||||
ssh_client: Option<Arc<SshRemoteClient>>,
|
||||
ssh_session: Option<Arc<SshSession>>,
|
||||
client_state: ProjectClientState,
|
||||
collaborators: HashMap<proto::PeerId, Collaborator>,
|
||||
client_subscriptions: Vec<client::Subscription>,
|
||||
@@ -643,7 +643,7 @@ impl Project {
|
||||
user_store,
|
||||
settings_observer,
|
||||
fs,
|
||||
ssh_client: None,
|
||||
ssh_session: None,
|
||||
buffers_needing_diff: Default::default(),
|
||||
git_diff_debouncer: DebouncedDelay::new(),
|
||||
terminals: Terminals {
|
||||
@@ -664,7 +664,7 @@ impl Project {
|
||||
}
|
||||
|
||||
pub fn ssh(
|
||||
ssh: Arc<SshRemoteClient>,
|
||||
ssh: Arc<SshSession>,
|
||||
client: Arc<Client>,
|
||||
node: NodeRuntime,
|
||||
user_store: Model<UserStore>,
|
||||
@@ -682,14 +682,14 @@ impl Project {
|
||||
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
|
||||
|
||||
let worktree_store =
|
||||
cx.new_model(|_| WorktreeStore::remote(false, ssh.to_proto_client(), 0, None));
|
||||
cx.new_model(|_| WorktreeStore::remote(false, ssh.clone().into(), 0, None));
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
let buffer_store = cx.new_model(|cx| {
|
||||
BufferStore::remote(
|
||||
worktree_store.clone(),
|
||||
ssh.to_proto_client(),
|
||||
ssh.clone().into(),
|
||||
SSH_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
@@ -698,7 +698,7 @@ impl Project {
|
||||
.detach();
|
||||
|
||||
let settings_observer = cx.new_model(|cx| {
|
||||
SettingsObserver::new_ssh(ssh.to_proto_client(), worktree_store.clone(), cx)
|
||||
SettingsObserver::new_ssh(ssh.clone().into(), worktree_store.clone(), cx)
|
||||
});
|
||||
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
|
||||
.detach();
|
||||
@@ -709,7 +709,7 @@ impl Project {
|
||||
buffer_store.clone(),
|
||||
worktree_store.clone(),
|
||||
languages.clone(),
|
||||
ssh.to_proto_client(),
|
||||
ssh.clone().into(),
|
||||
SSH_PROJECT_ID,
|
||||
cx,
|
||||
)
|
||||
@@ -733,7 +733,7 @@ impl Project {
|
||||
user_store,
|
||||
settings_observer,
|
||||
fs,
|
||||
ssh_client: Some(ssh.clone()),
|
||||
ssh_session: Some(ssh.clone()),
|
||||
buffers_needing_diff: Default::default(),
|
||||
git_diff_debouncer: DebouncedDelay::new(),
|
||||
terminals: Terminals {
|
||||
@@ -751,7 +751,7 @@ impl Project {
|
||||
search_excluded_history: Self::new_search_history(),
|
||||
};
|
||||
|
||||
let client: AnyProtoClient = ssh.to_proto_client();
|
||||
let client: AnyProtoClient = ssh.clone().into();
|
||||
|
||||
ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
|
||||
ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
|
||||
@@ -907,7 +907,7 @@ impl Project {
|
||||
user_store: user_store.clone(),
|
||||
snippets,
|
||||
fs,
|
||||
ssh_client: None,
|
||||
ssh_session: None,
|
||||
settings_observer: settings_observer.clone(),
|
||||
client_subscriptions: Default::default(),
|
||||
_subscriptions: vec![cx.on_release(Self::release)],
|
||||
@@ -1230,7 +1230,7 @@ impl Project {
|
||||
match self.client_state {
|
||||
ProjectClientState::Remote { replica_id, .. } => replica_id,
|
||||
_ => {
|
||||
if self.ssh_client.is_some() {
|
||||
if self.ssh_session.is_some() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
@@ -1638,7 +1638,7 @@ impl Project {
|
||||
pub fn is_local(&self) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||
self.ssh_client.is_none()
|
||||
self.ssh_session.is_none()
|
||||
}
|
||||
ProjectClientState::Remote { .. } => false,
|
||||
}
|
||||
@@ -1647,7 +1647,7 @@ impl Project {
|
||||
pub fn is_via_ssh(&self) -> bool {
|
||||
match &self.client_state {
|
||||
ProjectClientState::Local | ProjectClientState::Shared { .. } => {
|
||||
self.ssh_client.is_some()
|
||||
self.ssh_session.is_some()
|
||||
}
|
||||
ProjectClientState::Remote { .. } => false,
|
||||
}
|
||||
@@ -1933,9 +1933,8 @@ impl Project {
|
||||
}
|
||||
BufferStoreEvent::BufferChangedFilePath { .. } => {}
|
||||
BufferStoreEvent::BufferDropped(buffer_id) => {
|
||||
if let Some(ref ssh_client) = self.ssh_client {
|
||||
ssh_client
|
||||
.to_proto_client()
|
||||
if let Some(ref ssh_session) = self.ssh_session {
|
||||
ssh_session
|
||||
.send(proto::CloseBuffer {
|
||||
project_id: 0,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
@@ -2140,14 +2139,13 @@ impl Project {
|
||||
} => {
|
||||
let operation = language::proto::serialize_operation(operation);
|
||||
|
||||
if let Some(ssh) = &self.ssh_client {
|
||||
ssh.to_proto_client()
|
||||
.send(proto::UpdateBuffer {
|
||||
project_id: 0,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
operations: vec![operation.clone()],
|
||||
})
|
||||
.ok();
|
||||
if let Some(ssh) = &self.ssh_session {
|
||||
ssh.send(proto::UpdateBuffer {
|
||||
project_id: 0,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
operations: vec![operation.clone()],
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Operation {
|
||||
@@ -2827,13 +2825,14 @@ impl Project {
|
||||
) -> Receiver<Model<Buffer>> {
|
||||
let (tx, rx) = smol::channel::unbounded();
|
||||
|
||||
let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.ssh_client {
|
||||
(ssh_client.to_proto_client(), 0)
|
||||
} else if let Some(remote_id) = self.remote_id() {
|
||||
(self.client.clone().into(), remote_id)
|
||||
} else {
|
||||
return rx;
|
||||
};
|
||||
let (client, remote_id): (AnyProtoClient, _) =
|
||||
if let Some(ssh_session) = self.ssh_session.clone() {
|
||||
(ssh_session.into(), 0)
|
||||
} else if let Some(remote_id) = self.remote_id() {
|
||||
(self.client.clone().into(), remote_id)
|
||||
} else {
|
||||
return rx;
|
||||
};
|
||||
|
||||
let request = client.request(proto::FindSearchCandidates {
|
||||
project_id: remote_id,
|
||||
@@ -2962,13 +2961,11 @@ impl Project {
|
||||
|
||||
exists.then(|| ResolvedPath::AbsPath(expanded))
|
||||
})
|
||||
} else if let Some(ssh_client) = self.ssh_client.as_ref() {
|
||||
let request = ssh_client
|
||||
.to_proto_client()
|
||||
.request(proto::CheckFileExists {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
path: path.to_string(),
|
||||
});
|
||||
} else if let Some(ssh_session) = self.ssh_session.as_ref() {
|
||||
let request = ssh_session.request(proto::CheckFileExists {
|
||||
project_id: SSH_PROJECT_ID,
|
||||
path: path.to_string(),
|
||||
});
|
||||
cx.background_executor().spawn(async move {
|
||||
let response = request.await.log_err()?;
|
||||
if response.exists {
|
||||
@@ -3038,13 +3035,13 @@ impl Project {
|
||||
) -> Task<Result<Vec<PathBuf>>> {
|
||||
if self.is_local() {
|
||||
DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
|
||||
} else if let Some(session) = self.ssh_client.as_ref() {
|
||||
} else if let Some(session) = self.ssh_session.as_ref() {
|
||||
let request = proto::ListRemoteDirectory {
|
||||
dev_server_id: SSH_PROJECT_ID,
|
||||
path: query,
|
||||
};
|
||||
|
||||
let response = session.to_proto_client().request(request);
|
||||
let response = session.request(request);
|
||||
cx.background_executor().spawn(async move {
|
||||
let response = response.await?;
|
||||
Ok(response.entries.into_iter().map(PathBuf::from).collect())
|
||||
@@ -3468,11 +3465,11 @@ impl Project {
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let buffer_store = this.read_with(&cx, |this, cx| {
|
||||
if let Some(ssh) = &this.ssh_client {
|
||||
if let Some(ssh) = &this.ssh_session {
|
||||
let mut payload = envelope.payload.clone();
|
||||
payload.project_id = 0;
|
||||
cx.background_executor()
|
||||
.spawn(ssh.to_proto_client().request(payload))
|
||||
.spawn(ssh.request(payload))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
this.buffer_store.clone()
|
||||
|
||||
@@ -67,12 +67,8 @@ impl Project {
|
||||
}
|
||||
|
||||
fn ssh_command(&self, cx: &AppContext) -> Option<SshCommand> {
|
||||
if let Some(args) = self
|
||||
.ssh_client
|
||||
.as_ref()
|
||||
.and_then(|session| session.ssh_args())
|
||||
{
|
||||
return Some(SshCommand::Direct(args));
|
||||
if let Some(ssh_session) = self.ssh_session.as_ref() {
|
||||
return Some(SshCommand::Direct(ssh_session.ssh_args()));
|
||||
}
|
||||
|
||||
let dev_server_project_id = self.dev_server_project_id()?;
|
||||
|
||||
@@ -8,22 +8,20 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{
|
||||
items::entry_git_aware_label_color,
|
||||
scroll::{Autoscroll, ScrollbarAutoHide},
|
||||
Editor, EditorEvent, EditorSettings, ShowScrollbar,
|
||||
Editor,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{hash_map, BTreeSet, HashMap};
|
||||
use core::f32;
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::{
|
||||
actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
|
||||
AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, DragMoveEvent,
|
||||
Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView, InteractiveElement,
|
||||
KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Model, MouseButton,
|
||||
MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
|
||||
Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView,
|
||||
WindowContext,
|
||||
EventEmitter, ExternalPaths, FocusHandle, FocusableView, InteractiveElement, KeyContext,
|
||||
ListSizingBehavior, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||
@@ -31,7 +29,7 @@ use project::{
|
||||
relativize_path, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, Worktree,
|
||||
WorktreeId,
|
||||
};
|
||||
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
|
||||
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings, ShowScrollbar};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cell::{Cell, OnceCell},
|
||||
@@ -82,10 +80,8 @@ pub struct ProjectPanel {
|
||||
width: Option<Pixels>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
show_scrollbar: bool,
|
||||
vertical_scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
|
||||
horizontal_scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
|
||||
scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
max_width_item_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -94,8 +90,6 @@ struct EditState {
|
||||
entry_id: ProjectEntryId,
|
||||
is_new_entry: bool,
|
||||
is_dir: bool,
|
||||
is_symlink: bool,
|
||||
depth: usize,
|
||||
processing_filename: Option<String>,
|
||||
}
|
||||
|
||||
@@ -260,26 +254,23 @@ impl ProjectPanel {
|
||||
|
||||
let filename_editor = cx.new_view(Editor::single_line);
|
||||
|
||||
cx.subscribe(
|
||||
&filename_editor,
|
||||
|project_panel, _, editor_event, cx| match editor_event {
|
||||
EditorEvent::BufferEdited | EditorEvent::SelectionsChanged { .. } => {
|
||||
project_panel.autoscroll(cx);
|
||||
cx.subscribe(&filename_editor, |this, _, event, cx| match event {
|
||||
editor::EditorEvent::BufferEdited
|
||||
| editor::EditorEvent::SelectionsChanged { .. } => {
|
||||
this.autoscroll(cx);
|
||||
}
|
||||
editor::EditorEvent::Blurred => {
|
||||
if this
|
||||
.edit_state
|
||||
.as_ref()
|
||||
.map_or(false, |state| state.processing_filename.is_none())
|
||||
{
|
||||
this.edit_state = None;
|
||||
this.update_visible_entries(None, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
if project_panel
|
||||
.edit_state
|
||||
.as_ref()
|
||||
.map_or(false, |state| state.processing_filename.is_none())
|
||||
{
|
||||
project_panel.edit_state = None;
|
||||
project_panel.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_global::<FileIcons>(|_, cx| {
|
||||
@@ -320,9 +311,7 @@ impl ProjectPanel {
|
||||
pending_serialization: Task::ready(None),
|
||||
show_scrollbar: !Self::should_autohide_scrollbar(cx),
|
||||
hide_scrollbar_task: None,
|
||||
vertical_scrollbar_drag_thumb_offset: Default::default(),
|
||||
horizontal_scrollbar_drag_thumb_offset: Default::default(),
|
||||
max_width_item_index: None,
|
||||
scrollbar_drag_thumb_offset: Default::default(),
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
|
||||
@@ -838,7 +827,7 @@ impl ProjectPanel {
|
||||
Some(cx.spawn(|project_panel, mut cx| async move {
|
||||
let new_entry = edit_task.await;
|
||||
project_panel.update(&mut cx, |project_panel, cx| {
|
||||
project_panel.edit_state = None;
|
||||
project_panel.edit_state.take();
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
@@ -981,8 +970,6 @@ impl ProjectPanel {
|
||||
is_new_entry: true,
|
||||
is_dir,
|
||||
processing_filename: None,
|
||||
is_symlink: false,
|
||||
depth: 0,
|
||||
});
|
||||
self.filename_editor.update(cx, |editor, cx| {
|
||||
editor.clear(cx);
|
||||
@@ -1005,7 +992,6 @@ impl ProjectPanel {
|
||||
leaf_entry_id
|
||||
}
|
||||
}
|
||||
|
||||
fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
|
||||
if let Some(SelectedEntry {
|
||||
worktree_id,
|
||||
@@ -1021,8 +1007,6 @@ impl ProjectPanel {
|
||||
is_new_entry: false,
|
||||
is_dir: entry.is_dir(),
|
||||
processing_filename: None,
|
||||
is_symlink: entry.is_symlink,
|
||||
depth: 0,
|
||||
});
|
||||
let file_name = entry
|
||||
.path
|
||||
@@ -1766,7 +1750,6 @@ impl ProjectPanel {
|
||||
|
||||
let old_ancestors = std::mem::take(&mut self.ancestors);
|
||||
self.visible_entries.clear();
|
||||
let mut max_width_item = None;
|
||||
for worktree in project.visible_worktrees(cx) {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_id = snapshot.id();
|
||||
@@ -1822,12 +1805,6 @@ impl ProjectPanel {
|
||||
.get(&entry.id)
|
||||
.map(|ancestor| ancestor.current_ancestor_depth)
|
||||
.unwrap_or_default();
|
||||
if let Some(edit_state) = &mut self.edit_state {
|
||||
if edit_state.entry_id == entry.id {
|
||||
edit_state.is_symlink = entry.is_symlink;
|
||||
edit_state.depth = depth;
|
||||
}
|
||||
}
|
||||
let mut ancestors = std::mem::take(&mut auto_folded_ancestors);
|
||||
if ancestors.len() > 1 {
|
||||
ancestors.reverse();
|
||||
@@ -1860,78 +1837,6 @@ impl ProjectPanel {
|
||||
is_fifo: entry.is_fifo,
|
||||
});
|
||||
}
|
||||
let worktree_abs_path = worktree.read(cx).abs_path();
|
||||
let (depth, path) = if Some(entry) == worktree.read(cx).root_entry() {
|
||||
let Some(path_name) = worktree_abs_path
|
||||
.file_name()
|
||||
.with_context(|| {
|
||||
format!("Worktree abs path has no file name, root entry: {entry:?}")
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let path = Arc::from(Path::new(path_name));
|
||||
let depth = 0;
|
||||
(depth, path)
|
||||
} else if entry.is_file() {
|
||||
let Some(path_name) = entry
|
||||
.path
|
||||
.file_name()
|
||||
.with_context(|| format!("Non-root entry has no file name: {entry:?}"))
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let path = Arc::from(Path::new(path_name));
|
||||
let depth = entry.path.ancestors().count() - 1;
|
||||
(depth, path)
|
||||
} else {
|
||||
let path = self
|
||||
.ancestors
|
||||
.get(&entry.id)
|
||||
.and_then(|ancestors| {
|
||||
let outermost_ancestor = ancestors.ancestors.last()?;
|
||||
let root_folded_entry = worktree
|
||||
.read(cx)
|
||||
.entry_for_id(*outermost_ancestor)?
|
||||
.path
|
||||
.as_ref();
|
||||
entry
|
||||
.path
|
||||
.strip_prefix(root_folded_entry)
|
||||
.ok()
|
||||
.and_then(|suffix| {
|
||||
let full_path = Path::new(root_folded_entry.file_name()?);
|
||||
Some(Arc::<Path>::from(full_path.join(suffix)))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| entry.path.clone());
|
||||
let depth = path
|
||||
.strip_prefix(worktree_abs_path)
|
||||
.map(|suffix| suffix.components().count())
|
||||
.unwrap_or_default();
|
||||
(depth, path)
|
||||
};
|
||||
let width_estimate = item_width_estimate(
|
||||
depth,
|
||||
path.to_string_lossy().chars().count(),
|
||||
entry.is_symlink,
|
||||
);
|
||||
|
||||
match max_width_item.as_mut() {
|
||||
Some((id, worktree_id, width)) => {
|
||||
if *width < width_estimate {
|
||||
*id = entry.id;
|
||||
*worktree_id = worktree.read(cx).id();
|
||||
*width = width_estimate;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
max_width_item = Some((entry.id, worktree.read(cx).id(), width_estimate))
|
||||
}
|
||||
}
|
||||
|
||||
if expanded_dir_ids.binary_search(&entry.id).is_err()
|
||||
&& entry_iter.advance_to_sibling()
|
||||
{
|
||||
@@ -1946,22 +1851,6 @@ impl ProjectPanel {
|
||||
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
|
||||
}
|
||||
|
||||
if let Some((project_entry_id, worktree_id, _)) = max_width_item {
|
||||
let mut visited_worktrees_length = 0;
|
||||
let index = self.visible_entries.iter().find_map(|(id, entries, _)| {
|
||||
if worktree_id == *id {
|
||||
entries
|
||||
.iter()
|
||||
.position(|entry| entry.id == project_entry_id)
|
||||
} else {
|
||||
visited_worktrees_length += entries.len();
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(index) = index {
|
||||
self.max_width_item_index = Some(visited_worktrees_length + index);
|
||||
}
|
||||
}
|
||||
if let Some((worktree_id, entry_id)) = new_selected_entry {
|
||||
self.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
@@ -2585,8 +2474,7 @@ impl ProjectPanel {
|
||||
cx.stop_propagation();
|
||||
this.deploy_context_menu(event.position, entry_id, cx);
|
||||
},
|
||||
))
|
||||
.overflow_x(),
|
||||
)),
|
||||
)
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
@@ -2610,19 +2498,22 @@ impl ProjectPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
|
||||
if !Self::should_show_scrollbar(cx) {
|
||||
fn render_scrollbar(
|
||||
&self,
|
||||
items_count: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Stateful<Div>> {
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
if settings.scrollbar.show == ShowScrollbar::Never {
|
||||
return None;
|
||||
}
|
||||
let scroll_handle = self.scroll_handle.0.borrow();
|
||||
let total_list_length = scroll_handle
|
||||
.last_item_size
|
||||
.filter(|_| {
|
||||
self.show_scrollbar || self.vertical_scrollbar_drag_thumb_offset.get().is_some()
|
||||
})?
|
||||
.contents
|
||||
.height
|
||||
.0 as f64;
|
||||
|
||||
let height = scroll_handle
|
||||
.last_item_height
|
||||
.filter(|_| self.show_scrollbar || self.scrollbar_drag_thumb_offset.get().is_some())?;
|
||||
|
||||
let total_list_length = height.0 as f64 * items_count as f64;
|
||||
let current_offset = scroll_handle.base_handle.offset().y.0.min(0.).abs() as f64;
|
||||
let mut percentage = current_offset / total_list_length;
|
||||
let end_offset = (current_offset + scroll_handle.base_handle.bounds().size.height.0 as f64)
|
||||
@@ -2645,7 +2536,7 @@ impl ProjectPanel {
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("project-panel-vertical-scroll")
|
||||
.id("project-panel-scroll")
|
||||
.on_mouse_move(cx.listener(|_, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
@@ -2659,7 +2550,7 @@ impl ProjectPanel {
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, cx| {
|
||||
if this.vertical_scrollbar_drag_thumb_offset.get().is_none()
|
||||
if this.scrollbar_drag_thumb_offset.get().is_none()
|
||||
&& !this.focus_handle.contains_focused(cx)
|
||||
{
|
||||
this.hide_scrollbar(cx);
|
||||
@@ -2674,101 +2565,21 @@ impl ProjectPanel {
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_1()
|
||||
.right_0()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.child(ProjectPanelScrollbar::vertical(
|
||||
.child(ProjectPanelScrollbar::new(
|
||||
percentage as f32..end_offset as f32,
|
||||
self.scroll_handle.clone(),
|
||||
self.vertical_scrollbar_drag_thumb_offset.clone(),
|
||||
cx.view().entity_id(),
|
||||
self.scrollbar_drag_thumb_offset.clone(),
|
||||
cx.view().clone().into(),
|
||||
items_count,
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_horizontal_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
|
||||
if !Self::should_show_scrollbar(cx) {
|
||||
return None;
|
||||
}
|
||||
let scroll_handle = self.scroll_handle.0.borrow();
|
||||
let longest_item_width = scroll_handle
|
||||
.last_item_size
|
||||
.filter(|_| {
|
||||
self.show_scrollbar || self.horizontal_scrollbar_drag_thumb_offset.get().is_some()
|
||||
})
|
||||
.filter(|size| size.contents.width > size.item.width)?
|
||||
.contents
|
||||
.width
|
||||
.0 as f64;
|
||||
let current_offset = scroll_handle.base_handle.offset().x.0.min(0.).abs() as f64;
|
||||
let mut percentage = current_offset / longest_item_width;
|
||||
let end_offset = (current_offset + scroll_handle.base_handle.bounds().size.width.0 as f64)
|
||||
/ longest_item_width;
|
||||
// Uniform scroll handle might briefly report an offset greater than the length of a list;
|
||||
// in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.
|
||||
let overshoot = (end_offset - 1.).clamp(0., 1.);
|
||||
if overshoot > 0. {
|
||||
percentage -= overshoot;
|
||||
}
|
||||
const MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH: f64 = 0.005;
|
||||
if percentage + MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH > 1.0 || end_offset > longest_item_width
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
|
||||
return None;
|
||||
}
|
||||
let end_offset = end_offset.clamp(percentage + MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH, 1.);
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("project-panel-horizontal-scroll")
|
||||
.on_mouse_move(cx.listener(|_, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, cx| {
|
||||
if this.horizontal_scrollbar_drag_thumb_offset.get().is_none()
|
||||
&& !this.focus_handle.contains_focused(cx)
|
||||
{
|
||||
this.hide_scrollbar(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.w_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.left_1()
|
||||
.bottom_1()
|
||||
.h(px(12.))
|
||||
.cursor_default()
|
||||
.when(self.width.is_some(), |this| {
|
||||
this.child(ProjectPanelScrollbar::horizontal(
|
||||
percentage as f32..end_offset as f32,
|
||||
self.scroll_handle.clone(),
|
||||
self.horizontal_scrollbar_drag_thumb_offset.clone(),
|
||||
cx.view().entity_id(),
|
||||
))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
|
||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||
dispatch_context.add("ProjectPanel");
|
||||
@@ -2784,32 +2595,9 @@ impl ProjectPanel {
|
||||
dispatch_context
|
||||
}
|
||||
|
||||
fn should_show_scrollbar(cx: &AppContext) -> bool {
|
||||
let show = ProjectPanelSettings::get_global(cx)
|
||||
.scrollbar
|
||||
.show
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
|
||||
match show {
|
||||
ShowScrollbar::Auto => true,
|
||||
ShowScrollbar::System => true,
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn should_autohide_scrollbar(cx: &AppContext) -> bool {
|
||||
let show = ProjectPanelSettings::get_global(cx)
|
||||
.scrollbar
|
||||
.show
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
|
||||
match show {
|
||||
ShowScrollbar::Auto => true,
|
||||
ShowScrollbar::System => cx
|
||||
.try_global::<ScrollbarAutoHide>()
|
||||
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
|
||||
ShowScrollbar::Always => false,
|
||||
ShowScrollbar::Never => true,
|
||||
}
|
||||
cx.try_global::<ScrollbarAutoHide>()
|
||||
.map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0)
|
||||
}
|
||||
|
||||
fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -2835,7 +2623,7 @@ impl ProjectPanel {
|
||||
project: Model<Project>,
|
||||
entry_id: ProjectEntryId,
|
||||
skip_ignored: bool,
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
cx: &mut ViewContext<'_, ProjectPanel>,
|
||||
) {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
@@ -2857,22 +2645,13 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize {
|
||||
const ICON_SIZE_FACTOR: usize = 2;
|
||||
let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars;
|
||||
if is_symlink {
|
||||
item_width += ICON_SIZE_FACTOR;
|
||||
}
|
||||
item_width
|
||||
}
|
||||
|
||||
impl Render for ProjectPanel {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
let has_worktree = !self.visible_entries.is_empty();
|
||||
let project = self.project.read(cx);
|
||||
|
||||
if has_worktree {
|
||||
let item_count = self
|
||||
let items_count = self
|
||||
.visible_entries
|
||||
.iter()
|
||||
.map(|(_, worktree_entries, _)| worktree_entries.len())
|
||||
@@ -2963,7 +2742,7 @@ impl Render for ProjectPanel {
|
||||
)
|
||||
.track_focus(&self.focus_handle)
|
||||
.child(
|
||||
uniform_list(cx.view().clone(), "entries", item_count, {
|
||||
uniform_list(cx.view().clone(), "entries", items_count, {
|
||||
|this, range, cx| {
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
@@ -2974,12 +2753,9 @@ impl Render for ProjectPanel {
|
||||
})
|
||||
.size_full()
|
||||
.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
|
||||
.with_width_from_item(self.max_width_item_index)
|
||||
.track_scroll(self.scroll_handle.clone()),
|
||||
)
|
||||
.children(self.render_vertical_scrollbar(cx))
|
||||
.children(self.render_horizontal_scrollbar(cx))
|
||||
.children(self.render_scrollbar(items_count, cx))
|
||||
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
@@ -3158,7 +2934,6 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::path::{Path, PathBuf};
|
||||
use ui::Context;
|
||||
use workspace::{
|
||||
item::{Item, ProjectItem},
|
||||
register_project_item, AppState,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use editor::ShowScrollbar;
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@@ -25,20 +24,33 @@ pub struct ProjectPanelSettings {
|
||||
pub scrollbar: ScrollbarSettings,
|
||||
}
|
||||
|
||||
/// When to show the scrollbar in the project panel.
|
||||
///
|
||||
/// Default: always
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShowScrollbar {
|
||||
#[default]
|
||||
/// Always show the scrollbar.
|
||||
Always,
|
||||
/// Never show the scrollbar.
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ScrollbarSettings {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
///
|
||||
/// Default: inherits editor scrollbar settings
|
||||
pub show: Option<ShowScrollbar>,
|
||||
/// Default: always
|
||||
pub show: ShowScrollbar,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ScrollbarSettingsContent {
|
||||
/// When to show the scrollbar in the project panel.
|
||||
///
|
||||
/// Default: inherits editor scrollbar settings
|
||||
pub show: Option<Option<ShowScrollbar>>,
|
||||
/// Default: always
|
||||
pub show: Option<ShowScrollbar>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
|
||||
@@ -1,54 +1,34 @@
|
||||
use std::{cell::Cell, ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
point, quad, Bounds, ContentMask, Corners, Edges, EntityId, Hitbox, Hsla, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, Style, UniformListScrollHandle,
|
||||
point, AnyView, Bounds, ContentMask, Hitbox, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
ScrollWheelEvent, Style, UniformListScrollHandle,
|
||||
};
|
||||
use ui::{prelude::*, px, relative, IntoElement};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ScrollbarKind {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
pub(crate) struct ProjectPanelScrollbar {
|
||||
thumb: Range<f32>,
|
||||
scroll: UniformListScrollHandle,
|
||||
// If Some(), there's an active drag, offset by percentage from the top of thumb.
|
||||
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
||||
kind: ScrollbarKind,
|
||||
parent_id: EntityId,
|
||||
item_count: usize,
|
||||
view: AnyView,
|
||||
}
|
||||
|
||||
impl ProjectPanelScrollbar {
|
||||
pub(crate) fn vertical(
|
||||
pub(crate) fn new(
|
||||
thumb: Range<f32>,
|
||||
scroll: UniformListScrollHandle,
|
||||
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
||||
parent_id: EntityId,
|
||||
view: AnyView,
|
||||
item_count: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
thumb,
|
||||
scroll,
|
||||
scrollbar_drag_state,
|
||||
kind: ScrollbarKind::Vertical,
|
||||
parent_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn horizontal(
|
||||
thumb: Range<f32>,
|
||||
scroll: UniformListScrollHandle,
|
||||
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
|
||||
parent_id: EntityId,
|
||||
) -> Self {
|
||||
Self {
|
||||
thumb,
|
||||
scroll,
|
||||
scrollbar_drag_state,
|
||||
kind: ScrollbarKind::Horizontal,
|
||||
parent_id,
|
||||
item_count,
|
||||
view,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,14 +50,8 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
let mut style = Style::default();
|
||||
style.flex_grow = 1.;
|
||||
style.flex_shrink = 1.;
|
||||
if self.kind == ScrollbarKind::Vertical {
|
||||
style.size.width = px(12.).into();
|
||||
style.size.height = relative(1.).into();
|
||||
} else {
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = px(12.).into();
|
||||
}
|
||||
|
||||
style.size.width = px(12.).into();
|
||||
style.size.height = relative(1.).into();
|
||||
(cx.request_layout(style, None), ())
|
||||
}
|
||||
|
||||
@@ -103,65 +77,25 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
) {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
let colors = cx.theme().colors();
|
||||
let scrollbar_background = colors.scrollbar_track_background;
|
||||
let thumb_background = colors.scrollbar_thumb_background;
|
||||
let is_vertical = self.kind == ScrollbarKind::Vertical;
|
||||
let extra_padding = px(5.0);
|
||||
let padded_bounds = if is_vertical {
|
||||
Bounds::from_corners(
|
||||
bounds.origin + point(Pixels::ZERO, extra_padding),
|
||||
bounds.lower_right() - point(Pixels::ZERO, extra_padding * 3),
|
||||
)
|
||||
} else {
|
||||
Bounds::from_corners(
|
||||
bounds.origin + point(extra_padding, Pixels::ZERO),
|
||||
bounds.lower_right() - point(extra_padding * 3, Pixels::ZERO),
|
||||
)
|
||||
};
|
||||
cx.paint_quad(gpui::fill(bounds, scrollbar_background));
|
||||
|
||||
let mut thumb_bounds = if is_vertical {
|
||||
let thumb_offset = self.thumb.start * padded_bounds.size.height;
|
||||
let thumb_end = self.thumb.end * padded_bounds.size.height;
|
||||
let thumb_upper_left = point(
|
||||
padded_bounds.origin.x,
|
||||
padded_bounds.origin.y + thumb_offset,
|
||||
);
|
||||
let thumb_lower_right = point(
|
||||
padded_bounds.origin.x + padded_bounds.size.width,
|
||||
padded_bounds.origin.y + thumb_end,
|
||||
);
|
||||
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
|
||||
} else {
|
||||
let thumb_offset = self.thumb.start * padded_bounds.size.width;
|
||||
let thumb_end = self.thumb.end * padded_bounds.size.width;
|
||||
let thumb_upper_left = point(
|
||||
padded_bounds.origin.x + thumb_offset,
|
||||
padded_bounds.origin.y,
|
||||
);
|
||||
let thumb_lower_right = point(
|
||||
padded_bounds.origin.x + thumb_end,
|
||||
padded_bounds.origin.y + padded_bounds.size.height,
|
||||
);
|
||||
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
|
||||
};
|
||||
let corners = if is_vertical {
|
||||
thumb_bounds.size.width /= 1.5;
|
||||
Corners::all(thumb_bounds.size.width / 2.0)
|
||||
} else {
|
||||
thumb_bounds.size.height /= 1.5;
|
||||
Corners::all(thumb_bounds.size.height / 2.0)
|
||||
};
|
||||
cx.paint_quad(quad(
|
||||
thumb_bounds,
|
||||
corners,
|
||||
thumb_background,
|
||||
Edges::default(),
|
||||
Hsla::transparent_black(),
|
||||
));
|
||||
let thumb_offset = self.thumb.start * bounds.size.height;
|
||||
let thumb_end = self.thumb.end * bounds.size.height;
|
||||
|
||||
let scroll = self.scroll.clone();
|
||||
let kind = self.kind;
|
||||
let thumb_percentage_size = self.thumb.end - self.thumb.start;
|
||||
|
||||
let thumb_bounds = {
|
||||
let thumb_upper_left = point(bounds.origin.x, bounds.origin.y + thumb_offset);
|
||||
let thumb_lower_right = point(
|
||||
bounds.origin.x + bounds.size.width,
|
||||
bounds.origin.y + thumb_end,
|
||||
);
|
||||
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
|
||||
};
|
||||
cx.paint_quad(gpui::fill(thumb_bounds, thumb_background));
|
||||
let scroll = self.scroll.clone();
|
||||
let item_count = self.item_count;
|
||||
cx.on_mouse_event({
|
||||
let scroll = self.scroll.clone();
|
||||
let is_dragging = self.scrollbar_drag_state.clone();
|
||||
@@ -169,37 +103,20 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
if phase.bubble() && bounds.contains(&event.position) {
|
||||
if !thumb_bounds.contains(&event.position) {
|
||||
let scroll = scroll.0.borrow();
|
||||
if let Some(item_size) = scroll.last_item_size {
|
||||
match kind {
|
||||
ScrollbarKind::Horizontal => {
|
||||
let percentage = (event.position.x - bounds.origin.x)
|
||||
/ bounds.size.width;
|
||||
let max_offset = item_size.contents.width;
|
||||
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||
scroll.base_handle.set_offset(point(
|
||||
-max_offset * percentage,
|
||||
scroll.base_handle.offset().y,
|
||||
));
|
||||
}
|
||||
ScrollbarKind::Vertical => {
|
||||
let percentage = (event.position.y - bounds.origin.y)
|
||||
/ bounds.size.height;
|
||||
let max_offset = item_size.contents.height;
|
||||
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||
scroll.base_handle.set_offset(point(
|
||||
scroll.base_handle.offset().x,
|
||||
-max_offset * percentage,
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(last_height) = scroll.last_item_height {
|
||||
let max_offset = item_count as f32 * last_height;
|
||||
let percentage =
|
||||
(event.position.y - bounds.origin.y) / bounds.size.height;
|
||||
|
||||
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||
scroll
|
||||
.base_handle
|
||||
.set_offset(point(px(0.), -max_offset * percentage));
|
||||
}
|
||||
} else {
|
||||
let thumb_offset = if is_vertical {
|
||||
(event.position.y - thumb_bounds.origin.y) / bounds.size.height
|
||||
} else {
|
||||
(event.position.x - thumb_bounds.origin.x) / bounds.size.width
|
||||
};
|
||||
is_dragging.set(Some(thumb_offset));
|
||||
let thumb_top_offset =
|
||||
(event.position.y - thumb_bounds.origin.y) / bounds.size.height;
|
||||
is_dragging.set(Some(thumb_top_offset));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,7 +127,6 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
if phase.bubble() && bounds.contains(&event.position) {
|
||||
let scroll = scroll.0.borrow_mut();
|
||||
let current_offset = scroll.base_handle.offset();
|
||||
|
||||
scroll
|
||||
.base_handle
|
||||
.set_offset(current_offset + event.delta.pixel_delta(cx.line_height()));
|
||||
@@ -218,39 +134,19 @@ impl gpui::Element for ProjectPanelScrollbar {
|
||||
}
|
||||
});
|
||||
let drag_state = self.scrollbar_drag_state.clone();
|
||||
let view_id = self.parent_id;
|
||||
let kind = self.kind;
|
||||
let view_id = self.view.entity_id();
|
||||
cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
|
||||
if let Some(drag_state) = drag_state.get().filter(|_| event.dragging()) {
|
||||
let scroll = scroll.0.borrow();
|
||||
if let Some(item_size) = scroll.last_item_size {
|
||||
match kind {
|
||||
ScrollbarKind::Horizontal => {
|
||||
let max_offset = item_size.contents.width;
|
||||
let percentage = (event.position.x - bounds.origin.x)
|
||||
/ bounds.size.width
|
||||
- drag_state;
|
||||
|
||||
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||
scroll.base_handle.set_offset(point(
|
||||
-max_offset * percentage,
|
||||
scroll.base_handle.offset().y,
|
||||
));
|
||||
}
|
||||
ScrollbarKind::Vertical => {
|
||||
let max_offset = item_size.contents.height;
|
||||
let percentage = (event.position.y - bounds.origin.y)
|
||||
/ bounds.size.height
|
||||
- drag_state;
|
||||
|
||||
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||
scroll.base_handle.set_offset(point(
|
||||
scroll.base_handle.offset().x,
|
||||
-max_offset * percentage,
|
||||
));
|
||||
}
|
||||
};
|
||||
if let Some(last_height) = scroll.last_item_height {
|
||||
let max_offset = item_count as f32 * last_height;
|
||||
let percentage =
|
||||
(event.position.y - bounds.origin.y) / bounds.size.height - drag_state;
|
||||
|
||||
let percentage = percentage.min(1. - thumb_percentage_size);
|
||||
scroll
|
||||
.base_handle
|
||||
.set_offset(point(px(0.), -max_offset * percentage));
|
||||
cx.notify(view_id);
|
||||
}
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ use gpui::{
|
||||
Transformation, View,
|
||||
};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||
use remote::{SshConnectionOptions, SshPlatform, SshSession};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
@@ -376,12 +376,12 @@ pub fn connect_over_ssh(
|
||||
connection_options: SshConnectionOptions,
|
||||
ui: View<SshPrompt>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Arc<SshRemoteClient>>> {
|
||||
) -> Task<Result<Arc<SshSession>>> {
|
||||
let window = cx.window_handle();
|
||||
let known_password = connection_options.password.clone();
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
remote::SshRemoteClient::new(
|
||||
remote::SshSession::client(
|
||||
connection_options,
|
||||
Arc::new(SshClientDelegate {
|
||||
window,
|
||||
|
||||
@@ -2,4 +2,4 @@ pub mod json_log;
|
||||
pub mod protocol;
|
||||
pub mod ssh_session;
|
||||
|
||||
pub use ssh_session::{SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient};
|
||||
pub use ssh_session::{SshClientDelegate, SshConnectionOptions, SshPlatform, SshSession};
|
||||
|
||||
@@ -7,23 +7,19 @@ use crate::{
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::{
|
||||
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
oneshot,
|
||||
},
|
||||
channel::{mpsc, oneshot},
|
||||
future::BoxFuture,
|
||||
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt,
|
||||
StreamExt as _,
|
||||
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, StreamExt as _,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Model, SemanticVersion, Task};
|
||||
use parking_lot::Mutex;
|
||||
use rpc::{
|
||||
proto::{self, build_typed_envelope, Envelope, EnvelopedMessage, PeerId, RequestMessage},
|
||||
AnyProtoClient, EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet, RpcError,
|
||||
EntityMessageSubscriber, ProtoClient, ProtoMessageHandlerSet, RpcError,
|
||||
};
|
||||
use smol::{
|
||||
fs,
|
||||
process::{self, Child, Stdio},
|
||||
process::{self, Stdio},
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -31,7 +27,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering::SeqCst},
|
||||
Arc, Weak,
|
||||
Arc,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
@@ -48,6 +44,22 @@ pub struct SshSocket {
|
||||
socket_path: PathBuf,
|
||||
}
|
||||
|
||||
pub struct SshSession {
|
||||
next_message_id: AtomicU32,
|
||||
response_channels: ResponseChannels, // Lock
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
|
||||
client_socket: Option<SshSocket>,
|
||||
state: Mutex<ProtoMessageHandlerSet>, // Lock
|
||||
_io_task: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
struct SshClientState {
|
||||
socket: SshSocket,
|
||||
master_process: process::Child,
|
||||
_temp_dir: TempDir,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SshConnectionOptions {
|
||||
pub host: String,
|
||||
@@ -93,13 +105,18 @@ impl SshConnectionOptions {
|
||||
}
|
||||
}
|
||||
|
||||
struct SpawnRequest {
|
||||
command: String,
|
||||
process_tx: oneshot::Sender<process::Child>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct SshPlatform {
|
||||
pub os: &'static str,
|
||||
pub arch: &'static str,
|
||||
}
|
||||
|
||||
pub trait SshClientDelegate: Send + Sync {
|
||||
pub trait SshClientDelegate {
|
||||
fn ask_password(
|
||||
&self,
|
||||
prompt: String,
|
||||
@@ -115,249 +132,48 @@ pub trait SshClientDelegate: Send + Sync {
|
||||
fn set_error(&self, error_message: String, cx: &mut AsyncAppContext);
|
||||
}
|
||||
|
||||
impl SshSocket {
|
||||
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
||||
let mut command = process::Command::new("ssh");
|
||||
self.ssh_options(&mut command)
|
||||
.arg(self.connection_options.ssh_url())
|
||||
.arg(program);
|
||||
command
|
||||
}
|
||||
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
||||
|
||||
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.args(["-o", "ControlMaster=no", "-o"])
|
||||
.arg(format!("ControlPath={}", self.socket_path.display()))
|
||||
}
|
||||
|
||||
fn ssh_args(&self) -> Vec<String> {
|
||||
vec![
|
||||
"-o".to_string(),
|
||||
"ControlMaster=no".to_string(),
|
||||
"-o".to_string(),
|
||||
format!("ControlPath={}", self.socket_path.display()),
|
||||
self.connection_options.ssh_url(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||
let output = command.output().await?;
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"failed to run command: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))
|
||||
}
|
||||
}
|
||||
#[cfg(unix)]
|
||||
async fn read_with_timeout(
|
||||
stdout: &mut process::ChildStdout,
|
||||
timeout: std::time::Duration,
|
||||
output: &mut Vec<u8>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
smol::future::or(
|
||||
async {
|
||||
stdout.read_to_end(output).await?;
|
||||
Ok::<_, std::io::Error>(())
|
||||
},
|
||||
async {
|
||||
smol::Timer::after(timeout).await;
|
||||
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"Read operation timed out",
|
||||
))
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
struct ChannelForwarder {
|
||||
quit_tx: UnboundedSender<()>,
|
||||
forwarding_task: Task<(UnboundedSender<Envelope>, UnboundedReceiver<Envelope>)>,
|
||||
}
|
||||
|
||||
impl ChannelForwarder {
|
||||
fn new(
|
||||
mut incoming_tx: UnboundedSender<Envelope>,
|
||||
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> (Self, UnboundedSender<Envelope>, UnboundedReceiver<Envelope>) {
|
||||
let (quit_tx, mut quit_rx) = mpsc::unbounded::<()>();
|
||||
|
||||
let (proxy_incoming_tx, mut proxy_incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (mut proxy_outgoing_tx, proxy_outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
|
||||
let forwarding_task = cx.background_executor().spawn(async move {
|
||||
loop {
|
||||
select_biased! {
|
||||
_ = quit_rx.next().fuse() => {
|
||||
break;
|
||||
},
|
||||
incoming_envelope = proxy_incoming_rx.next().fuse() => {
|
||||
if let Some(envelope) = incoming_envelope {
|
||||
if incoming_tx.send(envelope).await.is_err() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
outgoing_envelope = outgoing_rx.next().fuse() => {
|
||||
if let Some(envelope) = outgoing_envelope {
|
||||
if proxy_outgoing_tx.send(envelope).await.is_err() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(incoming_tx, outgoing_rx)
|
||||
});
|
||||
|
||||
(
|
||||
Self {
|
||||
forwarding_task,
|
||||
quit_tx,
|
||||
},
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
)
|
||||
}
|
||||
|
||||
async fn into_channels(mut self) -> (UnboundedSender<Envelope>, UnboundedReceiver<Envelope>) {
|
||||
let _ = self.quit_tx.send(()).await;
|
||||
self.forwarding_task.await
|
||||
}
|
||||
}
|
||||
|
||||
struct SshRemoteClientState {
|
||||
ssh_connection: SshRemoteConnection,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
forwarder: ChannelForwarder,
|
||||
multiplex_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
pub struct SshRemoteClient {
|
||||
client: Arc<ChannelClient>,
|
||||
inner_state: Mutex<Option<SshRemoteClientState>>,
|
||||
}
|
||||
|
||||
impl SshRemoteClient {
|
||||
pub async fn new(
|
||||
impl SshSession {
|
||||
pub async fn client(
|
||||
connection_options: SshConnectionOptions,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Arc<Self>> {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
let client_state = SshClientState::new(connection_options, delegate.clone(), cx).await?;
|
||||
|
||||
let platform = client_state.query_platform().await?;
|
||||
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
||||
let remote_binary_path = delegate.remote_server_binary_path(cx)?;
|
||||
client_state
|
||||
.ensure_server_binary(
|
||||
&delegate,
|
||||
&local_binary_path,
|
||||
&remote_binary_path,
|
||||
version,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (spawn_process_tx, mut spawn_process_rx) = mpsc::unbounded::<SpawnRequest>();
|
||||
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
|
||||
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
|
||||
let this = Arc::new(Self {
|
||||
client,
|
||||
inner_state: Mutex::new(None),
|
||||
});
|
||||
let socket = client_state.socket.clone();
|
||||
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
||||
|
||||
let inner_state = {
|
||||
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, cx);
|
||||
|
||||
let (ssh_connection, ssh_process) =
|
||||
Self::establish_connection(connection_options.clone(), delegate.clone(), cx)
|
||||
.await?;
|
||||
|
||||
let multiplex_task = Self::multiplex(
|
||||
Arc::downgrade(&this),
|
||||
ssh_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
cx,
|
||||
);
|
||||
|
||||
SshRemoteClientState {
|
||||
ssh_connection,
|
||||
delegate,
|
||||
forwarder: proxy,
|
||||
multiplex_task,
|
||||
}
|
||||
};
|
||||
|
||||
this.inner_state.lock().replace(inner_state);
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn reconnect(this: Arc<Self>, cx: &mut AsyncAppContext) -> Result<()> {
|
||||
let Some(state) = this.inner_state.lock().take() else {
|
||||
return Err(anyhow!("reconnect is already in progress"));
|
||||
};
|
||||
|
||||
let SshRemoteClientState {
|
||||
mut ssh_connection,
|
||||
delegate,
|
||||
forwarder: proxy,
|
||||
multiplex_task,
|
||||
} = state;
|
||||
drop(multiplex_task);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (incoming_tx, outgoing_rx) = proxy.into_channels().await;
|
||||
|
||||
ssh_connection.master_process.kill()?;
|
||||
ssh_connection
|
||||
.master_process
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to kill ssh process")?;
|
||||
|
||||
let connection_options = ssh_connection.socket.connection_options.clone();
|
||||
|
||||
let (ssh_connection, ssh_process) =
|
||||
Self::establish_connection(connection_options, delegate.clone(), &mut cx).await?;
|
||||
|
||||
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||
|
||||
let inner_state = SshRemoteClientState {
|
||||
ssh_connection,
|
||||
delegate,
|
||||
forwarder: proxy,
|
||||
multiplex_task: Self::multiplex(
|
||||
Arc::downgrade(&this),
|
||||
ssh_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
&mut cx,
|
||||
),
|
||||
};
|
||||
this.inner_state.lock().replace(inner_state);
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn multiplex(
|
||||
this: Weak<Self>,
|
||||
mut ssh_process: Child,
|
||||
incoming_tx: UnboundedSender<Envelope>,
|
||||
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let mut child_stderr = ssh_process.stderr.take().unwrap();
|
||||
let mut child_stdout = ssh_process.stdout.take().unwrap();
|
||||
let mut child_stdin = ssh_process.stdin.take().unwrap();
|
||||
let mut remote_server_child = socket
|
||||
.ssh_command(format!(
|
||||
"RUST_LOG={} RUST_BACKTRACE={} {:?} run",
|
||||
std::env::var("RUST_LOG").unwrap_or_default(),
|
||||
std::env::var("RUST_BACKTRACE").unwrap_or_default(),
|
||||
remote_binary_path,
|
||||
))
|
||||
.spawn()
|
||||
.context("failed to spawn remote server")?;
|
||||
let mut child_stderr = remote_server_child.stderr.take().unwrap();
|
||||
let mut child_stdout = remote_server_child.stdout.take().unwrap();
|
||||
let mut child_stdin = remote_server_child.stdin.take().unwrap();
|
||||
|
||||
let io_task = cx.background_executor().spawn(async move {
|
||||
let mut stdin_buffer = Vec::new();
|
||||
@@ -378,15 +194,27 @@ impl SshRemoteClient {
|
||||
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
||||
}
|
||||
|
||||
request = spawn_process_rx.next().fuse() => {
|
||||
let Some(request) = request else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
log::info!("spawn process: {:?}", request.command);
|
||||
let child = client_state.socket
|
||||
.ssh_command(&request.command)
|
||||
.spawn()
|
||||
.context("failed to create channel")?;
|
||||
request.process_tx.send(child).ok();
|
||||
}
|
||||
|
||||
result = child_stdout.read(&mut stdout_buffer).fuse() => {
|
||||
match result {
|
||||
Ok(0) => {
|
||||
child_stdin.close().await?;
|
||||
outgoing_rx.close();
|
||||
let status = ssh_process.status().await?;
|
||||
let status = remote_server_child.status().await?;
|
||||
if !status.success() {
|
||||
log::error!("ssh process exited with status: {status:?}");
|
||||
return Err(anyhow!("ssh process exited with non-zero status code: {:?}", status.code()));
|
||||
log::error!("channel exited with status: {status:?}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -439,114 +267,239 @@ impl SshRemoteClient {
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = io_task.await;
|
||||
|
||||
if let Err(error) = result {
|
||||
log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
|
||||
if let Some(this) = this.upgrade() {
|
||||
Self::reconnect(this, &mut cx).ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
cx.update(|cx| {
|
||||
Self::new(
|
||||
incoming_rx,
|
||||
outgoing_tx,
|
||||
spawn_process_tx,
|
||||
Some(socket),
|
||||
Some(io_task),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn establish_connection(
|
||||
connection_options: SshConnectionOptions,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(SshRemoteConnection, Child)> {
|
||||
let ssh_connection =
|
||||
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
|
||||
|
||||
let platform = ssh_connection.query_platform().await?;
|
||||
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
||||
let remote_binary_path = delegate.remote_server_binary_path(cx)?;
|
||||
ssh_connection
|
||||
.ensure_server_binary(
|
||||
&delegate,
|
||||
&local_binary_path,
|
||||
&remote_binary_path,
|
||||
version,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let socket = ssh_connection.socket.clone();
|
||||
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
||||
|
||||
let ssh_process = socket
|
||||
.ssh_command(format!(
|
||||
"RUST_LOG={} RUST_BACKTRACE={} {:?} run",
|
||||
std::env::var("RUST_LOG").unwrap_or_default(),
|
||||
std::env::var("RUST_BACKTRACE").unwrap_or_default(),
|
||||
remote_binary_path,
|
||||
))
|
||||
.spawn()
|
||||
.context("failed to spawn remote server")?;
|
||||
|
||||
Ok((ssh_connection, ssh_process))
|
||||
}
|
||||
|
||||
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||
self.client.subscribe_to_entity(remote_id, entity);
|
||||
}
|
||||
|
||||
pub fn ssh_args(&self) -> Option<Vec<String>> {
|
||||
let state = self.inner_state.lock();
|
||||
state
|
||||
.as_ref()
|
||||
.map(|state| state.ssh_connection.socket.ssh_args())
|
||||
}
|
||||
|
||||
pub fn to_proto_client(&self) -> AnyProtoClient {
|
||||
self.client.clone().into()
|
||||
pub fn server(
|
||||
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
cx: &AppContext,
|
||||
) -> Arc<SshSession> {
|
||||
let (tx, _rx) = mpsc::unbounded();
|
||||
Self::new(incoming_rx, outgoing_tx, tx, None, None, cx)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake(
|
||||
client_cx: &mut gpui::TestAppContext,
|
||||
server_cx: &mut gpui::TestAppContext,
|
||||
) -> (Arc<Self>, Arc<ChannelClient>) {
|
||||
) -> (Arc<Self>, Arc<Self>) {
|
||||
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
|
||||
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
|
||||
|
||||
let (tx, _rx) = mpsc::unbounded();
|
||||
(
|
||||
client_cx.update(|cx| {
|
||||
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
|
||||
Arc::new(Self {
|
||||
client,
|
||||
inner_state: Mutex::new(None),
|
||||
})
|
||||
Self::new(
|
||||
server_to_client_rx,
|
||||
client_to_server_tx,
|
||||
tx.clone(),
|
||||
None, // todo()
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
server_cx.update(|cx| {
|
||||
Self::new(
|
||||
client_to_server_rx,
|
||||
server_to_client_tx,
|
||||
tx.clone(),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SshRemoteClient> for AnyProtoClient {
|
||||
fn from(client: SshRemoteClient) -> Self {
|
||||
AnyProtoClient::new(client.client.clone())
|
||||
fn new(
|
||||
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
|
||||
client_socket: Option<SshSocket>,
|
||||
io_task: Option<Task<Result<()>>>,
|
||||
cx: &AppContext,
|
||||
) -> Arc<SshSession> {
|
||||
let this = Arc::new(Self {
|
||||
next_message_id: AtomicU32::new(0),
|
||||
response_channels: ResponseChannels::default(),
|
||||
outgoing_tx,
|
||||
spawn_process_tx,
|
||||
client_socket,
|
||||
state: Default::default(),
|
||||
_io_task: io_task,
|
||||
});
|
||||
|
||||
cx.spawn(|cx| {
|
||||
let this = Arc::downgrade(&this);
|
||||
async move {
|
||||
let peer_id = PeerId { owner_id: 0, id: 0 };
|
||||
while let Some(incoming) = incoming_rx.next().await {
|
||||
let Some(this) = this.upgrade() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
if let Some(request_id) = incoming.responding_to {
|
||||
let request_id = MessageId(request_id);
|
||||
let sender = this.response_channels.lock().remove(&request_id);
|
||||
if let Some(sender) = sender {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
if incoming.payload.is_some() {
|
||||
sender.send((incoming, tx)).ok();
|
||||
}
|
||||
rx.await.ok();
|
||||
}
|
||||
} else if let Some(envelope) =
|
||||
build_typed_envelope(peer_id, Instant::now(), incoming)
|
||||
{
|
||||
let type_name = envelope.payload_type_name();
|
||||
if let Some(future) = ProtoMessageHandlerSet::handle_message(
|
||||
&this.state,
|
||||
envelope,
|
||||
this.clone().into(),
|
||||
cx.clone(),
|
||||
) {
|
||||
log::debug!("ssh message received. name:{type_name}");
|
||||
match future.await {
|
||||
Ok(_) => {
|
||||
log::debug!("ssh message handled. name:{type_name}");
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. type:{type_name}, error:{error}",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("unhandled ssh message name:{type_name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
struct SshRemoteConnection {
|
||||
socket: SshSocket,
|
||||
master_process: process::Child,
|
||||
_temp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl Drop for SshRemoteConnection {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = self.master_process.kill() {
|
||||
log::error!("failed to kill SSH master process: {}", error);
|
||||
pub fn request<T: RequestMessage>(
|
||||
&self,
|
||||
payload: T,
|
||||
) -> impl 'static + Future<Output = Result<T::Response>> {
|
||||
log::debug!("ssh request start. name:{}", T::NAME);
|
||||
let response = self.request_dynamic(payload.into_envelope(0, None, None), T::NAME);
|
||||
async move {
|
||||
let response = response.await?;
|
||||
log::debug!("ssh request finish. name:{}", T::NAME);
|
||||
T::Response::from_envelope(response)
|
||||
.ok_or_else(|| anyhow!("received a response of the wrong type"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send<T: EnvelopedMessage>(&self, payload: T) -> Result<()> {
|
||||
log::debug!("ssh send name:{}", T::NAME);
|
||||
self.send_dynamic(payload.into_envelope(0, None, None))
|
||||
}
|
||||
|
||||
pub fn request_dynamic(
|
||||
&self,
|
||||
mut envelope: proto::Envelope,
|
||||
type_name: &'static str,
|
||||
) -> impl 'static + Future<Output = Result<proto::Envelope>> {
|
||||
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let mut response_channels_lock = self.response_channels.lock();
|
||||
response_channels_lock.insert(MessageId(envelope.id), tx);
|
||||
drop(response_channels_lock);
|
||||
let result = self.outgoing_tx.unbounded_send(envelope);
|
||||
async move {
|
||||
if let Err(error) = &result {
|
||||
log::error!("failed to send message: {}", error);
|
||||
return Err(anyhow!("failed to send message: {}", error));
|
||||
}
|
||||
|
||||
let response = rx.await.context("connection lost")?.0;
|
||||
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
|
||||
return Err(RpcError::from_proto(error, type_name));
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||
self.outgoing_tx.unbounded_send(envelope)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||
let id = (TypeId::of::<E>(), remote_id);
|
||||
|
||||
let mut state = self.state.lock();
|
||||
if state.entities_by_type_and_remote_id.contains_key(&id) {
|
||||
panic!("already subscribed to entity");
|
||||
}
|
||||
|
||||
state.entities_by_type_and_remote_id.insert(
|
||||
id,
|
||||
EntityMessageSubscriber::Entity {
|
||||
handle: entity.downgrade().into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn spawn_process(&self, command: String) -> process::Child {
|
||||
let (process_tx, process_rx) = oneshot::channel();
|
||||
self.spawn_process_tx
|
||||
.unbounded_send(SpawnRequest {
|
||||
command,
|
||||
process_tx,
|
||||
})
|
||||
.ok();
|
||||
process_rx.await.unwrap()
|
||||
}
|
||||
|
||||
pub fn ssh_args(&self) -> Vec<String> {
|
||||
self.client_socket.as_ref().unwrap().ssh_args()
|
||||
}
|
||||
}
|
||||
|
||||
impl SshRemoteConnection {
|
||||
impl ProtoClient for SshSession {
|
||||
fn request(
|
||||
&self,
|
||||
envelope: proto::Envelope,
|
||||
request_type: &'static str,
|
||||
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
||||
self.request_dynamic(envelope, request_type).boxed()
|
||||
}
|
||||
|
||||
fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> {
|
||||
self.send_dynamic(envelope)
|
||||
}
|
||||
|
||||
fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> {
|
||||
self.send_dynamic(envelope)
|
||||
}
|
||||
|
||||
fn message_handler_set(&self) -> &Mutex<ProtoMessageHandlerSet> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
fn is_via_collab(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl SshClientState {
|
||||
#[cfg(not(unix))]
|
||||
async fn new(
|
||||
_connection_options: SshConnectionOptions,
|
||||
@@ -787,181 +740,74 @@ impl SshRemoteConnection {
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
||||
#[cfg(unix)]
|
||||
async fn read_with_timeout(
|
||||
stdout: &mut process::ChildStdout,
|
||||
timeout: std::time::Duration,
|
||||
output: &mut Vec<u8>,
|
||||
) -> Result<(), std::io::Error> {
|
||||
smol::future::or(
|
||||
async {
|
||||
stdout.read_to_end(output).await?;
|
||||
Ok::<_, std::io::Error>(())
|
||||
},
|
||||
async {
|
||||
smol::Timer::after(timeout).await;
|
||||
|
||||
pub struct ChannelClient {
|
||||
next_message_id: AtomicU32,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
response_channels: ResponseChannels, // Lock
|
||||
message_handlers: Mutex<ProtoMessageHandlerSet>, // Lock
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"Read operation timed out",
|
||||
))
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
impl ChannelClient {
|
||||
pub fn new(
|
||||
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
cx: &AppContext,
|
||||
) -> Arc<Self> {
|
||||
let this = Arc::new(Self {
|
||||
outgoing_tx,
|
||||
next_message_id: AtomicU32::new(0),
|
||||
response_channels: ResponseChannels::default(),
|
||||
message_handlers: Default::default(),
|
||||
});
|
||||
|
||||
Self::start_handling_messages(this.clone(), incoming_rx, cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn start_handling_messages(
|
||||
this: Arc<Self>,
|
||||
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
cx.spawn(|cx| {
|
||||
let this = Arc::downgrade(&this);
|
||||
async move {
|
||||
let peer_id = PeerId { owner_id: 0, id: 0 };
|
||||
while let Some(incoming) = incoming_rx.next().await {
|
||||
let Some(this) = this.upgrade() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
if let Some(request_id) = incoming.responding_to {
|
||||
let request_id = MessageId(request_id);
|
||||
let sender = this.response_channels.lock().remove(&request_id);
|
||||
if let Some(sender) = sender {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
if incoming.payload.is_some() {
|
||||
sender.send((incoming, tx)).ok();
|
||||
}
|
||||
rx.await.ok();
|
||||
}
|
||||
} else if let Some(envelope) =
|
||||
build_typed_envelope(peer_id, Instant::now(), incoming)
|
||||
{
|
||||
let type_name = envelope.payload_type_name();
|
||||
if let Some(future) = ProtoMessageHandlerSet::handle_message(
|
||||
&this.message_handlers,
|
||||
envelope,
|
||||
this.clone().into(),
|
||||
cx.clone(),
|
||||
) {
|
||||
log::debug!("ssh message received. name:{type_name}");
|
||||
match future.await {
|
||||
Ok(_) => {
|
||||
log::debug!("ssh message handled. name:{type_name}");
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. type:{type_name}, error:{error}",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("unhandled ssh message name:{type_name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||
let id = (TypeId::of::<E>(), remote_id);
|
||||
|
||||
let mut message_handlers = self.message_handlers.lock();
|
||||
if message_handlers
|
||||
.entities_by_type_and_remote_id
|
||||
.contains_key(&id)
|
||||
{
|
||||
panic!("already subscribed to entity");
|
||||
impl Drop for SshClientState {
|
||||
fn drop(&mut self) {
|
||||
if let Err(error) = self.master_process.kill() {
|
||||
log::error!("failed to kill SSH master process: {}", error);
|
||||
}
|
||||
|
||||
message_handlers.entities_by_type_and_remote_id.insert(
|
||||
id,
|
||||
EntityMessageSubscriber::Entity {
|
||||
handle: entity.downgrade().into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn request<T: RequestMessage>(
|
||||
&self,
|
||||
payload: T,
|
||||
) -> impl 'static + Future<Output = Result<T::Response>> {
|
||||
log::debug!("ssh request start. name:{}", T::NAME);
|
||||
let response = self.request_dynamic(payload.into_envelope(0, None, None), T::NAME);
|
||||
async move {
|
||||
let response = response.await?;
|
||||
log::debug!("ssh request finish. name:{}", T::NAME);
|
||||
T::Response::from_envelope(response)
|
||||
.ok_or_else(|| anyhow!("received a response of the wrong type"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send<T: EnvelopedMessage>(&self, payload: T) -> Result<()> {
|
||||
log::debug!("ssh send name:{}", T::NAME);
|
||||
self.send_dynamic(payload.into_envelope(0, None, None))
|
||||
}
|
||||
|
||||
pub fn request_dynamic(
|
||||
&self,
|
||||
mut envelope: proto::Envelope,
|
||||
type_name: &'static str,
|
||||
) -> impl 'static + Future<Output = Result<proto::Envelope>> {
|
||||
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let mut response_channels_lock = self.response_channels.lock();
|
||||
response_channels_lock.insert(MessageId(envelope.id), tx);
|
||||
drop(response_channels_lock);
|
||||
let result = self.outgoing_tx.unbounded_send(envelope);
|
||||
async move {
|
||||
if let Err(error) = &result {
|
||||
log::error!("failed to send message: {}", error);
|
||||
return Err(anyhow!("failed to send message: {}", error));
|
||||
}
|
||||
|
||||
let response = rx.await.context("connection lost")?.0;
|
||||
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
|
||||
return Err(RpcError::from_proto(error, type_name));
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||
self.outgoing_tx.unbounded_send(envelope)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoClient for ChannelClient {
|
||||
fn request(
|
||||
&self,
|
||||
envelope: proto::Envelope,
|
||||
request_type: &'static str,
|
||||
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
||||
self.request_dynamic(envelope, request_type).boxed()
|
||||
impl SshSocket {
|
||||
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
||||
let mut command = process::Command::new("ssh");
|
||||
self.ssh_options(&mut command)
|
||||
.arg(self.connection_options.ssh_url())
|
||||
.arg(program);
|
||||
command
|
||||
}
|
||||
|
||||
fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> {
|
||||
self.send_dynamic(envelope)
|
||||
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.args(["-o", "ControlMaster=no", "-o"])
|
||||
.arg(format!("ControlPath={}", self.socket_path.display()))
|
||||
}
|
||||
|
||||
fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> {
|
||||
self.send_dynamic(envelope)
|
||||
}
|
||||
|
||||
fn message_handler_set(&self) -> &Mutex<ProtoMessageHandlerSet> {
|
||||
&self.message_handlers
|
||||
}
|
||||
|
||||
fn is_via_collab(&self) -> bool {
|
||||
false
|
||||
fn ssh_args(&self) -> Vec<String> {
|
||||
vec![
|
||||
"-o".to_string(),
|
||||
"ControlMaster=no".to_string(),
|
||||
"-o".to_string(),
|
||||
format!("ControlPath={}", self.socket_path.display()),
|
||||
self.connection_options.ssh_url(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||
let output = command.output().await?;
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"failed to run command: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use project::{
|
||||
worktree_store::WorktreeStore,
|
||||
LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId,
|
||||
};
|
||||
use remote::ssh_session::ChannelClient;
|
||||
use remote::SshSession;
|
||||
use rpc::{
|
||||
proto::{self, SSH_PEER_ID, SSH_PROJECT_ID},
|
||||
AnyProtoClient, TypedEnvelope,
|
||||
@@ -41,7 +41,7 @@ impl HeadlessProject {
|
||||
project::Project::init_settings(cx);
|
||||
}
|
||||
|
||||
pub fn new(session: Arc<ChannelClient>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
@@ -6,6 +6,7 @@ use gpui::Context as _;
|
||||
use remote::{
|
||||
json_log::LogRecord,
|
||||
protocol::{read_message, write_message},
|
||||
SshSession,
|
||||
};
|
||||
use remote_server::HeadlessProject;
|
||||
use smol::{io::AsyncWriteExt, stream::StreamExt as _, Async};
|
||||
@@ -23,8 +24,6 @@ fn main() {
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn main() {
|
||||
use remote::ssh_session::ChannelClient;
|
||||
|
||||
env_logger::builder()
|
||||
.format(|buf, record| {
|
||||
serde_json::to_writer(&mut *buf, &LogRecord::new(record))?;
|
||||
@@ -56,7 +55,7 @@ fn main() {
|
||||
let mut stdin = Async::new(io::stdin()).unwrap();
|
||||
let mut stdout = Async::new(io::stdout()).unwrap();
|
||||
|
||||
let session = ChannelClient::new(incoming_rx, outgoing_tx, cx);
|
||||
let session = SshSession::server(incoming_rx, outgoing_tx, cx);
|
||||
let project = cx.new_model(|cx| {
|
||||
HeadlessProject::new(
|
||||
session.clone(),
|
||||
|
||||
@@ -15,7 +15,7 @@ use project::{
|
||||
search::{SearchQuery, SearchResult},
|
||||
Project, ProjectPath,
|
||||
};
|
||||
use remote::SshRemoteClient;
|
||||
use remote::SshSession;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsLocation, SettingsStore};
|
||||
use smol::stream::StreamExt;
|
||||
@@ -616,7 +616,7 @@ async fn init_test(
|
||||
cx: &mut TestAppContext,
|
||||
server_cx: &mut TestAppContext,
|
||||
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
|
||||
let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
|
||||
let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
|
||||
init_logger();
|
||||
|
||||
let fs = FakeFs::new(server_cx.executor());
|
||||
@@ -642,9 +642,8 @@ async fn init_test(
|
||||
);
|
||||
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let headless =
|
||||
server_cx.new_model(|cx| HeadlessProject::new(ssh_server_client, fs.clone(), cx));
|
||||
let project = build_project(ssh_remote_client, cx);
|
||||
let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
|
||||
let project = build_project(client_ssh, cx);
|
||||
|
||||
project
|
||||
.update(cx, {
|
||||
@@ -655,7 +654,7 @@ async fn init_test(
|
||||
(project, headless, fs)
|
||||
}
|
||||
|
||||
fn build_project(ssh: Arc<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
|
||||
fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
[package]
|
||||
name = "snippets_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/snippets_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,226 +0,0 @@
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Styled,
|
||||
View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use paths::config_dir;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{borrow::Borrow, fs, sync::Arc};
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing, WindowContext};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotifyResultExt, ModalView, Workspace};
|
||||
|
||||
actions!(snippets, [ConfigureSnippets, OpenFolder]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(register).detach();
|
||||
}
|
||||
|
||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
workspace.register_action(configure_snippets);
|
||||
workspace.register_action(open_folder);
|
||||
}
|
||||
|
||||
fn configure_snippets(
|
||||
workspace: &mut Workspace,
|
||||
_: &ConfigureSnippets,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let language_registry = workspace.app_state().languages.clone();
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
|
||||
workspace.toggle_modal(cx, move |cx| {
|
||||
ScopeSelector::new(language_registry, workspace_handle, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn open_folder(workspace: &mut Workspace, _: &OpenFolder, cx: &mut ViewContext<Workspace>) {
|
||||
fs::create_dir_all(config_dir().join("snippets")).notify_err(workspace, cx);
|
||||
cx.open_with_system(config_dir().join("snippets").borrow());
|
||||
}
|
||||
|
||||
pub struct ScopeSelector {
|
||||
picker: View<Picker<ScopeSelectorDelegate>>,
|
||||
}
|
||||
|
||||
impl ScopeSelector {
|
||||
fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate =
|
||||
ScopeSelectorDelegate::new(workspace, cx.view().downgrade(), language_registry);
|
||||
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ScopeSelector {}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ScopeSelector {}
|
||||
|
||||
impl FocusableView for ScopeSelector {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ScopeSelector {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScopeSelectorDelegate {
|
||||
workspace: WeakView<Workspace>,
|
||||
scope_selector: WeakView<ScopeSelector>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
candidates: Vec<StringMatchCandidate>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl ScopeSelectorDelegate {
|
||||
fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
scope_selector: WeakView<ScopeSelector>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) -> Self {
|
||||
let candidates = Vec::from(["Global".to_string()]).into_iter();
|
||||
let languages = language_registry.language_names().into_iter();
|
||||
|
||||
let candidates = candidates
|
||||
.chain(languages)
|
||||
.enumerate()
|
||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
workspace,
|
||||
scope_selector,
|
||||
language_registry,
|
||||
candidates,
|
||||
matches: vec![],
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ScopeSelectorDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _: &mut WindowContext) -> Arc<str> {
|
||||
"Select snippet scope...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||
let scope_name = self.candidates[mat.candidate_id].string.clone();
|
||||
let language = self.language_registry.language_for_name(&scope_name);
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let scope = match scope_name.as_str() {
|
||||
"Global" => "snippets".to_string(),
|
||||
_ => language.await?.lsp_id(),
|
||||
};
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_abs_path(
|
||||
config_dir().join("snippets").join(scope + ".json"),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
};
|
||||
}
|
||||
self.dismissed(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.scope_selector
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = self.candidates.clone();
|
||||
cx.spawn(|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,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let delegate = &mut this.delegate;
|
||||
delegate.matches = matches;
|
||||
delegate.selected_index = delegate
|
||||
.selected_index
|
||||
.min(delegate.matches.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches[ix];
|
||||
let label = mat.string.clone();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.selected(selected)
|
||||
.child(HighlightedLabel::new(label, mat.positions.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -520,10 +520,6 @@ pub fn reset_ui_font_size(cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_font_weight(weight: f32) -> FontWeight {
|
||||
FontWeight(weight.clamp(100., 950.))
|
||||
}
|
||||
|
||||
impl settings::Settings for ThemeSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
@@ -583,7 +579,7 @@ impl settings::Settings for ThemeSettings {
|
||||
this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value));
|
||||
}
|
||||
if let Some(value) = value.buffer_font_weight {
|
||||
this.buffer_font.weight = clamp_font_weight(value);
|
||||
this.buffer_font.weight = FontWeight(value);
|
||||
}
|
||||
|
||||
if let Some(value) = value.ui_font_family.clone() {
|
||||
@@ -596,7 +592,7 @@ impl settings::Settings for ThemeSettings {
|
||||
this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value));
|
||||
}
|
||||
if let Some(value) = value.ui_font_weight {
|
||||
this.ui_font.weight = clamp_font_weight(value);
|
||||
this.ui_font.weight = FontWeight(value);
|
||||
}
|
||||
|
||||
if let Some(value) = &value.theme {
|
||||
|
||||
@@ -36,7 +36,6 @@ pub struct ListItem {
|
||||
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
selectable: bool,
|
||||
overflow_x: bool,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
@@ -59,7 +58,6 @@ impl ListItem {
|
||||
tooltip: None,
|
||||
children: SmallVec::new(),
|
||||
selectable: true,
|
||||
overflow_x: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,11 +131,6 @@ impl ListItem {
|
||||
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn overflow_x(mut self) -> Self {
|
||||
self.overflow_x = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for ListItem {
|
||||
@@ -246,13 +239,7 @@ impl RenderOnce for ListItem {
|
||||
.flex_shrink_0()
|
||||
.flex_basis(relative(0.25))
|
||||
.gap(Spacing::Small.rems(cx))
|
||||
.map(|list_content| {
|
||||
if self.overflow_x {
|
||||
list_content
|
||||
} else {
|
||||
list_content.overflow_hidden()
|
||||
}
|
||||
})
|
||||
.overflow_hidden()
|
||||
.children(self.start_slot)
|
||||
.children(self.children),
|
||||
)
|
||||
|
||||
@@ -17,19 +17,19 @@ test-support = ["tempfile", "git2", "rand"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-fs.workspace = true
|
||||
collections.workspace = true
|
||||
dirs.workspace = true
|
||||
futures-lite.workspace = true
|
||||
futures.workspace = true
|
||||
git2 = { workspace = true, optional = true }
|
||||
globset.workspace = true
|
||||
log.workspace = true
|
||||
rand = { workspace = true, optional = true }
|
||||
rand = {workspace = true, optional = true}
|
||||
regex.workspace = true
|
||||
rust-embed.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
async-fs.workspace = true
|
||||
futures-lite.workspace = true
|
||||
take-until = "0.2.0"
|
||||
tempfile = { workspace = true, optional = true }
|
||||
unicase.workspace = true
|
||||
@@ -39,5 +39,5 @@ tendril = "0.4.3"
|
||||
|
||||
[dev-dependencies]
|
||||
git2.workspace = true
|
||||
rand.workspace = true
|
||||
tempfile.workspace = true
|
||||
rand.workspace = true
|
||||
|
||||
@@ -387,7 +387,7 @@ mod test {
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(false),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -432,9 +432,7 @@ mod test {
|
||||
request.next().await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.simulate_keystrokes("down enter");
|
||||
cx.run_until_parked();
|
||||
cx.simulate_keystrokes("! escape");
|
||||
cx.simulate_keystrokes("down enter ! escape");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
|
||||
@@ -1407,13 +1407,17 @@ impl Pane {
|
||||
self.pinned_tab_count -= 1;
|
||||
}
|
||||
if item_index == self.active_item_index {
|
||||
self.activation_history.pop();
|
||||
|
||||
let index_to_activate = if item_index + 1 < self.items.len() {
|
||||
item_index + 1
|
||||
} else {
|
||||
item_index.saturating_sub(1)
|
||||
};
|
||||
let index_to_activate = self
|
||||
.activation_history
|
||||
.pop()
|
||||
.and_then(|last_activated_item| {
|
||||
self.items.iter().enumerate().find_map(|(index, item)| {
|
||||
(item.item_id() == last_activated_item.entity_id).then_some(index)
|
||||
})
|
||||
})
|
||||
// We didn't have a valid activation history entry, so fallback
|
||||
// to activating the item to the left
|
||||
.unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1));
|
||||
|
||||
let should_activate = activate_pane || self.has_focus(cx);
|
||||
if self.items.len() == 1 && should_activate {
|
||||
@@ -3316,7 +3320,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_item_labels(&pane, ["A", "B", "C*", "D"], cx);
|
||||
assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
|
||||
|
||||
pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
|
||||
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
|
||||
@@ -3327,7 +3331,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
assert_item_labels(&pane, ["A", "B*", "C"], cx);
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
|
||||
@@ -3335,7 +3339,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_item_labels(&pane, ["A", "B*"], cx);
|
||||
assert_item_labels(&pane, ["A", "C*"], cx);
|
||||
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
|
||||
|
||||
@@ -61,7 +61,7 @@ use postage::stream::Stream;
|
||||
use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
};
|
||||
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||
use remote::{SshConnectionOptions, SshSession};
|
||||
use serde::Deserialize;
|
||||
use session::AppSession;
|
||||
use settings::{InvalidSettingsError, Settings};
|
||||
@@ -5514,7 +5514,7 @@ pub fn join_hosted_project(
|
||||
pub fn open_ssh_project(
|
||||
window: WindowHandle<Workspace>,
|
||||
connection_options: SshConnectionOptions,
|
||||
session: Arc<SshRemoteClient>,
|
||||
session: Arc<SshSession>,
|
||||
app_state: Arc<AppState>,
|
||||
paths: Vec<PathBuf>,
|
||||
cx: &mut AppContext,
|
||||
|
||||
@@ -48,12 +48,12 @@ text.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
clock = {workspace = true, features = ["test-support"]}
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
git2.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
gpui = {workspace = true, features = ["test-support"]}
|
||||
http_client.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
rand.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
settings = {workspace = true, features = ["test-support"]}
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -63,7 +63,7 @@ language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_selector.workspace = true
|
||||
language_tools.workspace = true
|
||||
languages = { workspace = true, features = ["load-grammars"] }
|
||||
languages = {workspace = true, features = ["load-grammars"] }
|
||||
libc.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
@@ -96,7 +96,6 @@ shellexpand.workspace = true
|
||||
simplelog.workspace = true
|
||||
smol.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
snippets_ui.workspace = true
|
||||
supermaven.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tab_switcher.workspace = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
@@ -256,7 +256,6 @@ fn init_ui(
|
||||
project_panel::init(Assets, cx);
|
||||
outline_panel::init(Assets, cx);
|
||||
tasks_ui::init(cx);
|
||||
snippets_ui::init(cx);
|
||||
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
search::init(cx);
|
||||
vim::init(cx);
|
||||
|
||||
@@ -33,7 +33,7 @@ Here's an example of language-specific settings:
|
||||
"Python": {
|
||||
"tab_size": 4,
|
||||
"formatter": "language_server",
|
||||
"format_on_save": "on"
|
||||
"format_on_save": true
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2,
|
||||
@@ -209,11 +209,11 @@ Zed supports both built-in and external formatters. Configure formatters globall
|
||||
"arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
}
|
||||
},
|
||||
"format_on_save": "on"
|
||||
"format_on_save": true
|
||||
},
|
||||
"Rust": {
|
||||
"formatter": "language_server",
|
||||
"format_on_save": "on"
|
||||
"format_on_save": true
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -225,7 +225,7 @@ To disable formatting for a specific language:
|
||||
```json
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"format_on_save": "off"
|
||||
"format_on_save": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -276,7 +276,7 @@ Zed allows you to run both formatting and linting on save. Here's an example tha
|
||||
"code_actions_on_format": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"format_on_save": "on"
|
||||
"format_on_save": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1736,7 +1736,7 @@ See Buffer Font Features
|
||||
|
||||
## Terminal: Detect Virtual Environments {#terminal-detect_venv}
|
||||
|
||||
- Description: Activate the [Python Virtual Environment](https://docs.python.org/3/library/venv.html), if one is found, in the terminal's working directory (as resolved by the working_directory and automatically activating the virtual environment.
|
||||
- Description: Activate the [Python Virtual Environment](https://docs.python.org/3/library/venv.html), if one is found, in the terminal's working directory (as resolved by the working_directory and automatically activating the virtual environemtn
|
||||
- Setting: `detect_venv`
|
||||
- Default:
|
||||
|
||||
@@ -1954,7 +1954,7 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||
"auto_reveal_entries": true,
|
||||
"auto_fold_dirs": true,
|
||||
"scrollbar": {
|
||||
"show": null
|
||||
"show": "always"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2074,13 +2074,13 @@ Run the `theme selector: toggle` action in the command palette to see a current
|
||||
|
||||
### Scrollbar
|
||||
|
||||
- Description: Scrollbar related settings. Possible values: null, "auto", "system", "always", "never". Inherits editor settings when absent, see its description for more details.
|
||||
- Description: Scrollbar related settings. Possible values: "always", "never".
|
||||
- Setting: `scrollbar`
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"scrollbar": {
|
||||
"show": null
|
||||
"show": "always"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -86,8 +86,6 @@ If you already have a published extension with the same name installed, your dev
|
||||
|
||||
To publish an extension, open a PR to [the `zed-industries/extensions` repo](https://github.com/zed-industries/extensions).
|
||||
|
||||
> Note: It is very helpful if you fork the `zed-industries/extensions` repo to a personal GitHub account instead of a GitHub organization, as this allows Zed staff to push any needed changes to your PR to expedite the publishing process.
|
||||
|
||||
In your PR, do the following:
|
||||
|
||||
1. Add your extension as a Git submodule within the `extensions/` directory
|
||||
|
||||
Reference in New Issue
Block a user