Skip to content
This repository was archived by the owner on Jan 22, 2019. It is now read-only.

Commit d2ce8e4

Browse files
chrismwendtijsnow
authored andcommitted
feat: support Sourcegraph extensions in the new inject code (#199)
This PR brings support for diff views in Sourcegraph extensions.
1 parent f959f49 commit d2ce8e4

17 files changed

+2550
-142
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
"reactstrap": "^5.0.0-beta.2",
116116
"rxjs": "^6.3.2",
117117
"socket.io-client": "^2.1.1",
118-
"sourcegraph": "^17.1.0",
118+
"sourcegraph": "^18.0.0",
119119
"string-score": "^1.0.1",
120120
"textarea-caret": "^3.1.0",
121121
"ts-key-enum": "^2.0.0",

src/browser/storage.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Observable } from 'rxjs'
1+
import { EMPTY, Observable } from 'rxjs'
22
import { shareReplay } from 'rxjs/operators'
33
import SafariStorageArea, { SafariSettingsChangeMessage, stringifyStorageArea } from './safari/StorageArea'
44
import { StorageChange, StorageItems } from './types'
@@ -82,6 +82,8 @@ const observe = (area: chrome.storage.StorageArea) => <T extends keyof StorageIt
8282
})
8383
}).pipe(shareReplay(1))
8484

