remote ssh: Make "get permalink to line" work (#19366)

This makes the `editor: copy permalink to line` and `editor: copy
permalink to line` actions work in SSH remote projects.

Previously it would only work in local projects.

Demo:


https://github.com/user-attachments/assets/a8012152-b631-4b34-9ff2-e4d033c97dee




Release Notes:

- N/A
This commit is contained in:
Thorsten Ball 2024-10-17 17:07:42 +02:00 committed by GitHub
parent c186e99a3d
commit 4be9da2641
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 219 additions and 79 deletions

3
Cargo.lock generated
View file

@ -8472,6 +8472,7 @@ dependencies = [
"terminal", "terminal",
"text", "text",
"unindent", "unindent",
"url",
"util", "util",
"which 6.0.3", "which 6.0.3",
"windows 0.58.0", "windows 0.58.0",
@ -9147,6 +9148,8 @@ dependencies = [
"env_logger", "env_logger",
"fs", "fs",
"futures 0.3.30", "futures 0.3.30",
"git",
"git_hosting_providers",
"gpui", "gpui",
"http_client", "http_client",
"language", "language",

View file

@ -48,7 +48,6 @@ mod signature_help;
pub mod test; pub mod test;
use ::git::diff::DiffHunkStatus; use ::git::diff::DiffHunkStatus;
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
pub(crate) use actions::*; pub(crate) use actions::*;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
@ -11488,11 +11487,8 @@ impl Editor {
snapshot.line_len(buffer_row) == 0 snapshot.line_len(buffer_row) == 0
} }
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> { fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<url::Url>> {
let (path, selection, repo) = maybe!({ let buffer_and_selection = maybe!({
let project_handle = self.project.as_ref()?.clone();
let project = project_handle.read(cx);
let selection = self.selections.newest::<Point>(cx); let selection = self.selections.newest::<Point>(cx);
let selection_range = selection.range(); let selection_range = selection.range();
@ -11516,64 +11512,58 @@ impl Editor {
(buffer.clone(), selection) (buffer.clone(), selection)
}; };
let path = buffer Some((buffer, selection))
.read(cx) });
.file()?
.as_local()? let Some((buffer, selection)) = buffer_and_selection else {
.path() return Task::ready(Err(anyhow!("failed to determine buffer and selection")));
.to_str()? };
.to_string();
let repo = project.get_repo(&buffer.read(cx).project_path(cx)?, cx)?; let Some(project) = self.project.as_ref() else {
Some((path, selection, repo)) 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<Self>) { pub fn copy_permalink_to_line(&mut self, _: &CopyPermalinkToLine, cx: &mut ViewContext<Self>) {
let permalink = self.get_permalink_to_line(cx); let permalink_task = self.get_permalink_to_line(cx);
let workspace = self.workspace();
match permalink { cx.spawn(|_, mut cx| async move {
Ok(permalink) => { match permalink_task.await {
cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); Ok(permalink) => {
} cx.update(|cx| {
Err(err) => { cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
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::<CopyPermalinkToLine>(), message),
cx,
)
}) })
.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::<CopyPermalinkToLine>(),
message,
),
cx,
)
})
.ok();
}
} }
} }
} })
.detach();
} }
pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext<Self>) { pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext<Self>) {
@ -11586,29 +11576,41 @@ impl Editor {
} }
pub fn open_permalink_to_line(&mut self, _: &OpenPermalinkToLine, cx: &mut ViewContext<Self>) { pub fn open_permalink_to_line(&mut self, _: &OpenPermalinkToLine, cx: &mut ViewContext<Self>) {
let permalink = self.get_permalink_to_line(cx); let permalink_task = self.get_permalink_to_line(cx);
let workspace = self.workspace();
match permalink { cx.spawn(|_, mut cx| async move {
Ok(permalink) => { match permalink_task.await {
cx.open_url(permalink.as_ref()); Ok(permalink) => {
} cx.update(|cx| {
Err(err) => { cx.open_url(permalink.as_ref());
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::<OpenPermalinkToLine>(), message),
cx,
)
}) })
.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::<OpenPermalinkToLine>(),
message,
),
cx,
)
})
.ok();
}
} }
} }
} })
.detach();
} }
/// Adds a row highlight for the given range. If a row has multiple highlights, the /// Adds a row highlight for the given range. If a row has multiple highlights, the

View file

@ -69,6 +69,7 @@ snippet_provider.workspace = true
terminal.workspace = true terminal.workspace = true
text.workspace = true text.workspace = true
util.workspace = true util.workspace = true
url.workspace = true
which.workspace = true which.workspace = true
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]

View file

