From e8e8c624ddd108de8927a09ac4fd426c5e087772 Mon Sep 17 00:00:00 2001 From: Camden Cheek Date: Fri, 24 Jan 2025 09:47:21 -0700 Subject: [PATCH] omnibox: open results locally if possible (#6781) This modifies the behavior of clicking search results to link to local files if the result represents a file in your local repo. With the [recent changes](https://github.com/sourcegraph/sourcegraph/pull/2943) that heavily prefer results from your current repo, this should now be most results. (cherry picked from commit 7ed51f2a2abaa6fc253f753a053f66c3985ea0c3) --- vscode/src/chat/chat-view/ChatController.ts | 59 ++++++++++++++++--- vscode/src/chat/protocol.ts | 9 ++- vscode/src/repository/remoteRepos.ts | 20 +++++++ .../components/codeSnippet/CodeSnippet.tsx | 1 + 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 86e2e4a39a07..be27db9898e5 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -94,6 +94,7 @@ import { migrateAndNotifyForOutdatedModels } from '../../models/modelMigrator' import { logDebug, outputChannelLogger } from '../../output-channel-logger' import { hydratePromptText } from '../../prompts/prompt-hydration' import { listPromptTags, mergedPromptsAndLegacyCommands } from '../../prompts/prompts' +import { workspaceFolderForRepo } from '../../repository/remoteRepos' import { authProvider } from '../../services/AuthProvider' import { AuthProviderSimplified } from '../../services/AuthProviderSimplified' import { localStorage } from '../../services/LocalStorageProvider' @@ -363,7 +364,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv }) break case 'openRemoteFile': - this.openRemoteFile(message.uri) + this.openRemoteFile(message.uri, message.tryLocal ?? false) break case 'newFile': await handleCodeFromSaveToNewFile(message.text, this.editor) @@ -976,18 +977,23 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv return } - private openRemoteFile(uri: vscode.Uri) { - const json = uri.toJSON() - const searchParams = (json.query || '').split('&') + private async openRemoteFile(uri: vscode.Uri, tryLocal?: boolean) { + if (tryLocal) { + try { + await this.openSourcegraphUriAsLocalFile(uri) + return + } catch { + // Ignore error, just continue to opening the remote file + } + } - const sourcegraphSchemaURI = vscode.Uri.from({ - ...json, + const sourcegraphSchemaURI = uri.with({ query: '', scheme: 'codysourcegraph', }) // Supported line params examples: L42 (single line) or L42-45 (line range) - const lineParam = searchParams.find((value: string) => value.match(/^L\d+(?:-\d+)?$/)?.length) + const lineParam = this.extractLineParamFromURI(uri) const range = this.lineParamToRange(lineParam) vscode.workspace.openTextDocument(sourcegraphSchemaURI).then(async doc => { @@ -997,6 +1003,45 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv }) } + /** + * Attempts to open a Sourcegraph file URL as a local file in VS Code. + * Fails if the URI is not a valid Sourcegraph URL for a file or if the + * file does not belong to the current workspace. + */ + private async openSourcegraphUriAsLocalFile(uri: vscode.Uri): Promise { + const match = uri.path.match( + /^\/*(?[^@]*)(?@.*)?\/-\/blob\/(?.*)$/ + ) + if (!match || !match.groups) { + throw new Error('failed to extract repo name and file path') + } + const { repoName, filePath } = match.groups + + const workspaceFolder = await workspaceFolderForRepo(repoName) + if (!workspaceFolder) { + throw new Error('could not find workspace for repo') + } + + const lineParam = this.extractLineParamFromURI(uri) + const selectionStart = this.lineParamToRange(lineParam).start + // Opening the file with an active selection is awkward, so use a zero-length + // selection to focus the target line without highlighting anything + const selection = new vscode.Range(selectionStart, selectionStart) + + const fileUri = workspaceFolder.uri.with({ + path: `${workspaceFolder.uri.path}/${filePath}`, + }) + const document = await vscode.workspace.openTextDocument(fileUri) + await vscode.window.showTextDocument(document, { + selection, + preview: true, + }) + } + + private extractLineParamFromURI(uri: vscode.Uri): string | undefined { + return uri.query.split('&').find(key => key.match(/^L\d+(?:-\d+)?$/)) + } + private lineParamToRange(lineParam?: string | null): vscode.Range { const lines = (lineParam ?? '0') .replace('L', '') diff --git a/vscode/src/chat/protocol.ts b/vscode/src/chat/protocol.ts index 1aee7cd3fbdc..23877b850d19 100644 --- a/vscode/src/chat/protocol.ts +++ b/vscode/src/chat/protocol.ts @@ -74,7 +74,14 @@ export type WebviewMessage = | { command: 'restoreHistory'; chatID: string } | { command: 'links'; value: string } | { command: 'openURI'; uri: Uri } - | { command: 'openRemoteFile'; uri: Uri } + | { + // Open a file from a Sourcegraph URL + command: 'openRemoteFile' + uri: Uri + // Attempt to open the same file locally if we can map + // the repository to an open workspace. + tryLocal?: boolean | undefined | null + } | { command: 'openFileLink' uri: Uri diff --git a/vscode/src/repository/remoteRepos.ts b/vscode/src/repository/remoteRepos.ts index aa2408366142..e361d94e36c0 100644 --- a/vscode/src/repository/remoteRepos.ts +++ b/vscode/src/repository/remoteRepos.ts @@ -3,10 +3,12 @@ import { authStatus, combineLatest, debounceTime, + firstValueFrom, fromVSCodeEvent, graphqlClient, isError, type pendingOperation, + skipPendingOperation, startWith, switchMapReplayOperation, } from '@sourcegraph/cody-shared' @@ -89,3 +91,21 @@ export const remoteReposForAllWorkspaceFolders: Observable< } ) ) + +async function remoteReposForWorkspaceFolder(folder: vscode.WorkspaceFolder): Promise { + return firstValueFrom( + repoNameResolver.getRepoNamesContainingUri(folder.uri).pipe(skipPendingOperation()) + ) +} + +export async function workspaceFolderForRepo( + repoName: string +): Promise { + for (const folder of vscode.workspace.workspaceFolders ?? []) { + const remoteRepos = await remoteReposForWorkspaceFolder(folder) + if (remoteRepos.some(remoteRepo => remoteRepo === repoName)) { + return folder + } + } + return undefined +} diff --git a/vscode/webviews/components/codeSnippet/CodeSnippet.tsx b/vscode/webviews/components/codeSnippet/CodeSnippet.tsx index 4a8bf7ff87af..b2f80a4b1efb 100644 --- a/vscode/webviews/components/codeSnippet/CodeSnippet.tsx +++ b/vscode/webviews/components/codeSnippet/CodeSnippet.tsx @@ -149,6 +149,7 @@ export const FileMatchSearchResult: FC