Skip to content

Commit

Permalink
Open remote files locally in VSCode
Browse files Browse the repository at this point in the history
  • Loading branch information
thenamankumar committed Dec 27, 2024
1 parent 91ac295 commit 77c43bc
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 4 deletions.
13 changes: 13 additions & 0 deletions vscode/src/chat/chat-view/ChatController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
viewColumn: vscode.ViewColumn.Beside,
})
break
case 'openRemoteFile':
this.openRemoteFile(message.uri)
break
case 'newFile':
await handleCodeFromSaveToNewFile(message.text, this.editor)
break
Expand Down Expand Up @@ -1059,6 +1062,16 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
}
}

private async openRemoteFile(uri: vscode.Uri) {
const json = uri.toJSON()
json.scheme = 'sourcegraph'
const sourcegraphSchemaURI = vscode.Uri.from(json)

vscode.workspace
.openTextDocument(sourcegraphSchemaURI)
.then(doc => vscode.window.showTextDocument(doc))
}

private async getSearchScopesFromMentions(mentions: ContextItem[]): Promise<string[]> {
const validMentions = mentions.reduce(
(groups, mention) => {
Expand Down
57 changes: 57 additions & 0 deletions vscode/src/chat/chat-view/sourcegraphRemoteFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { graphqlClient, isError } from '@sourcegraph/cody-shared'
import { LRUCache } from 'lru-cache'
import * as vscode from 'vscode'

export class SourcegraphRemoteFileProvider
implements vscode.TextDocumentContentProvider, vscode.Disposable
{
private cache = new LRUCache<string, string>({ max: 128 })
private disposables: vscode.Disposable[] = []

constructor() {
this.disposables.push(vscode.workspace.registerTextDocumentContentProvider('sourcegraph', this))
}

async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
const content =
this.cache.get(uri.toString()) ||
(await SourcegraphRemoteFileProvider.getFileContentsFromURL(uri))

this.cache.set(uri.toString(), content)

return content
}

public dispose(): void {
this.cache.clear()
for (const disposable of this.disposables) {
disposable.dispose()
}
this.disposables = []
}

private static async getFileContentsFromURL(URL: vscode.Uri): Promise<string> {
const path = URL.path
const [repoRev = '', filePath] = path.split('/-/blob/')
let [repoName, rev = 'HEAD'] = repoRev.split('@')
repoName = repoName.replace(/^\/+/, '')

if (!repoName || !filePath) {
throw new Error('Invalid URI')
}

const dataOrError = await graphqlClient.getFileContents(repoName, filePath, rev)

if (isError(dataOrError)) {
throw new Error(dataOrError.message)
}

const content = dataOrError.repository?.commit?.file?.content

if (!content) {
throw new Error('File not found')
}

return content
}
}
1 change: 1 addition & 0 deletions vscode/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type WebviewMessage =
| { command: 'restoreHistory'; chatID: string }
| { command: 'links'; value: string }
| { command: 'openURI'; uri: Uri }
| { command: 'openRemoteFile'; uri: Uri }
| {
command: 'openFileLink'
uri: Uri
Expand Down
11 changes: 10 additions & 1 deletion vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { registerAutoEditTestRenderCommand } from './autoedits/renderer/mock-ren
import type { MessageProviderOptions } from './chat/MessageProvider'
import { ChatsController, CodyChatEditorViewType } from './chat/chat-view/ChatsController'
import { ContextRetriever } from './chat/chat-view/ContextRetriever'
import { SourcegraphRemoteFileProvider } from './chat/chat-view/sourcegraphRemoteFile'
import type { ChatIntentAPIClient } from './chat/context/chatIntentAPIClient'
import {
ACCOUNT_LIMITS_INFO_URL,
Expand Down Expand Up @@ -905,7 +906,15 @@ function registerChat(
extensionClient: platform.extensionClient,
})
const promptsManager = new PromptsManager({ chatsController })
disposables.push(ghostHintDecorator, editorManager, new CodeActionProvider(), promptsManager)
const sourcegraphRemoteFileProvider = new SourcegraphRemoteFileProvider()

disposables.push(
ghostHintDecorator,
editorManager,
new CodeActionProvider(),
promptsManager,
sourcegraphRemoteFileProvider
)

// Register a serializer for reviving the chat panel on reload
if (vscode.window.registerWebviewPanelSerializer) {
Expand Down
20 changes: 20 additions & 0 deletions vscode/webviews/components/codeSnippet/CodeSnippet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ import {
pluralize,
} from './utils'

import { CodyIDE } from '@sourcegraph/cody-shared'
import type {
NLSSearchFileMatch,
NLSSearchResult,
} from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql/client'
import type { Observable } from 'observable-fns'
import { useInView } from 'react-intersection-observer'
import { URI } from 'vscode-uri'
import { getVSCodeAPI } from '../../utils/VSCodeApi'
import { useConfig } from '../../utils/useConfig'
import styles from './CodeSnippet.module.css'

const DEFAULT_VISIBILITY_OFFSET = '500px'
Expand Down Expand Up @@ -123,6 +127,20 @@ export const FileMatchSearchResult: FC<PropsWithChildren<FileMatchSearchResultPr
const expandable = !showAllMatches && expandedHighlightCount > collapsedHighlightCount

useEffect(() => setExpanded(allExpanded || defaultExpanded), [allExpanded, defaultExpanded])
const {
clientCapabilities: { agentIDE },
} = useConfig()
const openRemoteFile = useCallback(() => {
if (agentIDE !== CodyIDE.VSCode) {
return
}

const uri = URI.parse(fileURL)
getVSCodeAPI().postMessage({
command: 'openRemoteFile',
uri,
})
}, [fileURL, agentIDE])

const handleVisibility = useCallback(
(inView: boolean, entry: IntersectionObserverEntry) => {
Expand Down Expand Up @@ -183,6 +201,7 @@ export const FileMatchSearchResult: FC<PropsWithChildren<FileMatchSearchResultPr
repoName={result.repository.name}
repoURL={repoAtRevisionURL}
filePath={result.file.path}
onFilePathClick={openRemoteFile}
pathMatchRanges={result.pathMatches ?? []}
fileURL={fileURL}
repoDisplayName={
Expand Down Expand Up @@ -231,6 +250,7 @@ export const FileMatchSearchResult: FC<PropsWithChildren<FileMatchSearchResultPr
serverEndpoint={serverEndpoint}
result={result}
grouped={expanded ? expandedGroups : collapsedGroups}
onLineClick={openRemoteFile}
/>
{expandable && (
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import { getFileMatchUrl } from '../utils'

import { CodeExcerpt } from './CodeExcerpt'

import { CodyIDE } from '@sourcegraph/cody-shared'
import type { NLSSearchFileMatch } from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql/client'
import { getVSCodeAPI } from '../../../utils/VSCodeApi'
import { useConfig } from '../../../utils/useConfig'
import resultStyles from '../CodeSnippet.module.css'
import styles from './FileMatchChildren.module.css'

interface FileMatchProps {
result: NLSSearchFileMatch
grouped: MatchGroup[]
serverEndpoint: string
onLineClick?: () => void
}

export const FileMatchChildren: FC<PropsWithChildren<FileMatchProps>> = props => {
const { result, grouped, serverEndpoint } = props
const { result, grouped, serverEndpoint, onLineClick } = props

const createCodeExcerptLink = (group: MatchGroup): string => {
const urlBuilder = SourcegraphURL.from(getFileMatchUrl(serverEndpoint, result))
Expand All @@ -39,15 +42,24 @@ export const FileMatchChildren: FC<PropsWithChildren<FileMatchProps>> = props =>
return urlBuilder.toString()
}

const {
clientCapabilities: { agentIDE },
} = useConfig()

const navigateToFile = useCallback(
(line: number) => {
if (agentIDE === CodyIDE.VSCode && onLineClick) {
onLineClick()
return
}

// TODO: this does not work on web as opening links from within a web worker does not work.
getVSCodeAPI().postMessage({
command: 'links',
value: `${getFileMatchUrl(serverEndpoint, result)}?L${line}`,
})
},
[serverEndpoint, result]
[serverEndpoint, result, onLineClick, agentIDE]
)

return (
Expand Down
11 changes: 10 additions & 1 deletion vscode/webviews/components/codeSnippet/components/RepoLink.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type * as React from 'react'
import { useEffect, useRef } from 'react'

import { CodyIDE } from '@sourcegraph/cody-shared'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { useConfig } from '../../../utils/useConfig'
import { cn } from '../../shadcn/utils'
import { highlightNode } from '../highlights'
import type { Range } from '../types'
Expand All @@ -17,6 +19,7 @@ interface Props {
isKeyboardSelectable?: boolean
collapsed: boolean
onToggleCollapse: () => void
onFilePathClick?: () => void
}

/**
Expand All @@ -35,6 +38,7 @@ export const RepoFileLink: React.FunctionComponent<React.PropsWithChildren<Props
isKeyboardSelectable,
collapsed,
onToggleCollapse,
onFilePathClick,
} = props

const [fileBase, fileName] = splitPath(filePath)
Expand All @@ -52,6 +56,10 @@ export const RepoFileLink: React.FunctionComponent<React.PropsWithChildren<Props
}
}, [pathMatchRanges, fileName])

const {
clientCapabilities: { agentIDE },
} = useConfig()

return (
<span className={cn(className, 'tw-flex tw-items-center tw-w-full')}>
{collapsed ? (
Expand All @@ -73,11 +81,12 @@ export const RepoFileLink: React.FunctionComponent<React.PropsWithChildren<Props
</a>
<span aria-hidden={true}></span>{' '}
<a
href={fileURL}
href={agentIDE === CodyIDE.VSCode ? '' : fileURL}
ref={containerElement}
target="_blank"
rel="noreferrer"
data-selectable-search-result={isKeyboardSelectable}
onClick={onFilePathClick}
>
{fileBase ? `${fileBase}/` : null}
<strong>{fileName}</strong>
Expand Down

0 comments on commit 77c43bc

Please sign in to comment.