Detailed changes
@@ -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",
@@ -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<Self>) -> Result<url::Url> {
- 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<Self>) -> Task<Result<url::Url>> {
+ let buffer_and_selection = maybe!({
let selection = self.selections.newest::<Point>(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<Self>) {
- 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}");
+ 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();
+ Err::<(), anyhow::Error>(err).log_err();
- if let Some(workspace) = self.workspace() {
- workspace.update(cx, |workspace, cx| {
- struct CopyPermalinkToLine;
+ if let Some(workspace) = workspace {
+ workspace
+ .update(&mut cx, |workspace, cx| {
+ struct CopyPermalinkToLine;
- workspace.show_toast(
- Toast::new(NotificationId::unique::<CopyPermalinkToLine>(), message),
- cx,
- )
- })
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<CopyPermalinkToLine>(),
+ message,
+ ),
+ cx,
+ )
+ })
+ .ok();
+ }
}
}
- }
+ })
+ .detach();
}
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>) {
- 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}");
+ 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();
+ Err::<(), anyhow::Error>(err).log_err();
- if let Some(workspace) = self.workspace() {
- workspace.update(cx, |workspace, cx| {
- struct OpenPermalinkToLine;
+ if let Some(workspace) = workspace {
+ workspace
+ .update(&mut cx, |workspace, cx| {
+ struct OpenPermalinkToLine;
- workspace.show_toast(
- Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message),
- cx,
- )
- })
+ 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
@@ -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]
@@ -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<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<()> {
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<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(
mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
) -> Result<Model<Buffer>, Arc<anyhow::Error>> {
@@ -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<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
async fn handle_unshare_project(
@@ -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;
+}
@@ -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!(
@@ -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
@@ -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);