diff --git a/Cargo.lock b/Cargo.lock index 03d36c4445..aead615900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8472,6 +8472,7 @@ dependencies = [ "terminal", "text", "unindent", + "url", "util", "which 6.0.3", "windows 0.58.0", @@ -9147,6 +9148,8 @@ dependencies = [ "env_logger", "fs", "futures 0.3.30", + "git", + "git_hosting_providers", "gpui", "http_client", "language", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 378ccb905d..72205273f8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,7 +48,6 @@ mod signature_help; pub mod test; use ::git::diff::DiffHunkStatus; -use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; pub(crate) use actions::*; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; @@ -11488,11 +11487,8 @@ impl Editor { snapshot.line_len(buffer_row) == 0 } - fn get_permalink_to_line(&mut self, cx: &mut ViewContext) -> Result { - let (path, selection, repo) = maybe!({ - let project_handle = self.project.as_ref()?.clone(); - let project = project_handle.read(cx); - + fn get_permalink_to_line(&mut self, cx: &mut ViewContext) -> Task> { + let buffer_and_selection = maybe!({ let selection = self.selections.newest::(cx); let selection_range = selection.range(); @@ -11516,64 +11512,58 @@ impl Editor { (buffer.clone(), selection) }; - let path = buffer - .read(cx) - .file()? - .as_local()? - .path() - .to_str()? - .to_string(); - let repo = project.get_repo(&buffer.read(cx).project_path(cx)?, cx)?; - Some((path, selection, repo)) + Some((buffer, selection)) + }); + + let Some((buffer, selection)) = buffer_and_selection else { + return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); + }; + + let Some(project) = self.project.as_ref() else { + return Task::ready(Err(anyhow!("editor does not have project"))); + }; + + project.update(cx, |project, cx| { + project.get_permalink_to_line(&buffer, selection, cx) }) - .ok_or_else(|| anyhow!("unable to open git repository"))?; - - const REMOTE_NAME: &str = "origin"; - let origin_url = repo - .remote_url(REMOTE_NAME) - .ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?; - let sha = repo - .head_sha() - .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?; - - let (provider, remote) = - parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url) - .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; - - Ok(provider.build_permalink( - remote, - BuildPermalinkParams { - sha: &sha, - path: &path, - selection: Some(selection), - }, - )) } pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext) { - let permalink = self.get_permalink_to_line(cx); + let permalink_task = self.get_permalink_to_line(cx); + let workspace = self.workspace(); - match permalink { - Ok(permalink) => { - cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); - } - Err(err) => { - let message = format!("Failed to copy permalink: {err}"); - - Err::<(), anyhow::Error>(err).log_err(); - - if let Some(workspace) = self.workspace() { - workspace.update(cx, |workspace, cx| { - struct CopyPermalinkToLine; - - workspace.show_toast( - Toast::new(NotificationId::unique::(), message), - cx, - ) + cx.spawn(|_, mut cx| async move { + match permalink_task.await { + Ok(permalink) => { + cx.update(|cx| { + cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); }) + .ok(); + } + Err(err) => { + let message = format!("Failed to copy permalink: {err}"); + + Err::<(), anyhow::Error>(err).log_err(); + + if let Some(workspace) = workspace { + workspace + .update(&mut cx, |workspace, cx| { + struct CopyPermalinkToLine; + + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + message, + ), + cx, + ) + }) + .ok(); + } } } - } + }) + .detach(); } pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext) { @@ -11586,29 +11576,41 @@ impl Editor { } pub fn open_permalink_to_line(&mut self, _: &OpenPermalinkToLine, cx: &mut ViewContext) { - let permalink = self.get_permalink_to_line(cx); + let permalink_task = self.get_permalink_to_line(cx); + let workspace = self.workspace(); - match permalink { - Ok(permalink) => { - cx.open_url(permalink.as_ref()); - } - Err(err) => { - let message = format!("Failed to open permalink: {err}"); - - Err::<(), anyhow::Error>(err).log_err(); - - if let Some(workspace) = self.workspace() { - workspace.update(cx, |workspace, cx| { - struct OpenPermalinkToLine; - - workspace.show_toast( - Toast::new(NotificationId::unique::(), message), - cx, - ) + cx.spawn(|_, mut cx| async move { + match permalink_task.await { + Ok(permalink) => { + cx.update(|cx| { + cx.open_url(permalink.as_ref()); }) + .ok(); + } + Err(err) => { + let message = format!("Failed to open permalink: {err}"); + + Err::<(), anyhow::Error>(err).log_err(); + + if let Some(workspace) = workspace { + workspace + .update(&mut cx, |workspace, cx| { + struct OpenPermalinkToLine; + + workspace.show_toast( + Toast::new( + NotificationId::unique::(), + message, + ), + cx, + ) + }) + .ok(); + } } } - } + }) + .detach(); } /// Adds a row highlight for the given range. If a row has multiple highlights, the diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 1e7801e908..2a9bb82a35 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -69,6 +69,7 @@ snippet_provider.workspace = true terminal.workspace = true text.workspace = true util.workspace = true +url.workspace = true which.workspace = true [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index aa86a8f7e2..9f9e624d22 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -3,6 +3,7 @@ use crate::{ worktree_store::{WorktreeStore, WorktreeStoreEvent}, Item, NoRepositoryError, ProjectPath, }; +use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; use anyhow::{anyhow, Context as _, Result}; use client::Client; use collections::{hash_map, HashMap, HashSet}; @@ -23,7 +24,7 @@ use language::{ }; use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; use smol::channel::Receiver; -use std::{io, path::Path, str::FromStr as _, sync::Arc, time::Instant}; +use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant}; use text::BufferId; use util::{debug_panic, maybe, ResultExt as _, TryFutureExt}; use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId}; @@ -971,6 +972,7 @@ impl BufferStore { client.add_model_request_handler(Self::handle_save_buffer); client.add_model_request_handler(Self::handle_blame_buffer); client.add_model_request_handler(Self::handle_reload_buffers); + client.add_model_request_handler(Self::handle_get_permalink_to_line); } /// Creates a buffer store, optionally retaining its buffers. @@ -1170,6 +1172,78 @@ impl BufferStore { } } + pub fn get_permalink_to_line( + &self, + buffer: &Model, + selection: Range, + cx: &AppContext, + ) -> Task> { + let buffer = buffer.read(cx); + let Some(file) = File::from_dyn(buffer.file()) else { + return Task::ready(Err(anyhow!("buffer has no file"))); + }; + + match file.worktree.clone().read(cx) { + Worktree::Local(worktree) => { + let Some(repo) = worktree.local_git_repo(file.path()) else { + return Task::ready(Err(anyhow!("no repository for buffer found"))); + }; + + let path = file.path().clone(); + + cx.spawn(|cx| async move { + const REMOTE_NAME: &str = "origin"; + let origin_url = repo + .remote_url(REMOTE_NAME) + .ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?; + + let sha = repo + .head_sha() + .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?; + + let provider_registry = + cx.update(GitHostingProviderRegistry::default_global)?; + + let (provider, remote) = + parse_git_remote_url(provider_registry, &origin_url) + .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?; + + let path = path + .to_str() + .context("failed to convert buffer path to string")?; + + Ok(provider.build_permalink( + remote, + BuildPermalinkParams { + sha: &sha, + path, + selection: Some(selection), + }, + )) + }) + } + Worktree::Remote(worktree) => { + let buffer_id = buffer.remote_id(); + let project_id = worktree.project_id(); + let client = worktree.client(); + cx.spawn(|_| async move { + let response = client + .request(proto::GetPermalinkToLine { + project_id, + buffer_id: buffer_id.into(), + selection: Some(proto::Range { + start: selection.start as u64, + end: selection.end as u64, + }), + }) + .await?; + + url::Url::parse(&response.permalink).context("failed to parse permalink") + }) + } + } + } + fn add_buffer(&mut self, buffer: Model, cx: &mut ModelContext) -> Result<()> { let remote_id = buffer.read(cx).remote_id(); let is_remote = buffer.read(cx).replica_id() != 0; @@ -1775,6 +1849,31 @@ impl BufferStore { Ok(serialize_blame_buffer_response(blame)) } + pub async fn handle_get_permalink_to_line( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + // let version = deserialize_version(&envelope.payload.version); + let selection = { + let proto_selection = envelope + .payload + .selection + .context("no selection to get permalink for defined")?; + proto_selection.start as u32..proto_selection.end as u32 + }; + let buffer = this.read_with(&cx, |this, _| this.get_existing(buffer_id))??; + let permalink = this + .update(&mut cx, |this, cx| { + this.get_permalink_to_line(&buffer, selection, cx) + })? + .await?; + Ok(proto::GetPermalinkToLineResponse { + permalink: permalink.to_string(), + }) + } + pub async fn wait_for_loading_buffer( mut receiver: postage::watch::Receiver, Arc>>>, ) -> Result, Arc> { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 03074fb6a6..70d5962647 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3463,6 +3463,17 @@ impl Project { self.buffer_store.read(cx).blame_buffer(buffer, version, cx) } + pub fn get_permalink_to_line( + &self, + buffer: &Model, + selection: Range, + cx: &AppContext, + ) -> Task> { + self.buffer_store + .read(cx) + .get_permalink_to_line(buffer, selection, cx) + } + // RPC message handlers async fn handle_unshare_project( diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 822ccf6cf7..09891de6fe 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -292,7 +292,10 @@ message Envelope { Toast toast = 261; HideToast hide_toast = 262; - OpenServerSettings open_server_settings = 263; // current max + OpenServerSettings open_server_settings = 263; + + GetPermalinkToLine get_permalink_to_line = 264; + GetPermalinkToLineResponse get_permalink_to_line_response = 265; // current max } reserved 87 to 88; @@ -2508,3 +2511,13 @@ message HideToast { message OpenServerSettings { uint64 project_id = 1; } + +message GetPermalinkToLine { + uint64 project_id = 1; + uint64 buffer_id = 2; + Range selection = 3; +} + +message GetPermalinkToLineResponse { + string permalink = 1; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 8455439980..ffbbeb49c2 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -370,6 +370,8 @@ messages!( (Toast, Background), (HideToast, Background), (OpenServerSettings, Foreground), + (GetPermalinkToLine, Foreground), + (GetPermalinkToLineResponse, Foreground), ); request_messages!( @@ -494,7 +496,8 @@ request_messages!( (CheckFileExists, CheckFileExistsResponse), (ShutdownRemoteServer, Ack), (RemoveWorktree, Ack), - (OpenServerSettings, OpenBufferResponse) + (OpenServerSettings, OpenBufferResponse), + (GetPermalinkToLine, GetPermalinkToLineResponse), ); entity_messages!( @@ -571,7 +574,7 @@ entity_messages!( Toast, HideToast, OpenServerSettings, - + GetPermalinkToLine, ); entity_messages!( diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 6ba2c5c7c9..ffec42e570 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -30,6 +30,8 @@ client.workspace = true env_logger.workspace = true fs.workspace = true futures.workspace = true +git.workspace = true +git_hosting_providers.workspace = true gpui.workspace = true http_client.workspace = true language.workspace = true diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 74198b1891..60b7fc458d 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -5,6 +5,7 @@ use client::ProxySettings; use fs::{Fs, RealFs}; use futures::channel::mpsc; use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt}; +use git::GitHostingProviderRegistry; use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _}; use http_client::{read_proxy_from_env, Uri}; use language::LanguageRegistry; @@ -313,6 +314,8 @@ pub fn execute_run( let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?; log::info!("starting headless gpui app"); + + let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); gpui::App::headless().run(move |cx| { settings::init(cx); HeadlessProject::init(cx); @@ -322,6 +325,9 @@ pub fn execute_run( client::init_settings(cx); + GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); + git_hosting_providers::init(cx); + let project = cx.new_model(|cx| { let fs = Arc::new(RealFs::new(Default::default(), None)); let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);