@ -3,6 +3,7 @@ use crate::{
worktree_store::{WorktreeStore, WorktreeStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent},
Item, NoRepositoryError, ProjectPath, Item, NoRepositoryError, ProjectPath,
}; };
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use client::Client; use client::Client;
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
@ -23,7 +24,7 @@ use language::{
}; };
use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
use smol::channel::Receiver; 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 text::BufferId;
use util::{debug_panic, maybe, ResultExt as _, TryFutureExt}; use util::{debug_panic, maybe, ResultExt as _, TryFutureExt};
use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId}; 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_save_buffer);
client.add_model_request_handler(Self::handle_blame_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_reload_buffers);
client.add_model_request_handler(Self::handle_get_permalink_to_line);
} }
/// Creates a buffer store, optionally retaining its buffers. /// Creates a buffer store, optionally retaining its buffers.
@ -1170,6 +1172,78 @@ impl BufferStore {
} }
} }
pub fn get_permalink_to_line(
&self,
buffer: &Model<Buffer>,
selection: Range<u32>,
cx: &AppContext,
) -> Task<Result<url::Url>> {
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<Buffer>, cx: &mut ModelContext<Self>) -> Result<()> { fn add_buffer(&mut self, buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Result<()> {
let remote_id = buffer.read(cx).remote_id(); let remote_id = buffer.read(cx).remote_id();
let is_remote = buffer.read(cx).replica_id() != 0; let is_remote = buffer.read(cx).replica_id() != 0;
@ -1775,6 +1849,31 @@ impl BufferStore {
Ok(serialize_blame_buffer_response(blame)) Ok(serialize_blame_buffer_response(blame))
} }
pub async fn handle_get_permalink_to_line(
this: Model<Self>,
envelope: TypedEnvelope<proto::GetPermalinkToLine>,
mut cx: AsyncAppContext,
) -> Result<proto::GetPermalinkToLineResponse> {
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( pub async fn wait_for_loading_buffer(
mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>, mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
) -> Result<Model<Buffer>, Arc<anyhow::Error>> { ) -> Result<Model<Buffer>, Arc<anyhow::Error>> {

View file

@ -3463,6 +3463,17 @@ impl Project {
self.buffer_store.read(cx).blame_buffer(buffer, version, cx) self.buffer_store.read(cx).blame_buffer(buffer, version, cx)
} }
pub fn get_permalink_to_line(
&self,
buffer: &Model<Buffer>,
selection: Range<u32>,
cx: &AppContext,
) -> Task<Result<url::Url>> {
self.buffer_store
.read(cx)
.get_permalink_to_line(buffer, selection, cx)
}
// RPC message handlers // RPC message handlers
async fn handle_unshare_project( async fn handle_unshare_project(

View file

@ -292,7 +292,10 @@ message Envelope {
Toast toast = 261; Toast toast = 261;
HideToast hide_toast = 262; 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; reserved 87 to 88;
@ -2508,3 +2511,13 @@ message HideToast {
message OpenServerSettings { message OpenServerSettings {
uint64 project_id = 1; uint64 project_id = 1;
} }
message GetPermalinkToLine {
uint64 project_id = 1;
uint64 buffer_id = 2;
Range selection = 3;
}
message GetPermalinkToLineResponse {
string permalink = 1;
}

View file

@ -370,6 +370,8 @@ messages!(
(Toast, Background), (Toast, Background),
(HideToast, Background), (HideToast, Background),
(OpenServerSettings, Foreground), (OpenServerSettings, Foreground),
(GetPermalinkToLine, Foreground),
(GetPermalinkToLineResponse, Foreground),
); );
request_messages!( request_messages!(
@ -494,7 +496,8 @@ request_messages!(
(CheckFileExists, CheckFileExistsResponse), (CheckFileExists, CheckFileExistsResponse),
(ShutdownRemoteServer, Ack), (ShutdownRemoteServer, Ack),
(RemoveWorktree, Ack), (RemoveWorktree, Ack),
(OpenServerSettings, OpenBufferResponse) (OpenServerSettings, OpenBufferResponse),
(GetPermalinkToLine, GetPermalinkToLineResponse),
); );
entity_messages!( entity_messages!(
@ -571,7 +574,7 @@ entity_messages!(
Toast, Toast,
HideToast, HideToast,
OpenServerSettings, OpenServerSettings,
GetPermalinkToLine,
); );
entity_messages!( entity_messages!(

View file

@ -30,6 +30,8 @@ client.workspace = true
env_logger.workspace = true env_logger.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
git.workspace = true
git_hosting_providers.workspace = true
gpui.workspace = true gpui.workspace = true
http_client.workspace = true http_client.workspace = true
language.workspace = true language.workspace = true

View file

@ -5,6 +5,7 @@ use client::ProxySettings;
use fs::{Fs, RealFs}; use fs::{Fs, RealFs};
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt}; use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
use git::GitHostingProviderRegistry;
use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _}; use gpui::{AppContext, Context as _, ModelContext, UpdateGlobal as _};
use http_client::{read_proxy_from_env, Uri}; use http_client::{read_proxy_from_env, Uri};
use language::LanguageRegistry; use language::LanguageRegistry;
@ -313,6 +314,8 @@ pub fn execute_run(
let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?; let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?;
log::info!("starting headless gpui app"); log::info!("starting headless gpui app");
let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
gpui::App::headless().run(move |cx| { gpui::App::headless().run(move |cx| {
settings::init(cx); settings::init(cx);
HeadlessProject::init(cx); HeadlessProject::init(cx);
@ -322,6 +325,9 @@ pub fn execute_run(
client::init_settings(cx); client::init_settings(cx);
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
git_hosting_providers::init(cx);
let project = cx.new_model(|cx| { let project = cx.new_model(|cx| {
let fs = Arc::new(RealFs::new(Default::default(), None)); let fs = Arc::new(RealFs::new(Default::default(), None));
let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx); let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);