mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:31:43 +00:00
feat: popover to configure editor (#62452)
Co-authored-by: Felix Kling <felix@felix-kling.de>
This commit is contained in:
parent
7c15db348d
commit
d8284e34fe
@ -32,3 +32,7 @@ export function isWindowsPlatform(): boolean {
|
||||
// Examples: 'Win32', 'Windows', 'Win64'
|
||||
return typeof window !== 'undefined' && window.navigator.platform.includes('Win')
|
||||
}
|
||||
|
||||
export function getPlatform(): 'windows' | 'mac' | 'linux' | 'other' {
|
||||
return isWindowsPlatform() ? 'windows' : isMacPlatform() ? 'mac' : isLinuxPlatform() ? 'linux' : 'other'
|
||||
}
|
||||
|
||||
@ -12,7 +12,12 @@ export { logger } from '@sourcegraph/common/src/util/logger'
|
||||
export { isSafari } from '@sourcegraph/common/src/util/browserDetection'
|
||||
export { isExternalLink, type LineOrPositionOrRange, SourcegraphURL } from '@sourcegraph/common/src/util/url'
|
||||
export { parseJSONCOrError } from '@sourcegraph/common/src/util/jsonc'
|
||||
export { isWindowsPlatform, isMacPlatform, isLinuxPlatform } from '@sourcegraph/common/src/util/browserDetection'
|
||||
export {
|
||||
isWindowsPlatform,
|
||||
isMacPlatform,
|
||||
isLinuxPlatform,
|
||||
getPlatform,
|
||||
} from '@sourcegraph/common/src/util/browserDetection'
|
||||
|
||||
let highlightingLoaded = false
|
||||
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { getEditor, parseBrowserRepoURL, buildRepoBaseNameAndPath, buildEditorUrl } from '$lib/web'
|
||||
import {
|
||||
getEditor,
|
||||
parseBrowserRepoURL,
|
||||
buildRepoBaseNameAndPath,
|
||||
buildEditorUrl,
|
||||
isProjectPathValid,
|
||||
} from '$lib/web'
|
||||
import { getEditorSettingsErrorMessage } from './build-url'
|
||||
import Tooltip from '$lib/Tooltip.svelte'
|
||||
import EditorIcon from '$lib/repo/open-in-editor/EditorIcon.svelte'
|
||||
import { settings } from '$lib/stores'
|
||||
import { page } from '$app/stores'
|
||||
import type { ExternalRepository } from '$lib/graphql-types'
|
||||
import type { ExternalRepository, SettingsEdit } from '$lib/graphql-types'
|
||||
import DefaultEditorIcon from '$lib/repo/open-in-editor/DefaultEditorIcon.svelte'
|
||||
import Popover from '$lib/Popover.svelte';
|
||||
import { Button } from '$lib/wildcard';
|
||||
import { supportedEditors } from '$lib/web';
|
||||
import { getPlatform } from '$lib/common';
|
||||
|
||||
export let externalServiceType: ExternalRepository['serviceType'] = ''
|
||||
export let updateUserSetting: (edit: SettingsEdit) => Promise<void>;
|
||||
|
||||
$: openInEditor = $settings?.openInEditor
|
||||
|
||||
@ -20,6 +31,48 @@
|
||||
|
||||
$: ({ repoName, filePath, position, range } = parseBrowserRepoURL($page.url.toString()))
|
||||
$: start = position ?? range?.start
|
||||
|
||||
$: defaultProjectPath = ''
|
||||
$: selectedEditorId = undefined
|
||||
|
||||
$: areSettingsValid = !!selectedEditorId && isProjectPathValid(defaultProjectPath);
|
||||
|
||||
let isSaving = false;
|
||||
$: handleEditorUpdate = async (): Promise<void> => {
|
||||
if (!selectedEditorId || !defaultProjectPath) {
|
||||
return;
|
||||
}
|
||||
isSaving = true;
|
||||
try {
|
||||
await updateUserSetting({
|
||||
value: defaultProjectPath,
|
||||
keyPath: [{property: 'openInEditor'}, {property: 'projectPaths.default'}],
|
||||
});
|
||||
await updateUserSetting({
|
||||
value: [selectedEditorId],
|
||||
keyPath: [{property: 'openInEditor'}, {property: 'editorIds'}],
|
||||
});
|
||||
|
||||
openInEditor = {
|
||||
editorIds: [selectedEditorId],
|
||||
'projectPaths.default': defaultProjectPath,
|
||||
}
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemAwareProjectPathExample(suffix?: string) {
|
||||
switch (getPlatform()) {
|
||||
case 'windows':
|
||||
return 'C:\\Users\\username\\Projects' + (suffix ? `\\${suffix}` : '');
|
||||
case 'linux':
|
||||
return '/home/username/Projects' + (suffix ? `/${suffix}` : '');
|
||||
case 'mac':
|
||||
default:
|
||||
return '/Users/username/Projects' + (suffix ? `/${suffix}` : '');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if editors}
|
||||
@ -27,6 +80,7 @@
|
||||
{#if editor}
|
||||
<Tooltip tooltip={`Open in ${editor.name}`}>
|
||||
<a
|
||||
class="action-href"
|
||||
href={buildEditorUrl(
|
||||
buildRepoBaseNameAndPath(repoName, externalServiceType, filePath),
|
||||
start,
|
||||
@ -37,23 +91,72 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<EditorIcon editorId={editor.id} />
|
||||
<EditorIcon editorId={editor.id}/>
|
||||
<span data-action-label> Editor </span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if editorSettingsErrorMessage}
|
||||
<Tooltip tooltip={editorSettingsErrorMessage}>
|
||||
<a href="/help/integration/open_in_editor" target="_blank">
|
||||
<DefaultEditorIcon />
|
||||
<span data-action-label> Editor </span>
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Popover let:registerTrigger let:toggle placement="left-start">
|
||||
<Tooltip tooltip="Set your preferred editor">
|
||||
<span use:registerTrigger on:click={() => toggle()}>
|
||||
<DefaultEditorIcon/>
|
||||
<span data-action-label> Editor </span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<div slot="content" class="open-in-editor-popover">
|
||||
<form on:submit={handleEditorUpdate} novalidate>
|
||||
<h3>Set your preferred editor</h3>
|
||||
<p>
|
||||
Open this and other files directly in your editor. Set your path and editor to get started. Update
|
||||
any time in your user settings.
|
||||
</p>
|
||||
<label>
|
||||
Default projects path
|
||||
<input
|
||||
id="OpenInEditorForm-projectPath"
|
||||
type="text"
|
||||
name="projectPath"
|
||||
placeholder="/Users/username/projects"
|
||||
required
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck={false}
|
||||
bind:value={defaultProjectPath}
|
||||
class="form-input"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<p class="small form-info">
|
||||
The directory that contains your repository checkouts. For example, if this repository is
|
||||
checked out to <code>{`${getSystemAwareProjectPathExample('cody')}`}</code>, then set your default projects path
|
||||
to <code>{getSystemAwareProjectPathExample()}</code>.
|
||||
</p>
|
||||
<label>
|
||||
Editor
|
||||
<select class="form-input" id="OpenInEditorForm-editor" bind:value={selectedEditorId}>
|
||||
<option value=""></option>
|
||||
{#each supportedEditors.sort((a, b) => a.name.localeCompare(b.name)).filter(editor => editor.id !== 'custom') as editor}
|
||||
<option value={editor.id}>{editor.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<p class="small form-info">Use a different editor?{' '}
|
||||
<a href="/help/integration/open_in_editor" target="_blank" rel="noreferrer noopener">Set up a
|
||||
different editor</a>
|
||||
</p>
|
||||
<Button variant="primary" type="submit" disabled={!areSettingsValid || isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
a {
|
||||
.action-href {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -66,4 +169,27 @@
|
||||
color: var(--text-title);
|
||||
}
|
||||
}
|
||||
|
||||
.open-in-editor-popover {
|
||||
isolation: isolate;
|
||||
width: 25rem;
|
||||
padding: 1.25rem 1rem;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.form-info {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -2,10 +2,18 @@ import { error, redirect } from '@sveltejs/kit'
|
||||
|
||||
import { isErrorLike, parseJSONCOrError } from '$lib/common'
|
||||
import { getGraphQLClient } from '$lib/graphql'
|
||||
import type { SettingsEdit } from '$lib/graphql-types'
|
||||
import type { Settings } from '$lib/shared'
|
||||
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { Init, EvaluatedFeatureFlagsQuery, GlobalAlertsSiteFlags, DisableSveltePrototype } from './layout.gql'
|
||||
import {
|
||||
Init,
|
||||
EvaluatedFeatureFlagsQuery,
|
||||
GlobalAlertsSiteFlags,
|
||||
DisableSveltePrototype,
|
||||
EditSettings,
|
||||
LatestSettingsQuery,
|
||||
} from './layout.gql'
|
||||
import { dotcomMainNavigation, mainNavigation } from './navigation'
|
||||
|
||||
// Disable server side rendering for the whole app
|
||||
@ -62,5 +70,32 @@ export const load: LayoutLoad = async ({ fetch }) => {
|
||||
throw new Error(`Failed to disable svelte feature flags: ${result.error}`)
|
||||
}
|
||||
},
|
||||
updateUserSetting: async (edit: SettingsEdit): Promise<void> => {
|
||||
// We have to set network-only here, because otherwise the client will reuse a previously cached value
|
||||
const latestSettings = await client.query(LatestSettingsQuery, {}, { requestPolicy: 'network-only', fetch })
|
||||
if (!latestSettings.data || latestSettings.error) {
|
||||
throw new Error(`Failed to fetch latest settings during editor update: ${latestSettings.error}`)
|
||||
}
|
||||
const userSetting = latestSettings.data.viewerSettings.subjects.find(s => s.__typename === 'User')
|
||||
if (!userSetting) {
|
||||
throw new Error('Failed to find user settings subject')
|
||||
}
|
||||
const lastID = userSetting.latestSettings?.id
|
||||
if (!lastID) {
|
||||
throw new Error('Failed to get new last ID from settings result')
|
||||
}
|
||||
const mutationResult = await client.mutation(
|
||||
EditSettings,
|
||||
{
|
||||
lastID,
|
||||
subject: userSetting.id,
|
||||
edit,
|
||||
},
|
||||
{ requestPolicy: 'network-only', fetch }
|
||||
)
|
||||
if (!mutationResult.data || mutationResult.error) {
|
||||
throw new Error(`Failed to update editor path: ${mutationResult.error}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { BehaviorSubject, concatMap, from, map } from 'rxjs'
|
||||
|
||||
import { fetchBlameHunksMemoized, type BlameHunkData } from '@sourcegraph/web/src/repo/blame/shared'
|
||||
import { type BlameHunkData, fetchBlameHunksMemoized } from '@sourcegraph/web/src/repo/blame/shared'
|
||||
|
||||
import { SourcegraphURL } from '$lib/common'
|
||||
import { getGraphQLClient, mapOrThrow } from '$lib/graphql'
|
||||
@ -11,9 +11,9 @@ import { assertNonNullable } from '$lib/utils'
|
||||
import type { PageLoad, PageLoadEvent } from './$types'
|
||||
import {
|
||||
BlobDiffViewCommitQuery,
|
||||
BlobFileViewHighlightedFileQuery,
|
||||
BlobFileViewCommitQuery_revisionOverride,
|
||||
BlobFileViewBlobQuery,
|
||||
BlobFileViewCommitQuery_revisionOverride,
|
||||
BlobFileViewHighlightedFileQuery,
|
||||
} from './page.gql'
|
||||
|
||||
function loadDiffView({ params, url }: PageLoadEvent) {
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
<svelte:fragment slot="actions">
|
||||
{#await data.externalServiceType then externalServiceType}
|
||||
{#if externalServiceType && !isBinaryFile}
|
||||
<OpenInEditor {externalServiceType} />
|
||||
<OpenInEditor {externalServiceType} updateUserSetting={data.updateUserSetting} />
|
||||
{/if}
|
||||
{/await}
|
||||
{#if blob}
|
||||
|
||||
@ -42,3 +42,24 @@ fragment AuthenticatedUser on User {
|
||||
...GlobalNavigation_User
|
||||
...SearchInput_AuthenticatedUser
|
||||
}
|
||||
|
||||
mutation EditSettings($subject: ID!, $lastID: Int, $edit: SettingsEdit!) {
|
||||
settingsMutation(input: { subject: $subject, lastID: $lastID }) {
|
||||
editSettings(edit: $edit) {
|
||||
empty {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query LatestSettingsQuery {
|
||||
viewerSettings {
|
||||
subjects {
|
||||
id
|
||||
latestSettings {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user