Merge pull request #5349 from gitbutlerapp/e-branch-4

fix: Open files on editor in Windows
This commit is contained in:
Esteban Vega 2024-10-29 11:39:03 +01:00 committed by GitHub
commit a216ccedaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 130 additions and 20 deletions

View File

@ -5,7 +5,7 @@
import { SETTINGS, type Settings } from '$lib/settings/userSettings'; import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import * as events from '$lib/utils/events'; import * as events from '$lib/utils/events';
import { unsubscribe } from '$lib/utils/unsubscribe'; import { unsubscribe } from '$lib/utils/unsubscribe';
import { openExternalUrl } from '$lib/utils/url'; import { getEditorUri, openExternalUrl } from '$lib/utils/url';
import { getContextStoreBySymbol } from '@gitbutler/shared/context'; import { getContextStoreBySymbol } from '@gitbutler/shared/context';
import { getContext } from '@gitbutler/shared/context'; import { getContext } from '@gitbutler/shared/context';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -23,7 +23,11 @@
const unsubscribeopenInEditor = listen<string>( const unsubscribeopenInEditor = listen<string>(
'menu://project/open-in-vscode/clicked', 'menu://project/open-in-vscode/clicked',
async () => { async () => {
const path = `${$userSettings.defaultCodeEditor.schemeIdentifer}://file${project.vscodePath}?windowId=_blank`; const path = getEditorUri({
schemeId: $userSettings.defaultCodeEditor.schemeIdentifer,
path: [project.vscodePath],
searchParams: { windowId: '_blank' }
});
openExternalUrl(path); openExternalUrl(path);
} }
); );

View File

