feat: copy note git URLs

This commit is contained in:
lucaronin
2026-06-06 14:24:51 +02:00
parent 3a88724d8f
commit 191066a3bf
39 changed files with 853 additions and 7 deletions

View File

@@ -516,6 +516,7 @@ interface PulseCommit {
| `history.rs` | File history | `git log` — last 20 commits per file | | `history.rs` | File history | `git log` — last 20 commits per file |
| `status.rs` | Modified files | `git status --porcelain` — filtered to `.md` | | `status.rs` | Modified files | `git status --porcelain` — filtered to `.md` |
| `status.rs` | File diff | `git diff`, fallback to `--cached`, then synthetic for untracked | | `status.rs` | File diff | `git diff`, fallback to `--cached`, then synthetic for untracked |
| `file_url.rs` | File URL | Builds a copyable remote URL from the primary remote, current branch, and vault-relative path without exposing remote credentials |
| `commit.rs` | Commit | Ensures a local author fallback when needed, then runs `git add -A && git commit -m "..."`; broken signing helpers trigger one unsigned retry for the same app-managed commit | | `commit.rs` | Commit | Ensures a local author fallback when needed, then runs `git add -A && git commit -m "..."`; broken signing helpers trigger one unsigned retry for the same app-managed commit |
| `remote.rs` | Pull / Push | `git pull --rebase` / `git push` | | `remote.rs` | Pull / Push | `git pull --rebase` / `git push` |
| `connect.rs` | Add remote | Adds `origin`, fetches it, validates history compatibility, and only starts tracking when the remote is safe | | `connect.rs` | Add remote | Adds `origin`, fetches it, validates history compatibility, and only starts tracking when the remote is safe |

View File

@@ -782,6 +782,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `git_pull` | Pull from remote | | `git_pull` | Pull from remote |
| `git_push` | Push to remote | | `git_push` | Push to remote |
| `git_remote_status` | Get branch name + ahead/behind counts | | `git_remote_status` | Get branch name + ahead/behind counts |
| `git_file_url` | Build a remote-backed browser/git URL for a vault file |
| `git_add_remote` | Connect a local-only vault to a compatible remote and start tracking it | | `git_add_remote` | Connect a local-only vault to a compatible remote and start tracking it |
| `git_resolve_conflict` | Resolve a merge conflict | | `git_resolve_conflict` | Resolve a merge conflict |
| `git_commit_conflict_resolution` | Commit conflict resolution | | `git_commit_conflict_resolution` | Commit conflict resolution |
@@ -921,7 +922,7 @@ Data flows unidirectionally: `App` passes data and callbacks as props to child c
| Cmd+[ / Cmd+] | Navigate back / forward | | Cmd+[ / Cmd+] | Navigate back / forward |
| `[[` in editor | Open wikilink suggestion menu | | `[[` in editor | Open wikilink suggestion menu |
Selection-dependent actions are wired through the command palette and the native menus. For example, a deleted file opened from Changes view becomes a read-only diff preview, and that state enables the "Restore Deleted Note" menu/command while normal note mutation actions stay disabled. Folder selection follows the same pattern: when `selection.kind === 'folder'`, the command palette exposes "Reveal Folder in Finder", "Copy Folder Path", "Rename Folder", and "Delete Folder", and the sidebar row can launch the same flows directly through inline rename or the folder context menu. Active files also expose "Reveal in Finder" and "Copy File Path" through the command palette; non-Markdown file tabs additionally expose "Open in Default App", matching the `FilePreview` header controls. Markdown notes expose "Export note as PDF" from Cmd+K, the native Note menu, the breadcrumb overflow menu, and the note-list context menu, all routed through the same editor export hook. Active notes now follow the same shared-action model for retargeting: Cmd+K can open "Change Note Type…" and "Move Note to Folder…", and the sidebar drop targets call the same hook-backed implementations instead of maintaining separate mutation paths. Selection-dependent actions are wired through the command palette and the native menus. For example, a deleted file opened from Changes view becomes a read-only diff preview, and that state enables the "Restore Deleted Note" menu/command while normal note mutation actions stay disabled. Folder selection follows the same pattern: when `selection.kind === 'folder'`, the command palette exposes "Reveal Folder in Finder", "Copy Folder Path", "Rename Folder", and "Delete Folder", and the sidebar row can launch the same flows directly through inline rename or the folder context menu. Active files also expose "Reveal in Finder" and "Copy File Path" through the command palette; non-Markdown file tabs additionally expose "Open in Default App", matching the `FilePreview` header controls. Markdown notes expose "Export note as PDF" from Cmd+K, the native Note menu, the breadcrumb overflow menu, and the note-list context menu, all routed through the same editor export hook. Remote-backed notes expose "Copy git URL" from the breadcrumb overflow and note-list context menu; the renderer gates the action per note workspace via `git_remote_status`, then asks `git_file_url` to build the copied URL from the primary remote, current branch, and vault-relative path. Active notes now follow the same shared-action model for retargeting: Cmd+K can open "Change Note Type…" and "Move Note to Folder…", and the sidebar drop targets call the same hook-backed implementations instead of maintaining separate mutation paths.
Shortcut routing is explicit: Shortcut routing is explicit:

View File

@@ -588,6 +588,7 @@ files:
editor.toolbar.revealFile: 76f4ed52da9937ae432dcf5a108fabb4 editor.toolbar.revealFile: 76f4ed52da9937ae432dcf5a108fabb4
editor.toolbar.copyFilePath: 64c6cec5ca57efbb8c9c93ae5fbccd27 editor.toolbar.copyFilePath: 64c6cec5ca57efbb8c9c93ae5fbccd27
editor.toolbar.copyNoteDeepLink: 0fff274a7c47e7c12457c0b7412e86ce editor.toolbar.copyNoteDeepLink: 0fff274a7c47e7c12457c0b7412e86ce
editor.toolbar.copyNoteGitUrl: 04a732b98f7585e3c3b5474688e0baac
editor.toolbar.exportPdf: 7a39acf8742a59d1a4f7ae064b6219a0 editor.toolbar.exportPdf: 7a39acf8742a59d1a4f7ae064b6219a0
editor.toolbar.moreActions: 89e19353176b94068dec4c768c25e70b editor.toolbar.moreActions: 89e19353176b94068dec4c768c25e70b
editor.toolbar.openProperties: 948b90b60b8ce827f179e8c5b85b112e editor.toolbar.openProperties: 948b90b60b8ce827f179e8c5b85b112e
@@ -611,6 +612,9 @@ files:
deepLinks.error.unavailableVault: 911dfaf8e855bd8cadefe7df9a98a7e8 deepLinks.error.unavailableVault: 911dfaf8e855bd8cadefe7df9a98a7e8
deepLinks.error.unknownVault: b4940569e884833f50466177dab1f62f deepLinks.error.unknownVault: b4940569e884833f50466177dab1f62f
deepLinks.error.unsafePath: e09caab43be8c2372bfc94e947ea34e6 deepLinks.error.unsafePath: e09caab43be8c2372bfc94e947ea34e6
noteGitUrls.copied: 6832bf85abaa03a6304cd1cce073d747
noteGitUrls.error.copyFailed: 1952ee267f473efe8e28ca90659f75f6
noteGitUrls.error.unavailable: ed08c134bba26350fd28f780c6f1b55b
editor.slash.math: a49950aa047c2292e989e368a97a3aae editor.slash.math: a49950aa047c2292e989e368a97a3aae
tableOfContents.title: f61d6c3e3733db355168b7e1aee6780b tableOfContents.title: f61d6c3e3733db355168b7e1aee6780b
tableOfContents.close: d3292972a10edd1a06a627f3552f760a tableOfContents.close: d3292972a10edd1a06a627f3552f760a

View File

@@ -150,6 +150,19 @@ pub async fn git_remote_status(vault_path: VaultPathArg) -> Result<GitRemoteStat
.map_err(|e| format!("Task panicked: {e}"))? .map_err(|e| format!("Task panicked: {e}"))?
} }
#[cfg(desktop)]
#[tauri::command]
pub async fn git_file_url(
vault_path: VaultPathArg,
path: NotePathArg,
) -> Result<Option<String>, String> {
let vault_path = expand_tilde(&vault_path).into_owned();
let path = expand_tilde(&path).into_owned();
tokio::task::spawn_blocking(move || crate::git::git_file_url(&vault_path, &path))
.await
.map_err(|e| format!("Task panicked: {e}"))?
}
#[cfg(desktop)] #[cfg(desktop)]
#[tauri::command] #[tauri::command]
pub fn git_discard_file( pub fn git_discard_file(
@@ -344,6 +357,15 @@ pub async fn git_remote_status(_vault_path: VaultPathArg) -> Result<GitRemoteSta
}) })
} }
#[cfg(mobile)]
#[tauri::command]
pub async fn git_file_url(
_vault_path: VaultPathArg,
_path: NotePathArg,
) -> Result<Option<String>, String> {
Ok(None)
}
#[cfg(mobile)] #[cfg(mobile)]
#[tauri::command] #[tauri::command]
pub fn git_discard_file( pub fn git_discard_file(

View File

@@ -0,0 +1,403 @@
use std::path::Path;
use crate::vault::path_identity::vault_relative_path_string;
use super::command::{git_output, stderr_or_failure, stdout_text};
use super::remote_config::primary_remote_url;
enum RemoteWebKind {
Bitbucket,
Gitea,
GitLab,
Generic,
}
struct RemoteWebBase {
base_url: String,
kind: RemoteWebKind,
}
struct GitFileLocation {
branch: BranchName,
relative_path: RelativeGitPath,
}
struct BranchName(String);
struct RelativeGitPath(String);
struct RemoteUrl(String);
struct RemoteParts {
host: RemoteHost,
repo_path: RepoPath,
}
struct RemoteHost(String);
struct RepoPath(String);
pub fn git_file_url(vault_path: &str, file_path: &str) -> Result<Option<String>, String> {
let vault = Path::new(vault_path);
let file = Path::new(file_path);
let Some(relative_path) = RelativeGitPath::from_paths(vault, file)? else {
return Ok(None);
};
let Some(remote_url) = primary_remote_url(vault)?.map(RemoteUrl::new) else {
return Ok(None);
};
let location = GitFileLocation::new(current_ref_name(vault)?, relative_path);
let url = match remote_web_base(&remote_url) {
Some(remote) => remote.file_url(&location),
None => remote_url.git_fragment_url(&location),
};
Ok(Some(url))
}
fn current_ref_name(vault: &Path) -> Result<BranchName, String> {
let output = git_output(vault, &["branch", "--show-current"])
.map_err(|e| format!("Failed to get branch: {e}"))?;
if !output.status.success() {
return Err(stderr_or_failure("git branch", &output));
}
let branch = stdout_text(&output);
Ok(BranchName::new(branch))
}
impl GitFileLocation {
fn new(branch: BranchName, relative_path: RelativeGitPath) -> Self {
Self {
branch,
relative_path,
}
}
}
impl BranchName {
fn new(value: String) -> Self {
if value.is_empty() {
return Self("HEAD".to_string());
}
Self(value)
}
fn encoded_fragment(&self) -> String {
encode_fragment_part(&self.0)
}
fn encoded_path(&self) -> String {
encode_path(&self.0)
}
}
impl RelativeGitPath {
fn from_paths(vault: &Path, file: &Path) -> Result<Option<Self>, String> {
let value = vault_relative_path_string(vault, file)?;
if value.is_empty() {
return Ok(None);
}
Ok(Some(Self(value)))
}
fn encoded_fragment(&self) -> String {
encode_fragment_part(&self.0)
}
fn encoded_path(&self) -> String {
encode_path(&self.0)
}
}
impl RemoteWebBase {
fn file_url(&self, location: &GitFileLocation) -> String {
let branch = location.branch.encoded_path();
let path = location.relative_path.encoded_path();
match self.kind {
RemoteWebKind::Bitbucket => format!("{}/src/{}/{}", self.base_url, branch, path),
RemoteWebKind::Gitea => format!("{}/src/branch/{}/{}", self.base_url, branch, path),
RemoteWebKind::GitLab => format!("{}/-/blob/{}/{}", self.base_url, branch, path),
RemoteWebKind::Generic => format!("{}/blob/{}/{}", self.base_url, branch, path),
}
}
}
impl RemoteUrl {
fn new(value: String) -> Self {
Self(value)
}
fn trimmed(&self) -> &str {
self.0.trim()
}
fn git_fragment_url(&self, location: &GitFileLocation) -> String {
format!(
"{}#{}:{}",
self.trimmed(),
location.branch.encoded_fragment(),
location.relative_path.encoded_fragment(),
)
}
fn host_and_path(&self) -> Option<RemoteParts> {
self.http_parts()
.or_else(|| self.scheme_parts())
.or_else(|| self.scp_parts())
}
fn http_parts(&self) -> Option<RemoteParts> {
let rest = self
.trimmed()
.strip_prefix("https://")
.or_else(|| self.trimmed().strip_prefix("http://"))?;
RemoteParts::from_authority_path(rest)
}
fn scheme_parts(&self) -> Option<RemoteParts> {
let rest = self
.trimmed()
.strip_prefix("ssh://")
.or_else(|| self.trimmed().strip_prefix("git://"))?;
RemoteParts::from_authority_path(rest)
}
fn scp_parts(&self) -> Option<RemoteParts> {
let (_, target) = self.trimmed().split_once('@')?;
let (host, path) = target.split_once(':')?;
Some(RemoteParts::new(RemoteHost::new(host), RepoPath::new(path)))
}
}
impl RemoteParts {
fn new(host: RemoteHost, repo_path: RepoPath) -> Self {
Self { host, repo_path }
}
fn from_authority_path(value: &str) -> Option<Self> {
let (authority, path) = value.split_once('/')?;
Some(Self::new(
RemoteHost::from_authority(authority),
RepoPath::new(path),
))
}
fn into_web_base(self) -> Option<RemoteWebBase> {
let clean_path = self.repo_path.clean();
if self.host.is_empty() {
return None;
}
if clean_path.is_empty() {
return None;
}
let base_url = format!("https://{}/{clean_path}", self.host.as_str());
Some(RemoteWebBase {
kind: remote_web_kind(&self.host),
base_url,
})
}
}
impl RemoteHost {
fn new(value: &str) -> Self {
Self(value.to_string())
}
fn from_authority(authority: &str) -> Self {
Self::new(authority.rsplit('@').next().unwrap_or_default())
}
fn as_str(&self) -> &str {
&self.0
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn lower(&self) -> String {
self.0.to_ascii_lowercase()
}
}
impl RepoPath {
fn new(value: &str) -> Self {
Self(value.to_string())
}
fn clean(&self) -> &str {
let trimmed = self.0.trim_matches('/');
trimmed.strip_suffix(".git").unwrap_or(trimmed)
}
}
fn remote_web_base(remote_url: &RemoteUrl) -> Option<RemoteWebBase> {
remote_url.host_and_path()?.into_web_base()
}
fn remote_web_kind(host: &RemoteHost) -> RemoteWebKind {
let lower_host = host.lower();
if lower_host.contains("gitlab") {
return RemoteWebKind::GitLab;
}
if lower_host.contains("bitbucket") {
return RemoteWebKind::Bitbucket;
}
if lower_host.contains("gitea") {
return RemoteWebKind::Gitea;
}
if lower_host.contains("forgejo") {
return RemoteWebKind::Gitea;
}
if lower_host == "codeberg.org" {
return RemoteWebKind::Gitea;
}
RemoteWebKind::Generic
}
fn encode_path(path: &str) -> String {
path.split('/')
.map(encode_segment)
.collect::<Vec<_>>()
.join("/")
}
fn encode_fragment_part(value: &str) -> String {
let mut encoded = String::new();
for byte in value.bytes() {
match byte {
b' ' => encoded.push_str("%20"),
b'#' => encoded.push_str("%23"),
b'%' => encoded.push_str("%25"),
_ => encoded.push(char::from(byte)),
}
}
encoded
}
fn encode_segment(segment: &str) -> String {
segment
.bytes()
.flat_map(|byte| {
if is_unreserved_url_byte(byte) {
vec![byte]
} else {
format!("%{byte:02X}").into_bytes()
}
})
.map(char::from)
.collect()
}
fn is_unreserved_url_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::git_command;
use crate::git::tests::setup_git_repo;
use std::fs;
use std::path::Path;
struct RemoteFixture {
name: &'static str,
url: &'static str,
}
struct GitUrlCase {
remote: RemoteFixture,
note_path: &'static str,
expected_url: &'static str,
}
fn add_remote(vault: &Path, remote: RemoteFixture) {
git_command()
.args(["remote", "add", remote.name, remote.url])
.current_dir(vault)
.output()
.unwrap();
}
fn write_note(vault: &Path, relative_path: &str) -> String {
let file = vault.join(relative_path);
fs::create_dir_all(file.parent().unwrap()).unwrap();
fs::write(&file, "# Note\n").unwrap();
file.to_string_lossy().to_string()
}
fn assert_git_file_url(test_case: GitUrlCase) {
let dir = setup_git_repo();
let note = write_note(dir.path(), test_case.note_path);
add_remote(dir.path(), test_case.remote);
let url = git_file_url(dir.path().to_str().unwrap(), &note).unwrap();
assert_eq!(url.as_deref(), Some(test_case.expected_url));
}
#[test]
fn returns_none_without_remote() {
let dir = setup_git_repo();
let note = write_note(dir.path(), "note.md");
let url = git_file_url(dir.path().to_str().unwrap(), &note).unwrap();
assert_eq!(url, None);
}
#[test]
fn returns_none_outside_git_repository() {
let dir = tempfile::tempdir().unwrap();
let note = write_note(dir.path(), "note.md");
let url = git_file_url(dir.path().to_str().unwrap(), &note).unwrap();
assert_eq!(url, None);
}
#[test]
fn builds_remote_note_urls() {
[
GitUrlCase {
remote: RemoteFixture {
name: "origin",
url: "git@github.com:owner/repo.git",
},
note_path: "Notes/Project Plan.md",
expected_url: "https://github.com/owner/repo/blob/main/Notes/Project%20Plan.md",
},
GitUrlCase {
remote: RemoteFixture {
name: "origin",
url: "https://gho_secret@github.com/owner/repo.git",
},
note_path: "private.md",
expected_url: "https://github.com/owner/repo/blob/main/private.md",
},
GitUrlCase {
remote: RemoteFixture {
name: "origin",
url: "https://gitlab.com/group/repo.git",
},
note_path: "notes/topic.md",
expected_url: "https://gitlab.com/group/repo/-/blob/main/notes/topic.md",
},
GitUrlCase {
remote: RemoteFixture {
name: "upstream",
url: "https://github.com/team/vault.git",
},
note_path: "shared.md",
expected_url: "https://github.com/team/vault/blob/main/shared.md",
},
]
.into_iter()
.for_each(assert_git_file_url);
}
}

View File

@@ -5,6 +5,7 @@ mod conflict;
mod connect; mod connect;
mod credentials; mod credentials;
mod dates; mod dates;
mod file_url;
mod history; mod history;
mod pulse; mod pulse;
mod remote; mod remote;
@@ -27,6 +28,7 @@ pub use conflict::{
}; };
pub use connect::{disconnect_all_remotes, git_add_remote, GitAddRemoteResult}; pub use connect::{disconnect_all_remotes, git_add_remote, GitAddRemoteResult};
pub use dates::{get_all_file_dates, GitDates}; pub use dates::{get_all_file_dates, GitDates};
pub use file_url::git_file_url;
pub use history::{get_file_diff, get_file_diff_at_commit, get_file_history}; pub use history::{get_file_diff, get_file_diff_at_commit, get_file_history};
pub use pulse::{get_last_commit_info, get_vault_pulse, LastCommitInfo, PulseCommit, PulseFile}; pub use pulse::{get_last_commit_info, get_vault_pulse, LastCommitInfo, PulseCommit, PulseFile};
pub use remote::{ pub use remote::{

View File

@@ -7,11 +7,33 @@ const REMOTE_URL_CONFIG_PATTERN: &str = r"^remote\..*\.url$";
const ORIGIN_URL_CONFIG_KEY: &str = "remote.origin.url"; const ORIGIN_URL_CONFIG_KEY: &str = "remote.origin.url";
const ORIGIN_FETCH_CONFIG_KEY: &str = "remote.origin.fetch"; const ORIGIN_FETCH_CONFIG_KEY: &str = "remote.origin.fetch";
#[derive(Debug, Clone, PartialEq, Eq)]
struct ConfiguredRemote {
name: String,
url: String,
}
pub(super) fn has_configured_remote(vault: &Path) -> Result<bool, String> { pub(super) fn has_configured_remote(vault: &Path) -> Result<bool, String> {
Ok(!list_configured_remotes(vault)?.is_empty()) Ok(!list_configured_remotes(vault)?.is_empty())
} }
pub(super) fn list_configured_remotes(vault: &Path) -> Result<Vec<String>, String> { pub(super) fn list_configured_remotes(vault: &Path) -> Result<Vec<String>, String> {
Ok(list_configured_remote_urls(vault)?
.into_iter()
.map(|remote| remote.name)
.collect())
}
pub(super) fn primary_remote_url(vault: &Path) -> Result<Option<String>, String> {
let remotes = list_configured_remote_urls(vault)?;
Ok(remotes
.iter()
.find(|remote| remote.name == "origin")
.or_else(|| remotes.first())
.map(|remote| remote.url.clone()))
}
fn list_configured_remote_urls(vault: &Path) -> Result<Vec<ConfiguredRemote>, String> {
let output = git_output( let output = git_output(
vault, vault,
&["config", "--get-regexp", REMOTE_URL_CONFIG_PATTERN], &["config", "--get-regexp", REMOTE_URL_CONFIG_PATTERN],
@@ -27,19 +49,26 @@ pub(super) fn list_configured_remotes(vault: &Path) -> Result<Vec<String>, Strin
Ok(stdout_lines(&output) Ok(stdout_lines(&output)
.into_iter() .into_iter()
.filter_map(|line| remote_name_from_url_config(&line)) .filter_map(|line| remote_from_url_config(&line))
.collect()) .collect())
} }
fn remote_name_from_url_config(line: &str) -> Option<String> { fn remote_from_url_config(line: &str) -> Option<ConfiguredRemote> {
let (key, value) = line.split_once(' ')?; let (key, value) = line.split_once(' ')?;
if value.trim().is_empty() { let url = value.trim();
if url.is_empty() {
return None; return None;
} }
key.strip_prefix("remote.") let name = key
.strip_prefix("remote.")
.and_then(|name| name.strip_suffix(".url")) .and_then(|name| name.strip_suffix(".url"))
.filter(|name| !name.is_empty()) .filter(|name| !name.is_empty())
.map(ToString::to_string) .map(ToString::to_string)?;
Some(ConfiguredRemote {
name,
url: url.to_string(),
})
} }
pub(super) fn configure_origin_remote(vault: &Path, remote_url: &str) -> Result<(), String> { pub(super) fn configure_origin_remote(vault: &Path, remote_url: &str) -> Result<(), String> {

View File

@@ -501,6 +501,7 @@ macro_rules! app_invoke_handler {
commands::git_pull, commands::git_pull,
commands::git_push, commands::git_push,
commands::git_remote_status, commands::git_remote_status,
commands::git_file_url,
commands::git_add_remote, commands::git_add_remote,
commands::get_conflict_files, commands::get_conflict_files,
commands::get_conflict_mode, commands::get_conflict_mode,

View File

@@ -53,6 +53,7 @@ import { useDeleteActions } from './hooks/useDeleteActions'
import { useFolderActions } from './hooks/useFolderActions' import { useFolderActions } from './hooks/useFolderActions'
import { useFileActions } from './hooks/useFileActions' import { useFileActions } from './hooks/useFileActions'
import { useDeepLinks } from './hooks/useDeepLinks' import { useDeepLinks } from './hooks/useDeepLinks'
import { useNoteGitUrls } from './hooks/useNoteGitUrls'
import { useLayoutPanels } from './hooks/useLayoutPanels' import { useLayoutPanels } from './hooks/useLayoutPanels'
import { useConflictFlow } from './hooks/useConflictFlow' import { useConflictFlow } from './hooks/useConflictFlow'
import { useAppSave } from './hooks/useAppSave' import { useAppSave } from './hooks/useAppSave'
@@ -1379,6 +1380,12 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
vaults: vaultSwitcher.allVaults, vaults: vaultSwitcher.allVaults,
}) })
const activeEditorVaultPath = activeTab ? vaultPathForEntry(activeTab.entry, resolvedPath) : resolvedPath const activeEditorVaultPath = activeTab ? vaultPathForEntry(activeTab.entry, resolvedPath) : resolvedPath
const noteGitUrls = useNoteGitUrls({
currentVaultPath: resolvedPath,
locale: appLocale,
remoteStatusForRepository: gitSurfaces.remoteStatusForRepository,
setToastMessage,
})
const commandAiActions = useAppCommandAiActions(aiFeaturesEnabled, dialogs, aiAgentsStatus, vaultAiGuidanceStatus, restoreVaultAiGuidanceCommand, aiAgentPreferences) const commandAiActions = useAppCommandAiActions(aiFeaturesEnabled, dialogs, aiAgentsStatus, vaultAiGuidanceStatus, restoreVaultAiGuidanceCommand, aiAgentPreferences)
const undoCommand = useCallback(() => { const undoCommand = useCallback(() => {
if (runNativeTextHistoryCommand('undo')) return if (runNativeTextHistoryCommand('undo')) return
@@ -1594,7 +1601,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? ( {effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (
<PulseView vaultPath={gitSurfaces.historyRepositoryPath} onOpenNote={handlePulseOpenNote} refreshKey={gitHistoryRefreshKey} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => handleSetViewMode('all')} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.historyRepositoryPath} onRepositoryChange={gitSurfaces.setHistoryRepositoryPath} locale={appLocale} /> <PulseView vaultPath={gitSurfaces.historyRepositoryPath} onOpenNote={handlePulseOpenNote} refreshKey={gitHistoryRefreshKey} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => handleSetViewMode('all')} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.historyRepositoryPath} onRepositoryChange={gitSurfaces.setHistoryRepositoryPath} locale={appLocale} />
) : ( ) : (
<NoteList entries={visibleEntries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} loading={isVaultContentLoading} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={noteListModifiedFiles} modifiedFilesError={noteListModifiedFilesError} gitRepositories={gitRepositories} selectedGitRepositoryPath={gitSurfaces.changesRepositoryPath} onGitRepositoryChange={gitSurfaces.setChangesRepositoryPath} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={handleReplaceActiveTabWithQueuedDiff} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onExportPdf={handleExportNotePdfFromList} onToggleFavorite={entryActions.handleToggleFavorite} onToggleOrganized={explicitOrganizationEnabled ? entryActions.handleToggleOrganized : undefined} onRevealFile={fileActions.revealFile} onCopyFilePath={fileActions.copyFilePath} onDiscardFile={handleDiscardFile} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} allNotesFileVisibility={allNotesFileVisibility} multiSelectionCommandRef={multiSelectionCommandRef} locale={appLocale} /> <NoteList entries={visibleEntries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} loading={isVaultContentLoading} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={noteListModifiedFiles} modifiedFilesError={noteListModifiedFilesError} gitRepositories={gitRepositories} selectedGitRepositoryPath={gitSurfaces.changesRepositoryPath} onGitRepositoryChange={gitSurfaces.setChangesRepositoryPath} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={handleReplaceActiveTabWithQueuedDiff} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onExportPdf={handleExportNotePdfFromList} onToggleFavorite={entryActions.handleToggleFavorite} onToggleOrganized={explicitOrganizationEnabled ? entryActions.handleToggleOrganized : undefined} onRevealFile={fileActions.revealFile} onCopyFilePath={fileActions.copyFilePath} canCopyGitUrl={noteGitUrls.canCopyEntryGitUrl} onCopyGitUrl={noteGitUrls.copyEntryGitUrl} onDiscardFile={handleDiscardFile} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} allNotesFileVisibility={allNotesFileVisibility} multiSelectionCommandRef={multiSelectionCommandRef} locale={appLocale} />
)} )}
</div> </div>
<ResizeHandle onResize={layout.handleNoteListResize} /> <ResizeHandle onResize={layout.handleNoteListResize} />
@@ -1645,6 +1652,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
onRevealFile={fileActions.revealFile} onRevealFile={fileActions.revealFile}
onCopyFilePath={fileActions.copyFilePath} onCopyFilePath={fileActions.copyFilePath}
onCopyDeepLink={activeDeletedFile ? undefined : deepLinks.copyEntryDeepLink} onCopyDeepLink={activeDeletedFile ? undefined : deepLinks.copyEntryDeepLink}
onCopyGitUrl={activeDeletedFile || !activeTabEntry || !noteGitUrls.canCopyEntryGitUrl(activeTabEntry) ? undefined : noteGitUrls.copyEntryGitUrl}
onOpenExternalFile={fileActions.openExternalFile} onOpenExternalFile={fileActions.openExternalFile}
onDeleteNote={activeDeletedFile ? undefined : deleteActions.handleDeleteNote} onDeleteNote={activeDeletedFile ? undefined : deleteActions.handleDeleteNote}
onArchiveNote={activeDeletedFile ? undefined : entryActions.handleArchiveNote} onArchiveNote={activeDeletedFile ? undefined : entryActions.handleArchiveNote}

View File

@@ -249,6 +249,24 @@ describe('BreadcrumbBar — file actions', () => {
expect(onCopyDeepLink).toHaveBeenCalledWith(baseEntry) expect(onCopyDeepLink).toHaveBeenCalledWith(baseEntry)
}) })
it('copies the current note git URL from the overflow menu when available', async () => {
const onCopyGitUrl = vi.fn()
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onCopyGitUrl={onCopyGitUrl} />)
const menu = await openOverflowMenu()
fireEvent.click(within(menu).getByRole('menuitem', { name: 'Copy git URL' }))
expect(onCopyGitUrl).toHaveBeenCalledWith(baseEntry)
})
it('does not show the note git URL action without a remote-backed handler', async () => {
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} />)
const menu = await openOverflowMenu()
expect(within(menu).queryByRole('menuitem', { name: 'Copy git URL' })).not.toBeInTheDocument()
})
it('exports the current note as PDF from the overflow menu', async () => { it('exports the current note as PDF from the overflow menu', async () => {
const onExportPdf = vi.fn() const onExportPdf = vi.fn()
render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onExportPdf={onExportPdf} />) render(<BreadcrumbBar entry={baseEntry} {...defaultProps} onExportPdf={onExportPdf} />)

View File

@@ -60,6 +60,7 @@ interface BreadcrumbBarProps {
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onExportPdf?: () => void onExportPdf?: () => void
onDelete?: () => void onDelete?: () => void
onArchive?: () => void onArchive?: () => void
@@ -819,6 +820,7 @@ function BreadcrumbActions({
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
onCopyDeepLink, onCopyDeepLink,
onCopyGitUrl,
onExportPdf, onExportPdf,
onDelete, onDelete,
onArchive, onArchive,
@@ -868,6 +870,7 @@ function BreadcrumbActions({
onRevealFile={onRevealFile} onRevealFile={onRevealFile}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onCopyDeepLink={onCopyDeepLink} onCopyDeepLink={onCopyDeepLink}
onCopyGitUrl={onCopyGitUrl}
onExportPdf={onExportPdf} onExportPdf={onExportPdf}
onArchive={onArchive} onArchive={onArchive}
onUnarchive={onUnarchive} onUnarchive={onUnarchive}
@@ -892,6 +895,7 @@ function BreadcrumbOverflowMenu({
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
onCopyDeepLink, onCopyDeepLink,
onCopyGitUrl,
onExportPdf, onExportPdf,
onArchive, onArchive,
onUnarchive, onUnarchive,
@@ -911,6 +915,7 @@ function BreadcrumbOverflowMenu({
| 'onRevealFile' | 'onRevealFile'
| 'onCopyFilePath' | 'onCopyFilePath'
| 'onCopyDeepLink' | 'onCopyDeepLink'
| 'onCopyGitUrl'
| 'onExportPdf' | 'onExportPdf'
| 'onArchive' | 'onArchive'
| 'onUnarchive' | 'onUnarchive'
@@ -986,6 +991,7 @@ function BreadcrumbOverflowMenu({
<Link size={16} /> <Link size={16} />
{translate(locale, 'editor.toolbar.copyNoteDeepLink')} {translate(locale, 'editor.toolbar.copyNoteDeepLink')}
</DropdownMenuItem> </DropdownMenuItem>
<CopyGitUrlMenuItem action={entryAction(onCopyGitUrl, entry)} locale={locale} />
<DropdownMenuItem disabled={!runArchiveAction} onSelect={runArchiveAction}> <DropdownMenuItem disabled={!runArchiveAction} onSelect={runArchiveAction}>
<ArchiveMenuIcon archived={entry.archived} /> <ArchiveMenuIcon archived={entry.archived} />
{archiveLabel} {archiveLabel}
@@ -999,6 +1005,22 @@ function BreadcrumbOverflowMenu({
) )
} }
function CopyGitUrlMenuItem({
action,
locale,
}: {
action: (() => void) | undefined
locale: AppLocale
}) {
if (!action) return null
return (
<DropdownMenuItem onSelect={action}>
<GitBranch size={16} />
{translate(locale, 'editor.toolbar.copyNoteGitUrl')}
</DropdownMenuItem>
)
}
function BreadcrumbSeparator() { function BreadcrumbSeparator() {
return <span aria-hidden="true" className="shrink-0 text-border"></span> return <span aria-hidden="true" className="shrink-0 text-border"></span>
} }

View File

@@ -96,6 +96,7 @@ interface EditorProps {
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onOpenExternalFile?: (path: string) => void onOpenExternalFile?: (path: string) => void
onDeleteNote?: (path: string) => void onDeleteNote?: (path: string) => void
onArchiveNote?: (path: string) => void onArchiveNote?: (path: string) => void
@@ -367,6 +368,7 @@ function EditorLayout({
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
onCopyDeepLink, onCopyDeepLink,
onCopyGitUrl,
onExportPdf, onExportPdf,
onOpenExternalFile, onOpenExternalFile,
onDeleteNote, onDeleteNote,
@@ -441,6 +443,7 @@ function EditorLayout({
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onOpenExternalFile?: (path: string) => void onOpenExternalFile?: (path: string) => void
onDeleteNote?: (path: string) => void onDeleteNote?: (path: string) => void
onArchiveNote?: (path: string) => void onArchiveNote?: (path: string) => void
@@ -533,6 +536,7 @@ function EditorLayout({
onRevealFile={onRevealFile} onRevealFile={onRevealFile}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
onCopyDeepLink={onCopyDeepLink} onCopyDeepLink={onCopyDeepLink}
onCopyGitUrl={onCopyGitUrl}
onExportPdf={() => onExportPdf?.('breadcrumb')} onExportPdf={() => onExportPdf?.('breadcrumb')}
onDeleteNote={onDeleteNote} onDeleteNote={onDeleteNote}
onArchiveNote={onArchiveNote} onArchiveNote={onArchiveNote}

View File

@@ -14,6 +14,8 @@ describe('NoteList context menu', () => {
const onToggleOrganized = vi.fn() const onToggleOrganized = vi.fn()
const onRevealFile = vi.fn() const onRevealFile = vi.fn()
const onCopyFilePath = vi.fn() const onCopyFilePath = vi.fn()
const canCopyGitUrl = vi.fn(() => true)
const onCopyGitUrl = vi.fn()
renderNoteList({ renderNoteList({
onOpenInNewWindow, onOpenInNewWindow,
@@ -25,6 +27,8 @@ describe('NoteList context menu', () => {
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}) })
fireEvent.contextMenu(screen.getByText('Build Laputa App')) fireEvent.contextMenu(screen.getByText('Build Laputa App'))
@@ -58,6 +62,11 @@ describe('NoteList context menu', () => {
fireEvent.click(screen.getByText('Copy file path')) fireEvent.click(screen.getByText('Copy file path'))
expect(onCopyFilePath).toHaveBeenCalledWith(mockEntries[0].path) expect(onCopyFilePath).toHaveBeenCalledWith(mockEntries[0].path)
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
fireEvent.click(screen.getByText('Copy git URL'))
expect(canCopyGitUrl).toHaveBeenCalledWith(mockEntries[0])
expect(onCopyGitUrl).toHaveBeenCalledWith(mockEntries[0])
fireEvent.contextMenu(screen.getByText('Build Laputa App')) fireEvent.contextMenu(screen.getByText('Build Laputa App'))
fireEvent.click(screen.getByText('Export note as PDF')) fireEvent.click(screen.getByText('Export note as PDF'))
expect(onExportPdf).toHaveBeenCalledWith(mockEntries[0]) expect(onExportPdf).toHaveBeenCalledWith(mockEntries[0])
@@ -90,4 +99,16 @@ describe('NoteList context menu', () => {
expect(screen.getByText('Remove from Favorites')).toBeInTheDocument() expect(screen.getByText('Remove from Favorites')).toBeInTheDocument()
expect(screen.getByText('Mark as Unorganized')).toBeInTheDocument() expect(screen.getByText('Mark as Unorganized')).toBeInTheDocument()
}) })
it('hides the git URL action for notes without a remote', () => {
renderNoteList({
canCopyGitUrl: () => false,
onCopyGitUrl: vi.fn(),
onCopyFilePath: vi.fn(),
})
fireEvent.contextMenu(screen.getByText('Build Laputa App'))
expect(screen.queryByText('Copy git URL')).not.toBeInTheDocument()
})
}) })

View File

@@ -35,6 +35,7 @@ type BreadcrumbActions = Pick<
| 'onRevealFile' | 'onRevealFile'
| 'onCopyFilePath' | 'onCopyFilePath'
| 'onCopyDeepLink' | 'onCopyDeepLink'
| 'onCopyGitUrl'
| 'onExportPdf' | 'onExportPdf'
| 'onDeleteNote' | 'onDeleteNote'
| 'onArchiveNote' | 'onArchiveNote'
@@ -204,6 +205,7 @@ function ActiveTabBreadcrumb({
onRevealFile={actions.onRevealFile} onRevealFile={actions.onRevealFile}
onCopyFilePath={actions.onCopyFilePath} onCopyFilePath={actions.onCopyFilePath}
onCopyDeepLink={actions.onCopyDeepLink} onCopyDeepLink={actions.onCopyDeepLink}
onCopyGitUrl={actions.onCopyGitUrl}
onExportPdf={actions.onExportPdf} onExportPdf={actions.onExportPdf}
onDelete={bindPath(actions.onDeleteNote, path)} onDelete={bindPath(actions.onDeleteNote, path)}
onArchive={bindPath(actions.onArchiveNote, path)} onArchive={bindPath(actions.onArchiveNote, path)}
@@ -271,6 +273,7 @@ function buildBreadcrumbActions(model: EditorContentModel): BreadcrumbActions {
onRevealFile: model.onRevealFile, onRevealFile: model.onRevealFile,
onCopyFilePath: model.onCopyFilePath, onCopyFilePath: model.onCopyFilePath,
onCopyDeepLink: model.onCopyDeepLink, onCopyDeepLink: model.onCopyDeepLink,
onCopyGitUrl: model.onCopyGitUrl,
onExportPdf: model.onExportPdf, onExportPdf: model.onExportPdf,
onDeleteNote: model.onDeleteNote, onDeleteNote: model.onDeleteNote,
onArchiveNote: model.onArchiveNote, onArchiveNote: model.onArchiveNote,

View File

@@ -43,6 +43,7 @@ export interface EditorContentProps {
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
onCopyDeepLink?: (entry: VaultEntry) => void onCopyDeepLink?: (entry: VaultEntry) => void
onCopyGitUrl?: (entry: VaultEntry) => void
onExportPdf?: () => void onExportPdf?: () => void
onDeleteNote?: (path: string) => void onDeleteNote?: (path: string) => void
onArchiveNote?: (path: string) => void onArchiveNote?: (path: string) => void

View File

@@ -28,6 +28,8 @@ interface NoteListContextMenuParams {
onToggleOrganized?: (path: string) => void onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
} }
function hasNoteListContextActions({ function hasNoteListContextActions({
@@ -41,6 +43,8 @@ function hasNoteListContextActions({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}: NoteListContextMenuParams & { entry: VaultEntry }) { }: NoteListContextMenuParams & { entry: VaultEntry }) {
return [ return [
onOpenInNewWindow, onOpenInNewWindow,
@@ -52,6 +56,7 @@ function hasNoteListContextActions({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
onCopyGitUrl && canCopyGitUrl?.(entry),
].some(Boolean) ].some(Boolean)
} }
@@ -66,6 +71,8 @@ export function useNoteListContextMenu({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}: NoteListContextMenuParams) { }: NoteListContextMenuParams) {
const [ctxMenu, setCtxMenu] = useState<NoteListContextMenuState | null>(null) const [ctxMenu, setCtxMenu] = useState<NoteListContextMenuState | null>(null)
const ctxMenuRef = useRef<HTMLDivElement>(null) const ctxMenuRef = useRef<HTMLDivElement>(null)
@@ -101,6 +108,8 @@ export function useNoteListContextMenu({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
})) return })) return
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
@@ -109,10 +118,12 @@ export function useNoteListContextMenu({
}, [ }, [
onArchivePaths, onArchivePaths,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onDeletePaths, onDeletePaths,
onEnterNeighborhood, onEnterNeighborhood,
onExportPdf, onExportPdf,
onOpenInNewWindow, onOpenInNewWindow,
onCopyGitUrl,
onRevealFile, onRevealFile,
onToggleFavorite, onToggleFavorite,
onToggleOrganized, onToggleOrganized,
@@ -132,6 +143,8 @@ export function useNoteListContextMenu({
onToggleOrganized={onToggleOrganized} onToggleOrganized={onToggleOrganized}
onRevealFile={onRevealFile} onRevealFile={onRevealFile}
onCopyFilePath={onCopyFilePath} onCopyFilePath={onCopyFilePath}
canCopyGitUrl={canCopyGitUrl}
onCopyGitUrl={onCopyGitUrl}
onClose={closeContextMenu} onClose={closeContextMenu}
/> />
) )

View File

@@ -6,6 +6,7 @@ import {
ClipboardText, ClipboardText,
FilePdf, FilePdf,
FolderOpen, FolderOpen,
GitBranch,
MapTrifold, MapTrifold,
Star, Star,
Trash, Trash,
@@ -43,6 +44,8 @@ interface NoteListContextMenuNodeProps {
onToggleOrganized?: (path: string) => void onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
onClose: () => void onClose: () => void
} }
@@ -58,6 +61,8 @@ type BuildContextMenuItemsParams = Pick<
| 'onToggleOrganized' | 'onToggleOrganized'
| 'onRevealFile' | 'onRevealFile'
| 'onCopyFilePath' | 'onCopyFilePath'
| 'canCopyGitUrl'
| 'onCopyGitUrl'
> >
function openWindowItem( function openWindowItem(
@@ -149,6 +154,21 @@ function copyFilePathItem(
}] }]
} }
function copyGitUrlItem(
entry: VaultEntry,
locale: AppLocale,
canCopyGitUrl: ((entry: VaultEntry) => boolean) | undefined,
onCopyGitUrl: ((entry: VaultEntry) => void) | undefined,
selectAction: SelectContextAction,
) {
if (!onCopyGitUrl || !canCopyGitUrl?.(entry)) return []
return [{
icon: GitBranch,
label: translate(locale, 'editor.toolbar.copyNoteGitUrl'),
onSelect: () => selectAction('copy_git_url', () => onCopyGitUrl(entry)),
}]
}
function exportPdfItem( function exportPdfItem(
entry: VaultEntry, entry: VaultEntry,
locale: AppLocale, locale: AppLocale,
@@ -206,6 +226,7 @@ function buildContextMenuItems(
...neighborhoodItem(entry, props.locale, props.onEnterNeighborhood, selectAction), ...neighborhoodItem(entry, props.locale, props.onEnterNeighborhood, selectAction),
...revealFileItem(entry, props.locale, props.onRevealFile, selectAction), ...revealFileItem(entry, props.locale, props.onRevealFile, selectAction),
...copyFilePathItem(entry, props.locale, props.onCopyFilePath, selectAction), ...copyFilePathItem(entry, props.locale, props.onCopyFilePath, selectAction),
...copyGitUrlItem(entry, props.locale, props.canCopyGitUrl, props.onCopyGitUrl, selectAction),
...exportPdfItem(entry, props.locale, props.onExportPdf, selectAction), ...exportPdfItem(entry, props.locale, props.onExportPdf, selectAction),
...archiveItem(entry, props.locale, props.onArchivePaths, selectAction), ...archiveItem(entry, props.locale, props.onArchivePaths, selectAction),
...deleteItem(entry, props.locale, props.onDeletePaths, selectAction), ...deleteItem(entry, props.locale, props.onDeletePaths, selectAction),
@@ -241,6 +262,8 @@ export function NoteListContextMenuNode({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onClose, onClose,
}: NoteListContextMenuNodeProps) { }: NoteListContextMenuNodeProps) {
if (!ctxMenu) return null if (!ctxMenu) return null
@@ -262,6 +285,8 @@ export function NoteListContextMenuNode({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}, entry, selectAction) }, entry, selectAction)
return ( return (

View File

@@ -345,6 +345,8 @@ interface UseNoteListInteractionStateParams {
onToggleOrganized?: (path: string) => void onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
onAutoTriggerDiff?: () => void onAutoTriggerDiff?: () => void
onDiscardFile?: (relativePath: string) => Promise<void> onDiscardFile?: (relativePath: string) => Promise<void>
onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void onCreateNote: (type?: string, options?: ImmediateCreateOptions) => void
@@ -374,6 +376,8 @@ function useNoteListInteractionState({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onAutoTriggerDiff, onAutoTriggerDiff,
onDiscardFile, onDiscardFile,
onCreateNote, onCreateNote,
@@ -393,6 +397,8 @@ function useNoteListInteractionState({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
}) })
const { const {
collapsedGroups, collapsedGroups,
@@ -555,6 +561,8 @@ export interface NoteListProps {
onToggleOrganized?: (path: string) => void onToggleOrganized?: (path: string) => void
onRevealFile?: (path: string) => void onRevealFile?: (path: string) => void
onCopyFilePath?: (path: string) => void onCopyFilePath?: (path: string) => void
canCopyGitUrl?: (entry: VaultEntry) => boolean
onCopyGitUrl?: (entry: VaultEntry) => void
onDiscardFile?: (relativePath: string) => Promise<void> onDiscardFile?: (relativePath: string) => Promise<void>
onAutoTriggerDiff?: () => void onAutoTriggerDiff?: () => void
onOpenDeletedNote?: (entry: DeletedNoteEntry) => void onOpenDeletedNote?: (entry: DeletedNoteEntry) => void
@@ -684,6 +692,8 @@ export function useNoteListModel({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onDiscardFile, onDiscardFile,
onAutoTriggerDiff, onAutoTriggerDiff,
onOpenDeletedNote, onOpenDeletedNote,
@@ -743,6 +753,8 @@ export function useNoteListModel({
onToggleOrganized, onToggleOrganized,
onRevealFile, onRevealFile,
onCopyFilePath, onCopyFilePath,
canCopyGitUrl,
onCopyGitUrl,
onAutoTriggerDiff, onAutoTriggerDiff,
onDiscardFile, onDiscardFile,
onCreateNote, onCreateNote,

View File

@@ -0,0 +1,108 @@
import { act, renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mockInvoke } from '../mock-tauri'
import type { GitRemoteStatus, VaultEntry } from '../types'
import { writeClipboardText } from '../utils/clipboardText'
import { useNoteGitUrls } from './useNoteGitUrls'
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }))
vi.mock('../mock-tauri', () => ({
isTauri: vi.fn(() => false),
mockInvoke: vi.fn(),
}))
vi.mock('../utils/clipboardText', () => ({
writeClipboardText: vi.fn(),
}))
vi.mock('../lib/telemetry', () => ({
trackEvent: vi.fn(),
}))
function remoteStatus(hasRemote: boolean): GitRemoteStatus {
return {
branch: 'main',
ahead: 0,
behind: 0,
hasRemote,
}
}
function entry(overrides: Partial<VaultEntry> = {}): VaultEntry {
return {
path: '/vault/notes/share.md',
filename: 'share.md',
title: 'Share',
isA: 'Note',
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
archived: false,
modifiedAt: null,
createdAt: null,
fileSize: 0,
snippet: '',
wordCount: 0,
relationships: {},
icon: null,
color: null,
order: null,
sidebarLabel: null,
template: null,
sort: null,
view: null,
visible: true,
organized: false,
favorite: false,
favoriteIndex: null,
listPropertiesDisplay: [],
outgoingLinks: [],
properties: {},
hasH1: true,
fileKind: 'markdown',
...overrides,
}
}
describe('useNoteGitUrls', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(writeClipboardText).mockResolvedValue(undefined)
})
it('copies the backend git URL for remote-backed notes', async () => {
const setToastMessage = vi.fn()
const note = entry()
vi.mocked(mockInvoke).mockResolvedValue('https://github.com/team/vault/blob/main/notes/share.md')
const { result } = renderHook(() => useNoteGitUrls({
currentVaultPath: '/vault',
remoteStatusForRepository: () => remoteStatus(true),
setToastMessage,
}))
expect(result.current.canCopyEntryGitUrl(note)).toBe(true)
act(() => result.current.copyEntryGitUrl(note))
await waitFor(() => {
expect(writeClipboardText).toHaveBeenCalledWith('https://github.com/team/vault/blob/main/notes/share.md')
})
expect(mockInvoke).toHaveBeenCalledWith('git_file_url', { vaultPath: '/vault', path: note.path })
expect(setToastMessage).toHaveBeenCalledWith('Git URL copied')
})
it('does not request a URL for local-only notes', () => {
const note = entry()
const { result } = renderHook(() => useNoteGitUrls({
currentVaultPath: '/vault',
remoteStatusForRepository: () => remoteStatus(false),
setToastMessage: vi.fn(),
}))
expect(result.current.canCopyEntryGitUrl(note)).toBe(false)
act(() => result.current.copyEntryGitUrl(note))
expect(mockInvoke).not.toHaveBeenCalled()
expect(writeClipboardText).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,70 @@
import { useCallback, useMemo } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { translate, type AppLocale } from '../lib/i18n'
import { trackEvent } from '../lib/telemetry'
import { isTauri, mockInvoke } from '../mock-tauri'
import type { GitRemoteStatus, VaultEntry } from '../types'
import { writeClipboardText } from '../utils/clipboardText'
import { vaultPathForEntry } from '../utils/workspaces'
interface UseNoteGitUrlsConfig {
currentVaultPath: string
locale?: AppLocale
remoteStatusForRepository: (path: string) => GitRemoteStatus | null
setToastMessage: (message: string) => void
}
function tauriCall<T>(command: string, args: Record<string, unknown>): Promise<T> {
return isTauri() ? invoke<T>(command, args) : mockInvoke<T>(command, args)
}
function errorDetail(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
function hasRemote(remoteStatus: GitRemoteStatus | null): boolean {
return remoteStatus?.hasRemote === true
}
async function loadNoteGitUrl(vaultPath: string, path: string): Promise<string | null> {
return tauriCall<string | null>('git_file_url', { vaultPath, path })
}
export function useNoteGitUrls({
currentVaultPath,
locale = 'en',
remoteStatusForRepository,
setToastMessage,
}: UseNoteGitUrlsConfig) {
const canCopyEntryGitUrl = useCallback((entry: VaultEntry) => (
hasRemote(remoteStatusForRepository(vaultPathForEntry(entry, currentVaultPath)))
), [currentVaultPath, remoteStatusForRepository])
const copyEntryGitUrl = useCallback((entry: VaultEntry) => {
const vaultPath = vaultPathForEntry(entry, currentVaultPath)
if (!hasRemote(remoteStatusForRepository(vaultPath))) return
void loadNoteGitUrl(vaultPath, entry.path)
.then((url) => {
if (!url) {
setToastMessage(translate(locale, 'noteGitUrls.error.unavailable'))
trackEvent('note_git_url_copied', { outcome: 'failed', reason: 'unavailable' })
return
}
return writeClipboardText(url).then(() => {
setToastMessage(translate(locale, 'noteGitUrls.copied'))
trackEvent('note_git_url_copied', { outcome: 'success' })
})
})
.catch((error) => {
setToastMessage(translate(locale, 'noteGitUrls.error.copyFailed', { detail: errorDetail(error) }))
trackEvent('note_git_url_copied', { outcome: 'failed', reason: 'copy_failed' })
})
}, [currentVaultPath, locale, remoteStatusForRepository, setToastMessage])
return useMemo(() => ({
canCopyEntryGitUrl,
copyEntryGitUrl,
}), [canCopyEntryGitUrl, copyEntryGitUrl])
}

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Паказаць у Finder", "editor.toolbar.revealFile": "Паказаць у Finder",
"editor.toolbar.copyFilePath": "Капіяваць шлях", "editor.toolbar.copyFilePath": "Капіяваць шлях",
"editor.toolbar.copyNoteDeepLink": "Капіяваць дэплінк нататкі", "editor.toolbar.copyNoteDeepLink": "Капіяваць дэплінк нататкі",
"editor.toolbar.copyNoteGitUrl": "Капіраваць URL git",
"editor.toolbar.exportPdf": "Экспартаваць нататку ў фармаце PDF", "editor.toolbar.exportPdf": "Экспартаваць нататку ў фармаце PDF",
"editor.toolbar.moreActions": "Больш дзеянняў", "editor.toolbar.moreActions": "Больш дзеянняў",
"editor.toolbar.openProperties": "Адкрыць уласцівасці", "editor.toolbar.openProperties": "Адкрыць уласцівасці",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Сховішча глыбокіх спасылак недаступнае на гэтай прыладзе.", "deepLinks.error.unavailableVault": "Сховішча глыбокіх спасылак недаступнае на гэтай прыладзе.",
"deepLinks.error.unknownVault": "Глыбокі спасылка накіравана на невядомы сховішча.", "deepLinks.error.unknownVault": "Глыбокі спасылка накіравана на невядомы сховішча.",
"deepLinks.error.unsafePath": "Шлях глыбокай спасылкі не можа выходзіць за межы сховішча.", "deepLinks.error.unsafePath": "Шлях глыбокай спасылкі не можа выходзіць за межы сховішча.",
"noteGitUrls.copied": "URL Git скапіяваны",
"noteGitUrls.error.copyFailed": "Не атрымалася скапіяваць URL git: {detail}",
"noteGitUrls.error.unavailable": "URL Git недаступны для гэтай нататкі.",
"editor.slash.math": "Матэматыка", "editor.slash.math": "Матэматыка",
"tableOfContents.title": "Змест", "tableOfContents.title": "Змест",
"tableOfContents.close": "Закрыць змест", "tableOfContents.close": "Закрыць змест",

View File

@@ -588,6 +588,7 @@
"editor.toolbar.revealFile": "Pakazać u Finder", "editor.toolbar.revealFile": "Pakazać u Finder",
"editor.toolbar.copyFilePath": "Kapijavać šliach", "editor.toolbar.copyFilePath": "Kapijavać šliach",
"editor.toolbar.copyNoteDeepLink": "Kapijavać deep-link natatki", "editor.toolbar.copyNoteDeepLink": "Kapijavać deep-link natatki",
"editor.toolbar.copyNoteGitUrl": "Kapijavać Git URL",
"editor.toolbar.exportPdf": "Ekspartavać natatku ŭ farmacie PDF", "editor.toolbar.exportPdf": "Ekspartavać natatku ŭ farmacie PDF",
"editor.toolbar.moreActions": "Boĺš dziejanniaŭ", "editor.toolbar.moreActions": "Boĺš dziejanniaŭ",
"editor.toolbar.openProperties": "Adkryć ulascivasci", "editor.toolbar.openProperties": "Adkryć ulascivasci",
@@ -611,6 +612,9 @@
"deepLinks.error.unavailableVault": "Schovišča z deep-link niedastupnaje na hetaj pryladzie.", "deepLinks.error.unavailableVault": "Schovišča z deep-link niedastupnaje na hetaj pryladzie.",
"deepLinks.error.unknownVault": "Deep-link viadzie da nieviadomaha schovišča.", "deepLinks.error.unknownVault": "Deep-link viadzie da nieviadomaha schovišča.",
"deepLinks.error.unsafePath": "Šliach deep-link nie moža vychodzić za miežy schovišča.", "deepLinks.error.unsafePath": "Šliach deep-link nie moža vychodzić za miežy schovišča.",
"noteGitUrls.copied": "Git URL skapijavany",
"noteGitUrls.error.copyFailed": "Nie ŭdalosia skapijavać Git URL: {detail}",
"noteGitUrls.error.unavailable": "Git URL niedastupny dla hetaj natatki.",
"tableOfContents.title": "Zmiest", "tableOfContents.title": "Zmiest",
"tableOfContents.close": "Zakryć zmiest", "tableOfContents.close": "Zakryć zmiest",
"tableOfContents.empty": "Niama zahaloŭkaŭ", "tableOfContents.empty": "Niama zahaloŭkaŭ",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Im Finder anzeigen", "editor.toolbar.revealFile": "Im Finder anzeigen",
"editor.toolbar.copyFilePath": "Dateipfad kopieren", "editor.toolbar.copyFilePath": "Dateipfad kopieren",
"editor.toolbar.copyNoteDeepLink": "Deeplink der Notiz kopieren", "editor.toolbar.copyNoteDeepLink": "Deeplink der Notiz kopieren",
"editor.toolbar.copyNoteGitUrl": "Git-URL kopieren",
"editor.toolbar.exportPdf": "Notiz als PDF exportieren", "editor.toolbar.exportPdf": "Notiz als PDF exportieren",
"editor.toolbar.moreActions": "Weitere Notizaktionen", "editor.toolbar.moreActions": "Weitere Notizaktionen",
"editor.toolbar.openProperties": "Eigenschaften-Panel öffnen", "editor.toolbar.openProperties": "Eigenschaften-Panel öffnen",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Der Deep-Link-Tresor ist auf diesem Gerät nicht verfügbar.", "deepLinks.error.unavailableVault": "Der Deep-Link-Tresor ist auf diesem Gerät nicht verfügbar.",
"deepLinks.error.unknownVault": "Der Deep Link verweist auf einen unbekannten Vault.", "deepLinks.error.unknownVault": "Der Deep Link verweist auf einen unbekannten Vault.",
"deepLinks.error.unsafePath": "Der Deep-Link-Pfad darf den Vault nicht verlassen.", "deepLinks.error.unsafePath": "Der Deep-Link-Pfad darf den Vault nicht verlassen.",
"noteGitUrls.copied": "Git-URL kopiert",
"noteGitUrls.error.copyFailed": "Git-URL konnte nicht kopiert werden: {detail}",
"noteGitUrls.error.unavailable": "Für diese Notiz ist keine Git-URL verfügbar.",
"editor.slash.math": "Mathematik", "editor.slash.math": "Mathematik",
"tableOfContents.title": "Inhaltsverzeichnis", "tableOfContents.title": "Inhaltsverzeichnis",
"tableOfContents.close": "Inhaltsverzeichnis schließen", "tableOfContents.close": "Inhaltsverzeichnis schließen",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Reveal in Finder", "editor.toolbar.revealFile": "Reveal in Finder",
"editor.toolbar.copyFilePath": "Copy file path", "editor.toolbar.copyFilePath": "Copy file path",
"editor.toolbar.copyNoteDeepLink": "Copy note deeplink", "editor.toolbar.copyNoteDeepLink": "Copy note deeplink",
"editor.toolbar.copyNoteGitUrl": "Copy git URL",
"editor.toolbar.exportPdf": "Export note as PDF", "editor.toolbar.exportPdf": "Export note as PDF",
"editor.toolbar.moreActions": "More note actions", "editor.toolbar.moreActions": "More note actions",
"editor.toolbar.openProperties": "Open the properties panel", "editor.toolbar.openProperties": "Open the properties panel",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Deep link vault is not available on this device.", "deepLinks.error.unavailableVault": "Deep link vault is not available on this device.",
"deepLinks.error.unknownVault": "Deep link targets an unknown vault.", "deepLinks.error.unknownVault": "Deep link targets an unknown vault.",
"deepLinks.error.unsafePath": "Deep link path cannot leave the vault.", "deepLinks.error.unsafePath": "Deep link path cannot leave the vault.",
"noteGitUrls.copied": "Git URL copied",
"noteGitUrls.error.copyFailed": "Failed to copy git URL: {detail}",
"noteGitUrls.error.unavailable": "Git URL is not available for this note.",
"editor.slash.math": "Math", "editor.slash.math": "Math",
"tableOfContents.title": "Table of Contents", "tableOfContents.title": "Table of Contents",
"tableOfContents.close": "Close table of contents", "tableOfContents.close": "Close table of contents",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Mostrar en el Finder", "editor.toolbar.revealFile": "Mostrar en el Finder",
"editor.toolbar.copyFilePath": "Copiar la ruta del archivo", "editor.toolbar.copyFilePath": "Copiar la ruta del archivo",
"editor.toolbar.copyNoteDeepLink": "Copiar el enlace profundo de la nota", "editor.toolbar.copyNoteDeepLink": "Copiar el enlace profundo de la nota",
"editor.toolbar.copyNoteGitUrl": "Copiar URL de Git",
"editor.toolbar.exportPdf": "Exportar nota como PDF", "editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Más acciones de nota", "editor.toolbar.moreActions": "Más acciones de nota",
"editor.toolbar.openProperties": "Abrir el panel de propiedades", "editor.toolbar.openProperties": "Abrir el panel de propiedades",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "El repositorio del enlace profundo no está disponible en este dispositivo.", "deepLinks.error.unavailableVault": "El repositorio del enlace profundo no está disponible en este dispositivo.",
"deepLinks.error.unknownVault": "El enlace profundo apunta a una bóveda desconocida.", "deepLinks.error.unknownVault": "El enlace profundo apunta a una bóveda desconocida.",
"deepLinks.error.unsafePath": "La ruta del enlace profundo no puede salir de la bóveda.", "deepLinks.error.unsafePath": "La ruta del enlace profundo no puede salir de la bóveda.",
"noteGitUrls.copied": "URL de Git copiada",
"noteGitUrls.error.copyFailed": "Error al copiar la URL de Git: {detail}",
"noteGitUrls.error.unavailable": "La URL de Git no está disponible para esta nota.",
"editor.slash.math": "Matemáticas", "editor.slash.math": "Matemáticas",
"tableOfContents.title": "Índice", "tableOfContents.title": "Índice",
"tableOfContents.close": "Cerrar índice", "tableOfContents.close": "Cerrar índice",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Mostrar en el Finder", "editor.toolbar.revealFile": "Mostrar en el Finder",
"editor.toolbar.copyFilePath": "Copiar la ruta del archivo", "editor.toolbar.copyFilePath": "Copiar la ruta del archivo",
"editor.toolbar.copyNoteDeepLink": "Copiar el enlace profundo de la nota", "editor.toolbar.copyNoteDeepLink": "Copiar el enlace profundo de la nota",
"editor.toolbar.copyNoteGitUrl": "Copiar URL de Git",
"editor.toolbar.exportPdf": "Exportar nota como PDF", "editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Más acciones de nota", "editor.toolbar.moreActions": "Más acciones de nota",
"editor.toolbar.openProperties": "Abrir el panel de propiedades", "editor.toolbar.openProperties": "Abrir el panel de propiedades",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "El almacén del enlace profundo no está disponible en este dispositivo.", "deepLinks.error.unavailableVault": "El almacén del enlace profundo no está disponible en este dispositivo.",
"deepLinks.error.unknownVault": "El enlace profundo apunta a un almacén desconocido.", "deepLinks.error.unknownVault": "El enlace profundo apunta a un almacén desconocido.",
"deepLinks.error.unsafePath": "La ruta del enlace profundo no puede salir de la caja fuerte.", "deepLinks.error.unsafePath": "La ruta del enlace profundo no puede salir de la caja fuerte.",
"noteGitUrls.copied": "URL de Git copiada",
"noteGitUrls.error.copyFailed": "Error al copiar la URL de Git: {detail}",
"noteGitUrls.error.unavailable": "La URL de Git no está disponible para esta nota.",
"editor.slash.math": "Matemáticas", "editor.slash.math": "Matemáticas",
"tableOfContents.title": "Índice", "tableOfContents.title": "Índice",
"tableOfContents.close": "Cerrar el índice", "tableOfContents.close": "Cerrar el índice",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Afficher dans le Finder", "editor.toolbar.revealFile": "Afficher dans le Finder",
"editor.toolbar.copyFilePath": "Copier le chemin du fichier", "editor.toolbar.copyFilePath": "Copier le chemin du fichier",
"editor.toolbar.copyNoteDeepLink": "Copier le deeplink de la note", "editor.toolbar.copyNoteDeepLink": "Copier le deeplink de la note",
"editor.toolbar.copyNoteGitUrl": "Copier l'URL Git",
"editor.toolbar.exportPdf": "Exporter la note au format PDF", "editor.toolbar.exportPdf": "Exporter la note au format PDF",
"editor.toolbar.moreActions": "Autres actions de note", "editor.toolbar.moreActions": "Autres actions de note",
"editor.toolbar.openProperties": "Ouvrir le panneau des propriétés", "editor.toolbar.openProperties": "Ouvrir le panneau des propriétés",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Le coffre-fort du lien profond n'est pas disponible sur cet appareil.", "deepLinks.error.unavailableVault": "Le coffre-fort du lien profond n'est pas disponible sur cet appareil.",
"deepLinks.error.unknownVault": "Le lien profond cible un coffre inconnu.", "deepLinks.error.unknownVault": "Le lien profond cible un coffre inconnu.",
"deepLinks.error.unsafePath": "Le chemin du lien profond ne peut pas sortir du coffre.", "deepLinks.error.unsafePath": "Le chemin du lien profond ne peut pas sortir du coffre.",
"noteGitUrls.copied": "URL Git copiée",
"noteGitUrls.error.copyFailed": "Échec de la copie de l'URL Git : {detail}",
"noteGitUrls.error.unavailable": "L'URL Git n'est pas disponible pour cette note.",
"editor.slash.math": "Mathématiques", "editor.slash.math": "Mathématiques",
"tableOfContents.title": "Table des matières", "tableOfContents.title": "Table des matières",
"tableOfContents.close": "Fermer la table des matières", "tableOfContents.close": "Fermer la table des matières",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Tampilkan di Finder", "editor.toolbar.revealFile": "Tampilkan di Finder",
"editor.toolbar.copyFilePath": "Salin jalur file", "editor.toolbar.copyFilePath": "Salin jalur file",
"editor.toolbar.copyNoteDeepLink": "Salin deeplink catatan", "editor.toolbar.copyNoteDeepLink": "Salin deeplink catatan",
"editor.toolbar.copyNoteGitUrl": "Salin URL git",
"editor.toolbar.exportPdf": "Ekspor catatan sebagai PDF", "editor.toolbar.exportPdf": "Ekspor catatan sebagai PDF",
"editor.toolbar.moreActions": "Tindakan catatan lainnya", "editor.toolbar.moreActions": "Tindakan catatan lainnya",
"editor.toolbar.openProperties": "Buka panel properti", "editor.toolbar.openProperties": "Buka panel properti",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Vault tautan dalam tidak tersedia di perangkat ini.", "deepLinks.error.unavailableVault": "Vault tautan dalam tidak tersedia di perangkat ini.",
"deepLinks.error.unknownVault": "Tautan dalam mengarah ke vault yang tidak dikenal.", "deepLinks.error.unknownVault": "Tautan dalam mengarah ke vault yang tidak dikenal.",
"deepLinks.error.unsafePath": "Jalur tautan dalam tidak boleh keluar dari vault.", "deepLinks.error.unsafePath": "Jalur tautan dalam tidak boleh keluar dari vault.",
"noteGitUrls.copied": "URL Git disalin",
"noteGitUrls.error.copyFailed": "Gagal menyalin URL git: {detail}",
"noteGitUrls.error.unavailable": "URL Git tidak tersedia untuk catatan ini.",
"editor.slash.math": "Matematika", "editor.slash.math": "Matematika",
"tableOfContents.title": "Daftar Isi", "tableOfContents.title": "Daftar Isi",
"tableOfContents.close": "Tutup daftar isi", "tableOfContents.close": "Tutup daftar isi",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Mostra nel Finder", "editor.toolbar.revealFile": "Mostra nel Finder",
"editor.toolbar.copyFilePath": "Copia il percorso del file", "editor.toolbar.copyFilePath": "Copia il percorso del file",
"editor.toolbar.copyNoteDeepLink": "Copia deeplink della nota", "editor.toolbar.copyNoteDeepLink": "Copia deeplink della nota",
"editor.toolbar.copyNoteGitUrl": "Copia URL Git",
"editor.toolbar.exportPdf": "Esporta nota in PDF", "editor.toolbar.exportPdf": "Esporta nota in PDF",
"editor.toolbar.moreActions": "Altre azioni della nota", "editor.toolbar.moreActions": "Altre azioni della nota",
"editor.toolbar.openProperties": "Apri il pannello delle proprietà", "editor.toolbar.openProperties": "Apri il pannello delle proprietà",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Il vault del deep link non è disponibile su questo dispositivo.", "deepLinks.error.unavailableVault": "Il vault del deep link non è disponibile su questo dispositivo.",
"deepLinks.error.unknownVault": "Il deep link punta a un vault sconosciuto.", "deepLinks.error.unknownVault": "Il deep link punta a un vault sconosciuto.",
"deepLinks.error.unsafePath": "Il percorso del deep link non può uscire dal vault.", "deepLinks.error.unsafePath": "Il percorso del deep link non può uscire dal vault.",
"noteGitUrls.copied": "URL Git copiato",
"noteGitUrls.error.copyFailed": "Impossibile copiare l'URL Git: {detail}",
"noteGitUrls.error.unavailable": "L'URL Git non è disponibile per questa nota.",
"editor.slash.math": "Matematica", "editor.slash.math": "Matematica",
"tableOfContents.title": "Indice", "tableOfContents.title": "Indice",
"tableOfContents.close": "Chiudi l'indice", "tableOfContents.close": "Chiudi l'indice",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Finderで表示", "editor.toolbar.revealFile": "Finderで表示",
"editor.toolbar.copyFilePath": "ファイルパスをコピー", "editor.toolbar.copyFilePath": "ファイルパスをコピー",
"editor.toolbar.copyNoteDeepLink": "ノートのディープリンクをコピー", "editor.toolbar.copyNoteDeepLink": "ノートのディープリンクをコピー",
"editor.toolbar.copyNoteGitUrl": "Git URLをコピー",
"editor.toolbar.exportPdf": "ートをPDFとしてエクスポート", "editor.toolbar.exportPdf": "ートをPDFとしてエクスポート",
"editor.toolbar.moreActions": "その他のノート操作", "editor.toolbar.moreActions": "その他のノート操作",
"editor.toolbar.openProperties": "プロパティパネルを開く", "editor.toolbar.openProperties": "プロパティパネルを開く",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "このデバイスではディープリンクのボールトは利用できません。", "deepLinks.error.unavailableVault": "このデバイスではディープリンクのボールトは利用できません。",
"deepLinks.error.unknownVault": "ディープリンクのターゲットは不明なボールトです。", "deepLinks.error.unknownVault": "ディープリンクのターゲットは不明なボールトです。",
"deepLinks.error.unsafePath": "ディープリンクのパスは、ボールトの外に出ることはできません。", "deepLinks.error.unsafePath": "ディープリンクのパスは、ボールトの外に出ることはできません。",
"noteGitUrls.copied": "Git URLをコピーしました",
"noteGitUrls.error.copyFailed": "Git URLのコピーに失敗しました{detail}",
"noteGitUrls.error.unavailable": "このートではGit URLは利用できません。",
"editor.slash.math": "数学", "editor.slash.math": "数学",
"tableOfContents.title": "目次", "tableOfContents.title": "目次",
"tableOfContents.close": "目次を閉じる", "tableOfContents.close": "目次を閉じる",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Finder에서 표시", "editor.toolbar.revealFile": "Finder에서 표시",
"editor.toolbar.copyFilePath": "파일 경로 복사", "editor.toolbar.copyFilePath": "파일 경로 복사",
"editor.toolbar.copyNoteDeepLink": "노트 딥링크 복사", "editor.toolbar.copyNoteDeepLink": "노트 딥링크 복사",
"editor.toolbar.copyNoteGitUrl": "Git URL 복사",
"editor.toolbar.exportPdf": "노트를 PDF로 내보내기", "editor.toolbar.exportPdf": "노트를 PDF로 내보내기",
"editor.toolbar.moreActions": "추가 노트 작업", "editor.toolbar.moreActions": "추가 노트 작업",
"editor.toolbar.openProperties": "속성 패널 열기", "editor.toolbar.openProperties": "속성 패널 열기",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "이 장치에서는 딥 링크 보관을 사용할 수 없습니다.", "deepLinks.error.unavailableVault": "이 장치에서는 딥 링크 보관을 사용할 수 없습니다.",
"deepLinks.error.unknownVault": "딥 링크가 알 수 없는 볼트를 대상으로 합니다.", "deepLinks.error.unknownVault": "딥 링크가 알 수 없는 볼트를 대상으로 합니다.",
"deepLinks.error.unsafePath": "딥 링크 경로는 볼트를 벗어날 수 없습니다.", "deepLinks.error.unsafePath": "딥 링크 경로는 볼트를 벗어날 수 없습니다.",
"noteGitUrls.copied": "Git URL 복사됨",
"noteGitUrls.error.copyFailed": "Git URL 복사에 실패했습니다: {detail}",
"noteGitUrls.error.unavailable": "이 노트에 대한 Git URL을 사용할 수 없습니다.",
"editor.slash.math": "수학", "editor.slash.math": "수학",
"tableOfContents.title": "목차", "tableOfContents.title": "목차",
"tableOfContents.close": "목차 닫기", "tableOfContents.close": "목차 닫기",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Pokaż w Finderze", "editor.toolbar.revealFile": "Pokaż w Finderze",
"editor.toolbar.copyFilePath": "Kopiuj ścieżkę pliku", "editor.toolbar.copyFilePath": "Kopiuj ścieżkę pliku",
"editor.toolbar.copyNoteDeepLink": "Skopiuj deeplink do notatki", "editor.toolbar.copyNoteDeepLink": "Skopiuj deeplink do notatki",
"editor.toolbar.copyNoteGitUrl": "Skopiuj adres URL Git",
"editor.toolbar.exportPdf": "Eksportuj notatkę jako PDF", "editor.toolbar.exportPdf": "Eksportuj notatkę jako PDF",
"editor.toolbar.moreActions": "Więcej działań notatki", "editor.toolbar.moreActions": "Więcej działań notatki",
"editor.toolbar.openProperties": "Otwórz panel właściwości", "editor.toolbar.openProperties": "Otwórz panel właściwości",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Magazyn deep linków nie jest dostępny na tym urządzeniu.", "deepLinks.error.unavailableVault": "Magazyn deep linków nie jest dostępny na tym urządzeniu.",
"deepLinks.error.unknownVault": "Głęboki link wskazuje na nieznany skarbiec.", "deepLinks.error.unknownVault": "Głęboki link wskazuje na nieznany skarbiec.",
"deepLinks.error.unsafePath": "Ścieżka deep linku nie może wychodzić poza skarbiec.", "deepLinks.error.unsafePath": "Ścieżka deep linku nie może wychodzić poza skarbiec.",
"noteGitUrls.copied": "Skopiowano adres URL Git",
"noteGitUrls.error.copyFailed": "Nie udało się skopiować adresu URL Git: {detail}",
"noteGitUrls.error.unavailable": "Adres URL Git nie jest dostępny dla tej notatki.",
"editor.slash.math": "Matematyka", "editor.slash.math": "Matematyka",
"tableOfContents.title": "Spis treści", "tableOfContents.title": "Spis treści",
"tableOfContents.close": "Zamknij spis treści", "tableOfContents.close": "Zamknij spis treści",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Exibir no Finder", "editor.toolbar.revealFile": "Exibir no Finder",
"editor.toolbar.copyFilePath": "Copiar caminho do arquivo", "editor.toolbar.copyFilePath": "Copiar caminho do arquivo",
"editor.toolbar.copyNoteDeepLink": "Copiar deeplink da nota", "editor.toolbar.copyNoteDeepLink": "Copiar deeplink da nota",
"editor.toolbar.copyNoteGitUrl": "Copiar URL do Git",
"editor.toolbar.exportPdf": "Exportar nota como PDF", "editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Mais ações da nota", "editor.toolbar.moreActions": "Mais ações da nota",
"editor.toolbar.openProperties": "Abrir o painel de propriedades", "editor.toolbar.openProperties": "Abrir o painel de propriedades",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "O cofre do deep link não está disponível neste dispositivo.", "deepLinks.error.unavailableVault": "O cofre do deep link não está disponível neste dispositivo.",
"deepLinks.error.unknownVault": "O deep link aponta para um cofre desconhecido.", "deepLinks.error.unknownVault": "O deep link aponta para um cofre desconhecido.",
"deepLinks.error.unsafePath": "O caminho do deep link não pode sair do cofre.", "deepLinks.error.unsafePath": "O caminho do deep link não pode sair do cofre.",
"noteGitUrls.copied": "URL do Git copiada",
"noteGitUrls.error.copyFailed": "Falha ao copiar o URL do Git: {detail}",
"noteGitUrls.error.unavailable": "O URL do Git não está disponível para esta nota.",
"editor.slash.math": "Matemática", "editor.slash.math": "Matemática",
"tableOfContents.title": "Índice", "tableOfContents.title": "Índice",
"tableOfContents.close": "Fechar índice", "tableOfContents.close": "Fechar índice",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Mostrar no Finder", "editor.toolbar.revealFile": "Mostrar no Finder",
"editor.toolbar.copyFilePath": "Copiar caminho do ficheiro", "editor.toolbar.copyFilePath": "Copiar caminho do ficheiro",
"editor.toolbar.copyNoteDeepLink": "Copiar deeplink da nota", "editor.toolbar.copyNoteDeepLink": "Copiar deeplink da nota",
"editor.toolbar.copyNoteGitUrl": "Copiar URL do Git",
"editor.toolbar.exportPdf": "Exportar nota como PDF", "editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Mais ações da nota", "editor.toolbar.moreActions": "Mais ações da nota",
"editor.toolbar.openProperties": "Abrir o painel de propriedades", "editor.toolbar.openProperties": "Abrir o painel de propriedades",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "O cofre da ligação profunda não está disponível neste dispositivo.", "deepLinks.error.unavailableVault": "O cofre da ligação profunda não está disponível neste dispositivo.",
"deepLinks.error.unknownVault": "O deep link aponta para um cofre desconhecido.", "deepLinks.error.unknownVault": "O deep link aponta para um cofre desconhecido.",
"deepLinks.error.unsafePath": "O caminho do deep link não pode sair do cofre.", "deepLinks.error.unsafePath": "O caminho do deep link não pode sair do cofre.",
"noteGitUrls.copied": "URL do Git copiado",
"noteGitUrls.error.copyFailed": "Falha ao copiar o URL do Git: {detail}",
"noteGitUrls.error.unavailable": "O URL do Git não está disponível para esta nota.",
"editor.slash.math": "Matemática", "editor.slash.math": "Matemática",
"tableOfContents.title": "Índice", "tableOfContents.title": "Índice",
"tableOfContents.close": "Fechar índice", "tableOfContents.close": "Fechar índice",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Показать в Finder", "editor.toolbar.revealFile": "Показать в Finder",
"editor.toolbar.copyFilePath": "Копировать путь к файлу", "editor.toolbar.copyFilePath": "Копировать путь к файлу",
"editor.toolbar.copyNoteDeepLink": "Копировать диплинк заметки", "editor.toolbar.copyNoteDeepLink": "Копировать диплинк заметки",
"editor.toolbar.copyNoteGitUrl": "Копировать URL Git",
"editor.toolbar.exportPdf": "Экспортировать заметку в формате PDF", "editor.toolbar.exportPdf": "Экспортировать заметку в формате PDF",
"editor.toolbar.moreActions": "Другие действия с заметкой", "editor.toolbar.moreActions": "Другие действия с заметкой",
"editor.toolbar.openProperties": "Открыть панель свойств", "editor.toolbar.openProperties": "Открыть панель свойств",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Хранилище для глубокой ссылки недоступно на этом устройстве.", "deepLinks.error.unavailableVault": "Хранилище для глубокой ссылки недоступно на этом устройстве.",
"deepLinks.error.unknownVault": "Глубинная ссылка указывает на неизвестное хранилище.", "deepLinks.error.unknownVault": "Глубинная ссылка указывает на неизвестное хранилище.",
"deepLinks.error.unsafePath": "Путь глубокой ссылки не может выходить за пределы хранилища.", "deepLinks.error.unsafePath": "Путь глубокой ссылки не может выходить за пределы хранилища.",
"noteGitUrls.copied": "URL Git скопирован",
"noteGitUrls.error.copyFailed": "Не удалось скопировать URL-адрес Git: {detail}",
"noteGitUrls.error.unavailable": "URL-адрес Git недоступен для этой заметки.",
"editor.slash.math": "Математика", "editor.slash.math": "Математика",
"tableOfContents.title": "Содержание", "tableOfContents.title": "Содержание",
"tableOfContents.close": "Закрыть оглавление", "tableOfContents.close": "Закрыть оглавление",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "Hiển thị trong Finder", "editor.toolbar.revealFile": "Hiển thị trong Finder",
"editor.toolbar.copyFilePath": "Sao chép đường dẫn tệp", "editor.toolbar.copyFilePath": "Sao chép đường dẫn tệp",
"editor.toolbar.copyNoteDeepLink": "Sao chép liên kết sâu của ghi chú", "editor.toolbar.copyNoteDeepLink": "Sao chép liên kết sâu của ghi chú",
"editor.toolbar.copyNoteGitUrl": "Sao chép URL git",
"editor.toolbar.exportPdf": "Xuất ghi chú dưới dạng PDF", "editor.toolbar.exportPdf": "Xuất ghi chú dưới dạng PDF",
"editor.toolbar.moreActions": "Thêm hành động ghi chú", "editor.toolbar.moreActions": "Thêm hành động ghi chú",
"editor.toolbar.openProperties": "Mở bảng thuộc tính", "editor.toolbar.openProperties": "Mở bảng thuộc tính",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "Kho liên kết sâu không khả dụng trên thiết bị này.", "deepLinks.error.unavailableVault": "Kho liên kết sâu không khả dụng trên thiết bị này.",
"deepLinks.error.unknownVault": "Liên kết sâu trỏ đến một vault không xác định.", "deepLinks.error.unknownVault": "Liên kết sâu trỏ đến một vault không xác định.",
"deepLinks.error.unsafePath": "Đường dẫn liên kết sâu không thể nằm ngoài vault.", "deepLinks.error.unsafePath": "Đường dẫn liên kết sâu không thể nằm ngoài vault.",
"noteGitUrls.copied": "Đã sao chép URL Git",
"noteGitUrls.error.copyFailed": "Không thể sao chép URL git: {detail}",
"noteGitUrls.error.unavailable": "URL Git không khả dụng cho ghi chú này.",
"editor.slash.math": "Toán học", "editor.slash.math": "Toán học",
"tableOfContents.title": "Mục lục", "tableOfContents.title": "Mục lục",
"tableOfContents.close": "Đóng mục lục", "tableOfContents.close": "Đóng mục lục",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "在文件管理器中显示", "editor.toolbar.revealFile": "在文件管理器中显示",
"editor.toolbar.copyFilePath": "复制文件路径", "editor.toolbar.copyFilePath": "复制文件路径",
"editor.toolbar.copyNoteDeepLink": "复制笔记深层链接", "editor.toolbar.copyNoteDeepLink": "复制笔记深层链接",
"editor.toolbar.copyNoteGitUrl": "复制 Git URL",
"editor.toolbar.exportPdf": "将笔记导出为 PDF", "editor.toolbar.exportPdf": "将笔记导出为 PDF",
"editor.toolbar.moreActions": "更多笔记操作", "editor.toolbar.moreActions": "更多笔记操作",
"editor.toolbar.openProperties": "打开属性面板", "editor.toolbar.openProperties": "打开属性面板",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "此设备上没有深层链接保管库。", "deepLinks.error.unavailableVault": "此设备上没有深层链接保管库。",
"deepLinks.error.unknownVault": "深层链接指向未知的保管库。", "deepLinks.error.unknownVault": "深层链接指向未知的保管库。",
"deepLinks.error.unsafePath": "深层链接路径不能离开保管库。", "deepLinks.error.unsafePath": "深层链接路径不能离开保管库。",
"noteGitUrls.copied": "Git URL 已复制",
"noteGitUrls.error.copyFailed": "复制 Git URL 失败:{detail}",
"noteGitUrls.error.unavailable": "此笔记没有 Git URL。",
"editor.slash.math": "数学", "editor.slash.math": "数学",
"tableOfContents.title": "目录", "tableOfContents.title": "目录",
"tableOfContents.close": "关闭目录", "tableOfContents.close": "关闭目录",

View File

@@ -586,6 +586,7 @@
"editor.toolbar.revealFile": "在檔案管理器中顯示", "editor.toolbar.revealFile": "在檔案管理器中顯示",
"editor.toolbar.copyFilePath": "複製檔案路徑", "editor.toolbar.copyFilePath": "複製檔案路徑",
"editor.toolbar.copyNoteDeepLink": "複製筆記深層連結", "editor.toolbar.copyNoteDeepLink": "複製筆記深層連結",
"editor.toolbar.copyNoteGitUrl": "複製 Git URL",
"editor.toolbar.exportPdf": "將筆記匯出為 PDF", "editor.toolbar.exportPdf": "將筆記匯出為 PDF",
"editor.toolbar.moreActions": "更多筆記操作", "editor.toolbar.moreActions": "更多筆記操作",
"editor.toolbar.openProperties": "開啟屬性面板", "editor.toolbar.openProperties": "開啟屬性面板",
@@ -609,6 +610,9 @@
"deepLinks.error.unavailableVault": "此裝置無法使用深層連結保管庫。", "deepLinks.error.unavailableVault": "此裝置無法使用深層連結保管庫。",
"deepLinks.error.unknownVault": "深層連結指向未知的保管庫。", "deepLinks.error.unknownVault": "深層連結指向未知的保管庫。",
"deepLinks.error.unsafePath": "深層連結路徑不能離開保管庫。", "deepLinks.error.unsafePath": "深層連結路徑不能離開保管庫。",
"noteGitUrls.copied": "Git URL 已複製",
"noteGitUrls.error.copyFailed": "無法複製 Git URL{detail}",
"noteGitUrls.error.unavailable": "此筆記無法使用 Git URL。",
"editor.slash.math": "數學", "editor.slash.math": "數學",
"tableOfContents.title": "目錄", "tableOfContents.title": "目錄",
"tableOfContents.close": "關閉目錄", "tableOfContents.close": "關閉目錄",

View File

@@ -460,6 +460,12 @@ export const mockHandlers: Record<string, (args: any) => any> = {
const vaultPath = args?.vaultPath ?? args?.vault_path ?? mockLastVaultPath ?? DEFAULT_MOCK_VAULT_PATH const vaultPath = args?.vaultPath ?? args?.vault_path ?? mockLastVaultPath ?? DEFAULT_MOCK_VAULT_PATH
return { branch: 'main', ahead: 0, behind: 0, hasRemote: getMockRemoteState(vaultPath) } return { branch: 'main', ahead: 0, behind: 0, hasRemote: getMockRemoteState(vaultPath) }
}, },
git_file_url: (args?: { vaultPath?: string; vault_path?: string; path?: string }): string | null => {
const vaultPath = args?.vaultPath ?? args?.vault_path ?? mockLastVaultPath ?? DEFAULT_MOCK_VAULT_PATH
if (!getMockRemoteState(vaultPath)) return null
const path = args?.path?.replace(/^.*?\/Laputa\//, '') ?? 'note.md'
return `https://github.com/lucaong/laputa-vault/blob/main/${encodeURI(path)}`
},
git_add_remote: (args?: { git_add_remote: (args?: {
request?: { vaultPath?: string; vault_path?: string; remoteUrl?: string } request?: { vaultPath?: string; vault_path?: string; remoteUrl?: string }
vaultPath?: string vaultPath?: string