85+
const noopObserve = () => EMPTY
86+
8587
const throwNoopErr = () => {
8688
throw new Error('do not call browser extension apis from an in page script')
8789
}
@@ -148,12 +150,12 @@ export default ((): Storage => {
148150
getSync: throwNoopErr,
149151
getSyncItem: throwNoopErr,
150152
setSync: throwNoopErr,
151-
observeSync: throwNoopErr,
153+
observeSync: noopObserve,
152154
onChanged: throwNoopErr,
153155
getLocal: throwNoopErr,
154156
getLocalItem: throwNoopErr,
155157
setLocal: throwNoopErr,
156-
observeLocal: throwNoopErr,
158+
observeLocal: noopObserve,
157159
addSyncMigration: throwNoopErr,
158160
addLocalMigration: throwNoopErr,
159161
}

src/libs/code_intelligence/code_intelligence.tsx

+169-45
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@ import { HoverMerged } from '@sourcegraph/codeintellify/lib/types'
1515
import { toPrettyBlobURL } from '@sourcegraph/codeintellify/lib/url'
1616
import * as React from 'react'
1717
import { render } from 'react-dom'
18-
import { animationFrameScheduler, Observable, of, Subject, Subscription } from 'rxjs'
18+
import { animationFrameScheduler, BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs'
1919
import { filter, map, mergeMap, observeOn, withLatestFrom } from 'rxjs/operators'
2020

21-
import { createJumpURLFetcher } from '../../shared/backend/lsp'
22-
import { lspViaAPIXlang } from '../../shared/backend/lsp'
21+
import { TextDocumentItem } from 'sourcegraph/module/client/types/textDocument'
22+
import { Disposable } from 'vscode-jsonrpc'
23+
import { createJumpURLFetcher, createLSPFromExtensions } from '../../shared/backend/lsp'
24+
import { lspViaAPIXlang, toTextDocumentIdentifier } from '../../shared/backend/lsp'
2325
import { ButtonProps, CodeViewToolbar } from '../../shared/components/CodeViewToolbar'
24-
import { eventLogger, sourcegraphUrl } from '../../shared/util/context'
26+
import { AbsoluteRepoFile } from '../../shared/repo'
27+
import { eventLogger, getModeFromPath, sourcegraphUrl, useExtensions } from '../../shared/util/context'
2528
import { githubCodeHost } from '../github/code_intelligence'
2629
import { gitlabCodeHost } from '../gitlab/code_intelligence'
2730
import { phabricatorCodeHost } from '../phabricator/code_intelligence'
28-
import { findCodeViews } from './code_views'
31+
import { findCodeViews, getContentOfCodeView } from './code_views'
32+
import { applyDecoration, Controllers, initializeExtensions } from './extensions'
2933
import { initSearch, SearchFeature } from './search'
3034

3135
/**
@@ -57,6 +61,19 @@ export interface CodeView {
5761
adjustPosition?: PositionAdjuster
5862
/** Props for styling the buttons in the `CodeViewToolbar`. */
5963
toolbarButtonProps?: ButtonProps
64+
65+
isDiff?: boolean
66+
67+
/** Gets the 1-indexed range of the code view */
68+
getLineRanges: (
69+
codeView: HTMLElement,
70+
part?: DiffPart
71+
) => {
72+
/** The first line shown in the code view. */
73+
start: number
74+
/** The last line shown in the code view. */
75+
end: number
76+
}[]
6077
}
6178

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

91+
/**
92+
* A function that gets the mount location for elements being mounted to the DOM.
93+
*/
94+
export type MountGetter = () => HTMLElement
95+
7496
/** Information for adding code intelligence to code views on arbitrary code hosts. */
7597
export interface CodeHost {
7698
/**
@@ -106,6 +128,13 @@ export interface CodeHost {
106128
* Implementation of the search feature for a code host.
107129
*/
108130
search?: SearchFeature
131+
132+
// Extensions related input
133+
134+
/**
135+
* Get the DOM element where we'll mount the command palette for extensions.
136+
*/
137+
getCommandPaletteMount?: MountGetter
109138
}
110139

111140
export interface FileInfo {
@@ -156,7 +185,19 @@ export interface FileInfo {
156185
*
157186
* @param codeHost
158187
*/
159-
function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
188+
function initCodeIntelligence(
189+
codeHost: CodeHost,
190+
documents: BehaviorSubject<TextDocumentItem[] | null>
191+
): {
192+
hoverifier: Hoverifier
193+
controllers: Partial<Controllers>
194+
} {
195+
const { extensionsContextController, extensionsController }: Partial<Controllers> =
196+
useExtensions && codeHost.getCommandPaletteMount
197+
? initializeExtensions(codeHost.getCommandPaletteMount, documents)
198+
: {}
199+
const simpleProviderFns = extensionsController ? createLSPFromExtensions(extensionsController) : lspViaAPIXlang
200+
160201
/** Emits when the go to definition button was clicked */
161202
const goToDefinitionClicks = new Subject<MouseEvent>()
162203
const nextGoToDefinitionClick = (event: MouseEvent) => goToDefinitionClicks.next(event)
@@ -185,7 +226,7 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
185226

186227
const relativeElement = document.body
187228

188-
const fetchJumpURL = createJumpURLFetcher(lspViaAPIXlang.fetchDefinition, toPrettyBlobURL)
229+
const fetchJumpURL = createJumpURLFetcher(simpleProviderFns.fetchDefinition, toPrettyBlobURL)
189230

190231
const containerComponentUpdates = new Subject<void>()
191232

@@ -202,7 +243,7 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
202243
location.href = path
203244
},
204245
fetchHover: ({ line, character, part, ...rest }) =>
205-
lspViaAPIXlang
246+
simpleProviderFns
206247
.fetchHover({ ...rest, position: { line, character } })
207248
.pipe(map(hover => (hover ? (hover as HoverMerged) : hover))),
208249
fetchJumpURL,
@@ -260,7 +301,7 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
260301

261302
render(<HoverOverlayContainer />, overlayMount)
262303

263-
return { hoverifier }
304+
return { hoverifier, controllers: { extensionsContextController, extensionsController } }
264305
}
265306

266307
/**
@@ -277,10 +318,19 @@ function handleCodeHost(codeHost: CodeHost): Subscription {
277318
initSearch(codeHost.search)
278319
}
279320

280-
const { hoverifier } = initCodeIntelligence(codeHost)
321+
const documentsSubject = new BehaviorSubject<TextDocumentItem[] | null>([])
322+
const {
323+
hoverifier,
324+
controllers: { extensionsContextController, extensionsController },
325+
} = initCodeIntelligence(codeHost, documentsSubject)
281326

282327
const subscriptions = new Subscription()
283328

329+
subscriptions.add(hoverifier)
330+
331+
// Keeps track of all documents on the page since calling this function (should be once per page).
332+
let documents: TextDocumentItem[] = []
333+
284334
subscriptions.add(
285335
of(document.body)
286336
.pipe(
@@ -290,45 +340,119 @@ function handleCodeHost(codeHost: CodeHost): Subscription {
290340
),
291341
observeOn(animationFrameScheduler)
292342
)
293-
.subscribe(({ codeView, info, dom, adjustPosition, getToolbarMount, toolbarButtonProps }) => {
294-
const resolveContext: ContextResolver = ({ part }) => ({
295-
repoPath: part === 'base' ? info.baseRepoPath || info.repoPath : info.repoPath,
296-
commitID: part === 'base' ? info.baseCommitID! : info.commitID,
297-
filePath: part === 'base' ? info.baseFilePath || info.filePath : info.filePath,
298-
rev: part === 'base' ? info.baseRev || info.baseCommitID! : info.rev || info.commitID,
299-
})
300-
301-
subscriptions.add(
302-
hoverifier.hoverify({
303-
dom,
304-
positionEvents: of(codeView).pipe(findPositionsFromEvents(dom)),
305-
resolveContext,
306-
adjustPosition,
307-
})
308-
)
309-
310-
codeView.classList.add('sg-mounted')
343+
.subscribe(
344+
({
345+
codeView,
346+
info,
347+
isDiff,
348+
getLineRanges,
349+
dom,
350+
adjustPosition,
351+
getToolbarMount,
352+
toolbarButtonProps,
353+
}) => {
354+
const toURIWithPath = (ctx: AbsoluteRepoFile) =>
355+
`git://${ctx.repoPath}?${ctx.commitID}#${ctx.filePath}`
356+
357+
if (extensionsController) {
358+
const { content, baseContent } = getContentOfCodeView(codeView, { isDiff, getLineRanges, dom })
359+
360+
documents = [
361+
// All the currently open documents
362+
...documents,
363+
// Either a normal file, or HEAD when codeView is a diff
364+
{
365+
uri: toURIWithPath(info),
366+
languageId: getModeFromPath(info.filePath) || 'could not determine mode',
367+
text: content,
368+
},
369+
// When codeView is a diff, add BASE too
370+
...(baseContent && info.baseRepoPath && info.baseCommitID && info.baseFilePath
371+
? [
372+
{
373+
uri: toURIWithPath({
374+
repoPath: info.baseRepoPath,
375+
commitID: info.baseCommitID,
376+
filePath: info.baseFilePath,
377+
}),
378+
languageId: getModeFromPath(info.filePath) || 'could not determine mode',
379+
text: baseContent,
380+
},
381+
]
382+
: []),
383+
]
384+
385+
if (extensionsController && !info.baseCommitID) {
386+
let oldDecorations: Disposable[] = []
387+
388+
extensionsController.registries.textDocumentDecoration
389+
.getDecorations(toTextDocumentIdentifier(info))
390+
.subscribe(decorations => {
391+
for (const old of oldDecorations) {
392+
old.dispose()
393+
}
394+
oldDecorations = []
395+
for (const decoration of decorations || []) {
396+
try {
397+
oldDecorations.push(
398+
applyDecoration(dom, {
399+
codeView,
400+
decoration,
401+
})
402+
)
403+
} catch (e) {
404+
console.warn(e)
405+
}
406+
}
407+
})
408+
}
311409

312-
if (!getToolbarMount) {
313-
return
314-
}
410+
documentsSubject.next(documents)
411+
}
315412

316-
const mount = getToolbarMount(codeView)
413+
const resolveContext: ContextResolver = ({ part }) => ({
414+
repoPath: part === 'base' ? info.baseRepoPath || info.repoPath : info.repoPath,
415+
commitID: part === 'base' ? info.baseCommitID! : info.commitID,
416+
filePath: part === 'base' ? info.baseFilePath || info.filePath : info.filePath,
417+
rev: part === 'base' ? info.baseRev || info.baseCommitID! : info.rev || info.commitID,
418+
})
317419

318-
render(
319-
<CodeViewToolbar
320-
{...info}
321-
buttonProps={
322-
toolbarButtonProps || {
323-
className: '',
324-
style: {},
420+
subscriptions.add(
421+
hoverifier.hoverify({
422+
dom,
423+
positionEvents: of(codeView).pipe(findPositionsFromEvents(dom)),
424+
resolveContext,
425+
adjustPosition,
426+
})
427+
)
428+
429+
codeView.classList.add('sg-mounted')
430+
431+
if (!getToolbarMount) {
432+
return
433+
}
434+
435+
const mount = getToolbarMount(codeView)
436+
437+
render(
438+
<CodeViewToolbar
439+
{...info}
440+
extensions={extensionsContextController}
441+
extensionsController={extensionsController}
442+
buttonProps={
443+
toolbarButtonProps || {
444+
className: '',
445+
style: {},
446+
}
325447
}
326-
}
327-
simpleProviderFns={lspViaAPIXlang}
328-
/>,
329-
mount
330-
)
331-
})
448+
simpleProviderFns={
449+
extensionsController ? createLSPFromExtensions(extensionsController) : lspViaAPIXlang
450+
}
451+
/>,
452+
mount
453+
)
454+
}
455+
)
332456
)
333457

334458
return subscriptions

src/libs/code_intelligence/code_views.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { last, range } from 'lodash'
12
import { from, merge, Observable, of, Subject } from 'rxjs'
23
import { filter, map, mergeMap } from 'rxjs/operators'
34

4-
import { CodeHost, ResolvedCodeView } from './code_intelligence'
5+
import { DiffPart } from '@sourcegraph/codeintellify'
6+
import { CodeHost, CodeView, ResolvedCodeView } from './code_intelligence'
57

68
/**
79
* Emits a ResolvedCodeView when it's DOM element is on or about to be on the page.
@@ -121,3 +123,41 @@ export const findCodeViews = (codeHost: CodeHost, watchChildrenModifications = t
121123
filter(({ codeView }) => !codeView.classList.contains('sg-mounted'))
122124
)
123125
}
126+
127+
export interface CodeViewContent {
128+
content: string
129+
baseContent?: string
130+
}
131+
132+
export const getContentOfCodeView = (
133+
codeView: HTMLElement,
134+
info: Pick<CodeView, 'dom' | 'isDiff' | 'getLineRanges'>
135+
): CodeViewContent => {
136+
const getContent = (part?: DiffPart): string => {
137+
const lines = new Map<number, string>()
138+
let min = 1
139+
let max = 1
140+
141+
for (const { start, end } of info.getLineRanges(codeView, part)) {
142+
for (const line of range(start, end + 1)) {
143+
min = Math.min(min, line)
144+
max = Math.max(max, line)
145+
146+
const codeElement = info.dom.getCodeElementFromLineNumber(codeView, line, part)
147+
if (codeElement) {
148+
lines.set(line, codeElement.textContent || '')
149+
}
150+
}
151+
}
152+
153+
return range(min, max + 1)
154+
.map(line => lines.get(line) || '\n')
155+
.map(content => (last(content) === '\n' ? content : `${content}\n`))
156+
.join('')
157+
}
158+
159+
return {
160+
content: getContent(info.isDiff ? 'head' : undefined),
161+
baseContent: info.isDiff ? getContent('base') : undefined,
162+
}
163+
}

0 commit comments

Comments
 (0)