@ -4,7 +4,7 @@
import { BaseBranch } from '$lib/baseBranch/baseBranch'; import { BaseBranch } from '$lib/baseBranch/baseBranch';
import { getGitHost } from '$lib/gitHost/interface/gitHost'; import { getGitHost } from '$lib/gitHost/interface/gitHost';
import { SETTINGS, type Settings } from '$lib/settings/userSettings'; import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import { openExternalUrl } from '$lib/utils/url'; import { getEditorUri, openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { getContext, getContextStore, getContextStoreBySymbol } from '@gitbutler/shared/context'; import { getContext, getContextStore, getContextStoreBySymbol } from '@gitbutler/shared/context';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
@ -18,9 +18,12 @@
const project = getContext(Project); const project = getContext(Project);
async function openInEditor() { async function openInEditor() {
openExternalUrl( const path = getEditorUri({
`${$userSettings.defaultCodeEditor.schemeIdentifer}://file${project.vscodePath}/?windowId=_blank` schemeId: $userSettings.defaultCodeEditor.schemeIdentifer,
); path: [project.vscodePath],
searchParams: { windowId: '_blank' }
});
openExternalUrl(path);
} }
</script> </script>

View File

@ -7,7 +7,7 @@
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte'; import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import { SETTINGS, type Settings } from '$lib/settings/userSettings'; import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher'; import { UncommitedFilesWatcher } from '$lib/uncommitedFiles/watcher';
import { openExternalUrl } from '$lib/utils/url'; import { getEditorUri, openExternalUrl } from '$lib/utils/url';
import { Commit, type RemoteFile } from '$lib/vbranches/types'; import { Commit, type RemoteFile } from '$lib/vbranches/types';
import { getContextStoreBySymbol } from '@gitbutler/shared/context'; import { getContextStoreBySymbol } from '@gitbutler/shared/context';
import { getContext } from '@gitbutler/shared/context'; import { getContext } from '@gitbutler/shared/context';
@ -16,7 +16,6 @@
import InfoButton from '@gitbutler/ui/InfoButton.svelte'; import InfoButton from '@gitbutler/ui/InfoButton.svelte';
import Avatar from '@gitbutler/ui/avatar/Avatar.svelte'; import Avatar from '@gitbutler/ui/avatar/Avatar.svelte';
import FileListItem from '@gitbutler/ui/file/FileListItem.svelte'; import FileListItem from '@gitbutler/ui/file/FileListItem.svelte';
import { join } from '@tauri-apps/api/path';
import type { FileStatus } from '@gitbutler/ui/file/types'; import type { FileStatus } from '@gitbutler/ui/file/types';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
@ -164,8 +163,11 @@
async function openAllConflictedFiles() { async function openAllConflictedFiles() {
for (const file of conflictedFiles) { for (const file of conflictedFiles) {
const absPath = await join(project.vscodePath, file.path); const path = getEditorUri({
openExternalUrl(`${$userSettings.defaultCodeEditor.schemeIdentifer}://file${absPath}`); schemeId: $userSettings.defaultCodeEditor.schemeIdentifer,
path: [project.vscodePath, file.path]
});
openExternalUrl(path);
} }
} }
</script> </script>

View File

@ -6,7 +6,7 @@
import { SETTINGS, type Settings } from '$lib/settings/userSettings'; import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import { computeFileStatus } from '$lib/utils/fileStatus'; import { computeFileStatus } from '$lib/utils/fileStatus';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url'; import { getEditorUri, openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { isAnyFile, LocalFile } from '$lib/vbranches/types'; import { isAnyFile, LocalFile } from '$lib/vbranches/types';
import { getContextStoreBySymbol } from '@gitbutler/shared/context'; import { getContextStoreBySymbol } from '@gitbutler/shared/context';
@ -108,10 +108,11 @@
try { try {
if (!project) return; if (!project) return;
for (let file of item.files) { for (let file of item.files) {
const absPath = await join(project.vscodePath, file.path); const path = getEditorUri({
openExternalUrl( schemeId: $userSettings.defaultCodeEditor.schemeIdentifer,
`${$userSettings.defaultCodeEditor.schemeIdentifer}://file${absPath}` path: [project.vscodePath, file.path]
); });
openExternalUrl(path);
} }
contextMenu.close(); contextMenu.close();
} catch { } catch {

View File

@ -3,7 +3,7 @@
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte'; import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte'; import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
import { SETTINGS, type Settings } from '$lib/settings/userSettings'; import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import { openExternalUrl } from '$lib/utils/url'; import { getEditorUri, openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { getContextStoreBySymbol } from '@gitbutler/shared/context'; import { getContextStoreBySymbol } from '@gitbutler/shared/context';
import { getContext } from '@gitbutler/shared/context'; import { getContext } from '@gitbutler/shared/context';
@ -49,9 +49,12 @@
label="Open in {$userSettings.defaultCodeEditor.displayName}" label="Open in {$userSettings.defaultCodeEditor.displayName}"
onclick={() => { onclick={() => {
if (projectPath) { if (projectPath) {
openExternalUrl( const path = getEditorUri({
`${$userSettings.defaultCodeEditor.schemeIdentifer}://file${projectPath}/${filePath}:${item.lineNumber}` schemeId: $userSettings.defaultCodeEditor.schemeIdentifer,
); path: [projectPath, filePath],
line: item.lineNumber
});
openExternalUrl(path);
} }
contextMenu?.close(); contextMenu?.close();
}} }}

View File

@ -1,4 +1,4 @@
import { remoteUrlIsHttp, convertRemoteToWebUrl } from '$lib/utils/url'; import { remoteUrlIsHttp, convertRemoteToWebUrl, getEditorUri } from '$lib/utils/url';
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
describe.concurrent('cleanUrl', () => { describe.concurrent('cleanUrl', () => {
@ -42,3 +42,71 @@ describe.concurrent('cleanUrl', () => {
expect(remoteUrlIsHttp(remoteUrl)).toBe(false); expect(remoteUrlIsHttp(remoteUrl)).toBe(false);
}); });
}); });
describe.concurrent('getEditorUri', () => {
test('it should handle editor path with no search params', () => {
expect(getEditorUri({ schemeId: 'vscode', path: ['/path', 'to', 'file'] })).toEqual(
'vscode://file/path/to/file'
);
});
test('it should handle editor path with search params', () => {
expect(
getEditorUri({
schemeId: 'vscode',
path: ['/path', 'to', 'file'],
searchParams: { something: 'cool' }
})
).toEqual('vscode://file/path/to/file?something=cool');
});
test('it should handle editor path with search params with special characters', () => {
expect(
getEditorUri({
schemeId: 'vscode',
path: ['/path', 'to', 'file'],
searchParams: {
search: 'hello world',
what: 'bye-&*%*\\ded-yeah'
}
})
).toEqual('vscode://file/path/to/file?search=hello+world&what=bye-%26*%25*%5Cded-yeah');
});
test('it should handle editor path with search params with line number', () => {
expect(
getEditorUri({
schemeId: 'vscode',
path: ['/path', 'to', 'file'],
line: 10
})
).toEqual('vscode://file/path/to/file:10');
});
test('it should handle editor path with search params with line and column number', () => {
expect(
getEditorUri({
schemeId: 'vscode',
path: ['/path', 'to', 'file'],
searchParams: {
another: 'thing'
},
line: 10,
column: 20
})
).toEqual('vscode://file/path/to/file:10:20?another=thing');
});
test('it should ignore the column if there is no line number', () => {
expect(
getEditorUri({
schemeId: 'vscode',
path: ['/path', 'to', 'file'],
searchParams: {
another: 'thing'
},
column: 20
})
).toEqual('vscode://file/path/to/file?another=thing');
});
});

View File

@ -3,6 +3,8 @@ import { showToast } from '$lib/notifications/toasts';
import GitUrlParse from 'git-url-parse'; import GitUrlParse from 'git-url-parse';
import { posthog } from 'posthog-js'; import { posthog } from 'posthog-js';
const SEPARATOR = '/';
export async function openExternalUrl(href: string) { export async function openExternalUrl(href: string) {
try { try {
await invoke<void>('open_url', { url: href }); await invoke<void>('open_url', { url: href });
@ -39,3 +41,30 @@ export function remoteUrlIsHttp(url: string): boolean {
return httpProtocols.includes(gitRemote.protocol); return httpProtocols.includes(gitRemote.protocol);
} }
export interface EditorUriParams {
schemeId: string;
path: string[];
searchParams?: Record<string, string>;
line?: number;
column?: number;
}
export function getEditorUri(params: EditorUriParams): string {
const searchParamsString = new URLSearchParams(params.searchParams).toString();
// Separator is always a forward slash for editor paths, even on Windows
const pathString = params.path.join(SEPARATOR);
let positionSuffix = '';
if (params.line !== undefined) {
positionSuffix += `:${params.line}`;
// Column is only valid if line is present
if (params.column !== undefined) {
positionSuffix += `:${params.column}`;
}
}
const searchSuffix = searchParamsString ? `?${searchParamsString}` : '';
return `${params.schemeId}://file${pathString}${positionSuffix}${searchSuffix}`;
}