Skip to content
This repository was archived by the owner on Jan 22, 2019. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"reactstrap": "^5.0.0-beta.2",
"rxjs": "^6.3.2",
"socket.io-client": "^2.1.1",
"sourcegraph": "^17.1.0",
"sourcegraph": "^18.0.0",
"string-score": "^1.0.1",
"textarea-caret": "^3.1.0",
"ts-key-enum": "^2.0.0",
Expand Down
8 changes: 5 additions & 3 deletions src/browser/storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable } from 'rxjs'
import { EMPTY, Observable } from 'rxjs'
import { shareReplay } from 'rxjs/operators'
import SafariStorageArea, { SafariSettingsChangeMessage, stringifyStorageArea } from './safari/StorageArea'
import { StorageChange, StorageItems } from './types'
Expand Down Expand Up @@ -82,6 +82,8 @@ const observe = (area: chrome.storage.StorageArea) => <T extends keyof StorageIt
})
}).pipe(shareReplay(1))

const noopObserve = () => EMPTY

const throwNoopErr = () => {
throw new Error('do not call browser extension apis from an in page script')
}
Expand Down Expand Up @@ -148,12 +150,12 @@ export default ((): Storage => {
getSync: throwNoopErr,
getSyncItem: throwNoopErr,
setSync: throwNoopErr,
observeSync: throwNoopErr,
observeSync: noopObserve,
onChanged: throwNoopErr,
getLocal: throwNoopErr,
getLocalItem: throwNoopErr,
setLocal: throwNoopErr,
observeLocal: throwNoopErr,
observeLocal: noopObserve,
addSyncMigration: throwNoopErr,
addLocalMigration: throwNoopErr,
}
Expand Down
214 changes: 169 additions & 45 deletions src/libs/code_intelligence/code_intelligence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ import { HoverMerged } from '@sourcegraph/codeintellify/lib/types'
import { toPrettyBlobURL } from '@sourcegraph/codeintellify/lib/url'
import * as React from 'react'
import { render } from 'react-dom'
import { animationFrameScheduler, Observable, of, Subject, Subscription } from 'rxjs'
import { animationFrameScheduler, BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs'
import { filter, map, mergeMap, observeOn, withLatestFrom } from 'rxjs/operators'

import { createJumpURLFetcher } from '../../shared/backend/lsp'
import { lspViaAPIXlang } from '../../shared/backend/lsp'
import { TextDocumentItem } from 'sourcegraph/module/client/types/textDocument'
import { Disposable } from 'vscode-jsonrpc'
import { createJumpURLFetcher, createLSPFromExtensions } from '../../shared/backend/lsp'
import { lspViaAPIXlang, toTextDocumentIdentifier } from '../../shared/backend/lsp'
import { ButtonProps, CodeViewToolbar } from '../../shared/components/CodeViewToolbar'
import { eventLogger, sourcegraphUrl } from '../../shared/util/context'
import { AbsoluteRepoFile } from '../../shared/repo'
import { eventLogger, getModeFromPath, sourcegraphUrl, useExtensions } from '../../shared/util/context'
import { githubCodeHost } from '../github/code_intelligence'
import { gitlabCodeHost } from '../gitlab/code_intelligence'
import { phabricatorCodeHost } from '../phabricator/code_intelligence'
import { findCodeViews } from './code_views'
import { findCodeViews, getContentOfCodeView } from './code_views'
import { applyDecoration, Controllers, initializeExtensions } from './extensions'
import { initSearch, SearchFeature } from './search'

/**
Expand Down Expand Up @@ -57,6 +61,19 @@ export interface CodeView {
adjustPosition?: PositionAdjuster
/** Props for styling the buttons in the `CodeViewToolbar`. */
toolbarButtonProps?: ButtonProps

isDiff?: boolean

/** Gets the 1-indexed range of the code view */
getLineRanges: (
codeView: HTMLElement,
part?: DiffPart
) => {
/** The first line shown in the code view. */
start: number
/** The last line shown in the code view. */
end: number
}[]
}

export type CodeViewWithOutSelector = Pick<CodeView, Exclude<keyof CodeView, 'selector'>>
Expand All @@ -71,6 +88,11 @@ interface OverlayPosition {
left: number
}

/**
* A function that gets the mount location for elements being mounted to the DOM.
*/
export type MountGetter = () => HTMLElement

/** Information for adding code intelligence to code views on arbitrary code hosts. */
export interface CodeHost {
/**
Expand Down Expand Up @@ -106,6 +128,13 @@ export interface CodeHost {
* Implementation of the search feature for a code host.
*/
search?: SearchFeature

// Extensions related input

/**
* Get the DOM element where we'll mount the command palette for extensions.
*/
getCommandPaletteMount?: MountGetter
}

export interface FileInfo {
Expand Down Expand Up @@ -156,7 +185,19 @@ export interface FileInfo {
*
* @param codeHost
*/
function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
function initCodeIntelligence(
codeHost: CodeHost,
documents: BehaviorSubject<TextDocumentItem[] | null>
): {
hoverifier: Hoverifier
controllers: Partial<Controllers>
} {
const { extensionsContextController, extensionsController }: Partial<Controllers> =
useExtensions && codeHost.getCommandPaletteMount
? initializeExtensions(codeHost.getCommandPaletteMount, documents)
: {}
const simpleProviderFns = extensionsController ? createLSPFromExtensions(extensionsController) : lspViaAPIXlang

/** Emits when the go to definition button was clicked */
const goToDefinitionClicks = new Subject<MouseEvent>()
const nextGoToDefinitionClick = (event: MouseEvent) => goToDefinitionClicks.next(event)
Expand Down Expand Up @@ -185,7 +226,7 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {

const relativeElement = document.body

const fetchJumpURL = createJumpURLFetcher(lspViaAPIXlang.fetchDefinition, toPrettyBlobURL)
const fetchJumpURL = createJumpURLFetcher(simpleProviderFns.fetchDefinition, toPrettyBlobURL)

const containerComponentUpdates = new Subject<void>()

Expand All @@ -202,7 +243,7 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
location.href = path
},
fetchHover: ({ line, character, part, ...rest }) =>
lspViaAPIXlang
simpleProviderFns
.fetchHover({ ...rest, position: { line, character } })
.pipe(map(hover => (hover ? (hover as HoverMerged) : hover))),
fetchJumpURL,
Expand Down Expand Up @@ -260,7 +301,7 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {

render(<HoverOverlayContainer />, overlayMount)

return { hoverifier }
return { hoverifier, controllers: { extensionsContextController, extensionsController } }
}

/**
Expand All @@ -277,10 +318,19 @@ function handleCodeHost(codeHost: CodeHost): Subscription {
initSearch(codeHost.search)
}

const { hoverifier } = initCodeIntelligence(codeHost)
const documentsSubject = new BehaviorSubject<TextDocumentItem[] | null>([])
const {
hoverifier,
controllers: { extensionsContextController, extensionsController },
} = initCodeIntelligence(codeHost, documentsSubject)

const subscriptions = new Subscription()

subscriptions.add(hoverifier)

// Keeps track of all documents on the page since calling this function (should be once per page).
let documents: TextDocumentItem[] = []

subscriptions.add(
of(document.body)
.pipe(
Expand All @@ -290,45 +340,119 @@ function handleCodeHost(codeHost: CodeHost): Subscription {
),
observeOn(animationFrameScheduler)
)
.subscribe(({ codeView, info, dom, adjustPosition, getToolbarMount, toolbarButtonProps }) => {
const resolveContext: ContextResolver = ({ part }) => ({
repoPath: part === 'base' ? info.baseRepoPath || info.repoPath : info.repoPath,
commitID: part === 'base' ? info.baseCommitID! : info.commitID,
filePath: part === 'base' ? info.baseFilePath || info.filePath : info.filePath,
rev: part === 'base' ? info.baseRev || info.baseCommitID! : info.rev || info.commitID,
})

subscriptions.add(
hoverifier.hoverify({
dom,
positionEvents: of(codeView).pipe(findPositionsFromEvents(dom)),
resolveContext,
adjustPosition,
})
)

codeView.classList.add('sg-mounted')
.subscribe(
({
codeView,
info,
isDiff,
getLineRanges,
dom,
adjustPosition,
getToolbarMount,
toolbarButtonProps,
}) => {
const toURIWithPath = (ctx: AbsoluteRepoFile) =>
`git://${ctx.repoPath}?${ctx.commitID}#${ctx.filePath}`

if (extensionsController) {
const { content, baseContent } = getContentOfCodeView(codeView, { isDiff, getLineRanges, dom })

documents = [
// All the currently open documents
...documents,
// Either a normal file, or HEAD when codeView is a diff
{
uri: toURIWithPath(info),
languageId: getModeFromPath(info.filePath) || 'could not determine mode',
text: content,
},
// When codeView is a diff, add BASE too
...(baseContent && info.baseRepoPath && info.baseCommitID && info.baseFilePath
? [
{
uri: toURIWithPath({
repoPath: info.baseRepoPath,
commitID: info.baseCommitID,
filePath: info.baseFilePath,
}),
languageId: getModeFromPath(info.filePath) || 'could not determine mode',
text: baseContent,
},
]
: []),
]

if (extensionsController && !info.baseCommitID) {
let oldDecorations: Disposable[] = []

extensionsController.registries.textDocumentDecoration
.getDecorations(toTextDocumentIdentifier(info))
.subscribe(decorations => {
for (const old of oldDecorations) {
old.dispose()
}
oldDecorations = []
for (const decoration of decorations || []) {
try {
oldDecorations.push(
applyDecoration(dom, {
codeView,
decoration,
})
)
} catch (e) {
console.warn(e)
}
}
})
}

if (!getToolbarMount) {
return
}
documentsSubject.next(documents)
}

const mount = getToolbarMount(codeView)
const resolveContext: ContextResolver = ({ part }) => ({
repoPath: part === 'base' ? info.baseRepoPath || info.repoPath : info.repoPath,
commitID: part === 'base' ? info.baseCommitID! : info.commitID,
filePath: part === 'base' ? info.baseFilePath || info.filePath : info.filePath,
rev: part === 'base' ? info.baseRev || info.baseCommitID! : info.rev || info.commitID,
})

render(
<CodeViewToolbar
{...info}
buttonProps={
toolbarButtonProps || {
className: '',
style: {},
subscriptions.add(
hoverifier.hoverify({
dom,
positionEvents: of(codeView).pipe(findPositionsFromEvents(dom)),
resolveContext,
adjustPosition,
})
)

codeView.classList.add('sg-mounted')

if (!getToolbarMount) {
return
}

const mount = getToolbarMount(codeView)

render(
<CodeViewToolbar
{...info}
extensions={extensionsContextController}
extensionsController={extensionsController}
buttonProps={
toolbarButtonProps || {
className: '',
style: {},
}
}
}
simpleProviderFns={lspViaAPIXlang}
/>,
mount
)
})
simpleProviderFns={
extensionsController ? createLSPFromExtensions(extensionsController) : lspViaAPIXlang
}
/>,
mount
)
}
)
)

return subscriptions
Expand Down
42 changes: 41 additions & 1 deletion src/libs/code_intelligence/code_views.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { last, range } from 'lodash'
import { from, merge, Observable, of, Subject } from 'rxjs'
import { filter, map, mergeMap } from 'rxjs/operators'

import { CodeHost, ResolvedCodeView } from './code_intelligence'
import { DiffPart } from '@sourcegraph/codeintellify'
import { CodeHost, CodeView, ResolvedCodeView } from './code_intelligence'

/**
* Emits a ResolvedCodeView when it's DOM element is on or about to be on the page.
Expand Down Expand Up @@ -121,3 +123,41 @@ export const findCodeViews = (codeHost: CodeHost, watchChildrenModifications = t
filter(({ codeView }) => !codeView.classList.contains('sg-mounted'))
)
}

export interface CodeViewContent {
content: string
baseContent?: string
}

export const getContentOfCodeView = (
codeView: HTMLElement,
info: Pick<CodeView, 'dom' | 'isDiff' | 'getLineRanges'>
): CodeViewContent => {
const getContent = (part?: DiffPart): string => {
const lines = new Map<number, string>()
let min = 1
let max = 1

for (const { start, end } of info.getLineRanges(codeView, part)) {
for (const line of range(start, end + 1)) {
min = Math.min(min, line)
max = Math.max(max, line)

const codeElement = info.dom.getCodeElementFromLineNumber(codeView, line, part)
if (codeElement) {
lines.set(line, codeElement.textContent || '')
}
}
}

return range(min, max + 1)
.map(line => lines.get(line) || '\n')
.map(content => (last(content) === '\n' ? content : `${content}\n`))
.join('')
}

return {
content: getContent(info.isDiff ? 'head' : undefined),
baseContent: info.isDiff ? getContent('base') : undefined,
}
}
Loading