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

Commit becfba3

Browse files
authored
feat: GitHub uses the new inject method (#186)
* feat: define GitHub code_intelligence functions and objects * feat: use generic inject code intelligence function * feat: annotate lazily loaded code views * refactor: findCodeElements is an operator * refactor: wip * feat: listens for new code views * feat: only on mutation/intersection observer * refactor: renames * feat: requestAnimationFrame * feat: requestAnimationFrame * feat: clean up subscriptions * refactor: use rx animation from scheduler * refactor: Promise.resolve(maybePromise) * refactor: inline await * refactor: address review comments * feat: single code view * feat: search and comment code snippets * feat: search and comment code snippets * feat: put behind feature flag * chore: tslint
1 parent ff75251 commit becfba3

File tree

16 files changed

+550
-436
lines changed

16 files changed

+550
-436
lines changed

src/browser/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ export interface PhabricatorMapping {
1212
*/
1313
export interface FeatureFlags {
1414
newTooltips: boolean
15+
newInject: boolean
1516
}
1617

1718
export const featureFlagDefaults: FeatureFlags = {
1819
newTooltips: true,
20+
newInject: false,
1921
}
2022

2123
// TODO(chris) Switch to Partial<StorageItems> to eliminate bugs caused by

src/extension/scripts/inject.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { featureFlags } from '../../shared/util/featureFlags'
1818

1919
import { injectBitbucketServer } from '../../libs/bitbucket/inject'
20+
import { injectCodeIntelligence } from '../../libs/code_intelligence'
2021
import { injectGitHubApplication } from '../../libs/github/inject'
2122
import { injectPhabricatorApplication } from '../../libs/phabricator/app'
2223
import { injectSourcegraphApp } from '../../libs/sourcegraph/inject'
@@ -37,7 +38,7 @@ function injectApplication(): void {
3738

3839
const href = window.location.href
3940

40-
const handleGetStorage = (items: StorageItems) => {
41+
const handleGetStorage = async (items: StorageItems) => {
4142
if (items.disableExtension) {
4243
return
4344
}
@@ -99,6 +100,14 @@ function injectApplication(): void {
99100
setSourcegraphUrl(sourcegraphServerUrl)
100101
injectBitbucketServer()
101102
}
103+
104+
if (isGitHub || isPhabricator) {
105+
if (await featureFlags.isEnabled('newInject')) {
106+
const subscriptions = await injectCodeIntelligence()
107+
window.addEventListener('unload', () => subscriptions.unsubscribe())
108+
}
109+
}
110+
102111
setUseExtensions(items.useExtensions === undefined ? false : items.useExtensions)
103112
}
104113

src/libs/code_intelligence/inject.tsx renamed to src/libs/code_intelligence/code_intelligence.tsx

Lines changed: 109 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,78 @@ 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 { Observable, of, Subject, Subscription } from 'rxjs'
19-
import { filter, map, withLatestFrom } from 'rxjs/operators'
18+
import { animationFrameScheduler, Observable, of, Subject, Subscription } from 'rxjs'
19+
import { filter, map, mergeMap, observeOn, withLatestFrom } from 'rxjs/operators'
2020

2121
import { createJumpURLFetcher } from '../../shared/backend/lsp'
2222
import { lspViaAPIXlang } from '../../shared/backend/lsp'
2323
import { ButtonProps, CodeViewToolbar } from '../../shared/components/CodeViewToolbar'
2424
import { eventLogger, sourcegraphUrl } from '../../shared/util/context'
25+
import { githubCodeHost } from '../github/code_intelligence'
26+
import { phabricatorCodeHost } from '../phabricator/code_intelligence'
27+
import { findCodeViews } from './code_views'
28+
29+
/**
30+
* Defines a type of code view a given code host can have. It tells us how to
31+
* look for the code view and how to do certain things when we find it.
32+
*/
33+
export interface CodeView {
34+
/** A selector used by `document.querySelectorAll` to find the code view. */
35+
selector: string
36+
/** The DOMFunctions for the code view. */
37+
dom: DOMFunctions
38+
/**
39+
* Finds or creates a DOM element where we should inject the
40+
* `CodeViewToolbar`. This function is responsible for ensuring duplicate
41+
* mounts aren't created.
42+
*/
43+
getToolbarMount?: (codeView: HTMLElement, part?: DiffPart) => HTMLElement
44+
/**
45+
* Resolves the file info for a given code view. It returns an observable
46+
* because some code hosts need to resolve this asynchronously. The
47+
* observable should only emit once.
48+
*/
49+
resolveFileInfo: (codeView: HTMLElement) => Observable<FileInfo>
50+
/**
51+
* In some situations, we need to be able to adjust the position going into
52+
* and coming out of codeintellify. For example, Phabricator converts tabs
53+
* to spaces in it's DOM.
54+
*/
55+
adjustPosition?: PositionAdjuster
56+
/** Props for styling the buttons in the `CodeViewToolbar`. */
57+
toolbarButtonProps?: ButtonProps
58+
}
59+
60+
export type CodeViewWithOutSelector = Pick<CodeView, Exclude<keyof CodeView, 'selector'>>
61+
62+
export interface CodeViewResolver {
63+
selector: string
64+
resolveCodeView: (elem: HTMLElement) => CodeViewWithOutSelector
65+
}
2566

2667
/** Information for adding code intelligence to code views on arbitrary code hosts. */
2768
export interface CodeHost {
2869
/**
2970
* The name of the code host. This will be added as a className to the overlay mount.
3071
*/
3172
name: string
73+
3274
/**
3375
* The list of types of code views to try to annotate.
3476
*/
35-
codeViews: CodeView[]
77+
codeViews?: CodeView[]
78+
79+
/**
80+
* Resolve `CodeView`s from the DOM. This is useful when each code view type
81+
* doesn't have a distinct selector for
82+
*/
83+
codeViewResolver?: CodeViewResolver
84+
85+
/**
86+
* Checks to see if the current context the code is running in is within
87+
* the given code host.
88+
*/
89+
check: () => Promise<boolean> | boolean
3690
}
3791

3892
export interface FileInfo {
@@ -55,7 +109,6 @@ export interface FileInfo {
55109
* The revision the code view is at. If a `baseRev` is provided, this value is treated as the head rev.
56110
*/
57111
rev?: string
58-
59112
/**
60113
* The repo bath for the BASE side of a diff. This is useful for Phabricator
61114
* staging areas since they are separate repos.
@@ -78,31 +131,6 @@ export interface FileInfo {
78131
baseHasFileContents?: boolean
79132
}
80133

81-
/**
82-
* Defines a type of code view a given code host can have. It tells us how to
83-
* look for the code view and how to do certain things when we find it.
84-
*/
85-
export interface CodeView {
86-
/** A selector used by `document.querySelectorAll` to find the code view. */
87-
selector: string
88-
/** The DOMFunctions for the code view. */
89-
dom: DOMFunctions
90-
/** Finds or creates a DOM element where we should inject the `CodeViewToolbar`. */
91-
getToolbarMount?: (codeView: HTMLElement, part?: DiffPart) => HTMLElement
92-
/** Resolves the file info for a given code view. It returns an observable
93-
* because some code hosts need to resolve this asynchronously. The
94-
* observable should only emit once.
95-
*/
96-
resolveFileInfo: (codeView: HTMLElement) => Observable<FileInfo>
97-
/** In some situations, we need to be able to adjust the position going into
98-
* and coming out of codeintellify. For example, Phabricator converts tabs
99-
* to spaces in it's DOM.
100-
*/
101-
adjustPosition?: PositionAdjuster
102-
/** Props for styling the buttons in the `CodeViewToolbar`. */
103-
toolbarButtonProps?: ButtonProps
104-
}
105-
106134
/**
107135
* Prepares the page for code intelligence. It creates the hoverifier, injects
108136
* and mounts the hover overlay and then returns the hoverifier.
@@ -196,41 +224,43 @@ function initCodeIntelligence(codeHost: CodeHost): { hoverifier: Hoverifier } {
196224
* ResolvedCodeView attaches an actual code view DOM element that was found on
197225
* the page to the CodeView type being passed around by this file.
198226
*/
199-
export interface ResolvedCodeView extends CodeView {
227+
export interface ResolvedCodeView extends CodeViewWithOutSelector {
200228
/** The code view DOM element. */
201229
codeView: HTMLElement
202230
}
203231

204-
function findCodeViews(codeViewInfos: CodeView[]): Observable<ResolvedCodeView> {
205-
return new Observable<ResolvedCodeView>(observer => {
206-
for (const info of codeViewInfos) {
207-
const elements = document.querySelectorAll<HTMLElement>(info.selector)
208-
for (const codeView of elements) {
209-
observer.next({ ...info, codeView })
210-
}
211-
}
212-
})
213-
}
232+
function handleCodeHost(codeHost: CodeHost): Subscription {
233+
const { hoverifier } = initCodeIntelligence(codeHost)
214234

215-
export function injectCodeIntelligence(codeHostInfo: CodeHost): Subscription {
216-
const { hoverifier } = initCodeIntelligence(codeHostInfo)
235+
const subscriptions = new Subscription()
217236

218-
return findCodeViews(codeHostInfo.codeViews).subscribe(
219-
({ codeView, dom, resolveFileInfo, adjustPosition, getToolbarMount, toolbarButtonProps }) =>
220-
resolveFileInfo(codeView).subscribe(info => {
237+
subscriptions.add(
238+
of(document.body)
239+
.pipe(
240+
findCodeViews(codeHost),
241+
mergeMap(({ codeView, resolveFileInfo, ...rest }) =>
242+
resolveFileInfo(codeView).pipe(map(info => ({ info, codeView, ...rest })))
243+
),
244+
observeOn(animationFrameScheduler)
245+
)
246+
.subscribe(({ codeView, info, dom, adjustPosition, getToolbarMount, toolbarButtonProps }) => {
221247
const resolveContext: ContextResolver = ({ part }) => ({
222248
repoPath: part === 'base' ? info.baseRepoPath || info.repoPath : info.repoPath,
223249
commitID: part === 'base' ? info.baseCommitID! : info.commitID,
224250
filePath: part === 'base' ? info.baseFilePath! : info.filePath,
225251
rev: part === 'base' ? info.baseRev || info.baseCommitID! : info.rev || info.commitID,
226252
})
227253

228-
hoverifier.hoverify({
229-
dom,
230-
positionEvents: of(codeView).pipe(findPositionsFromEvents(dom)),
231-
resolveContext,
232-
adjustPosition,
233-
})
254+
subscriptions.add(
255+
hoverifier.hoverify({
256+
dom,
257+
positionEvents: of(codeView).pipe(findPositionsFromEvents(dom)),
258+
resolveContext,
259+
adjustPosition,
260+
})
261+
)
262+
263+
codeView.classList.add('sg-mounted')
234264

235265
if (!getToolbarMount) {
236266
return
@@ -253,4 +283,32 @@ export function injectCodeIntelligence(codeHostInfo: CodeHost): Subscription {
253283
)
254284
})
255285
)
286+
287+
return subscriptions
288+
}
289+
290+
async function injectCodeIntelligenceToCodeHosts(codeHosts: CodeHost[]): Promise<Subscription> {
291+
const subscriptions = new Subscription()
292+
293+
for (const codeHost of codeHosts) {
294+
const isCodeHost = await Promise.resolve(codeHost.check())
295+
if (isCodeHost) {
296+
subscriptions.add(handleCodeHost(codeHost))
297+
}
298+
}
299+
300+
return subscriptions
301+
}
302+
303+
/**
304+
* Injects all code hosts into the page.
305+
*
306+
* @returns A promise with a subscription containing all subscriptions for code
307+
* intelligence. Unsubscribing will clean up subscriptions for hoverify and any
308+
* incomplete setup requests.
309+
*/
310+
export async function injectCodeIntelligence(): Promise<Subscription> {
311+
const codeHosts: CodeHost[] = [githubCodeHost, phabricatorCodeHost]
312+
313+
return await injectCodeIntelligenceToCodeHosts(codeHosts)
256314
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { from, merge, Observable, of, Subject } from 'rxjs'
2+
import { filter, map, mergeMap } from 'rxjs/operators'
3+
4+
import { CodeHost, ResolvedCodeView } from './code_intelligence'
5+
6+
/**
7+
* Emits a ResolvedCodeView when it's DOM element is on or about to be on the page.
8+
*/
9+
const emitWhenIntersecting = (margin: number) => {
10+
const codeViewStash = new Map<HTMLElement, ResolvedCodeView>()
11+
12+
const intersectingElements = new Subject<HTMLElement>()
13+
14+
const intersectionObserver = new IntersectionObserver(
15+
entries => {
16+
for (const entry of entries) {
17+
// `entry` is an `IntersectionObserverEntry`,
18+
// which has
19+
// [isIntersecting](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry/isIntersecting#Browser_compatibility)
20+
// as a prop, but TS complains that it does not
21+
// exist.
22+
if ((entry as any).isIntersecting) {
23+
intersectingElements.next(entry.target as HTMLElement)
24+
}
25+
}
26+
},
27+
{
28+
rootMargin: `${margin}px`,
29+
threshold: 0,
30+
}
31+
)
32+
33+
return (codeViews: Observable<ResolvedCodeView>) =>
34+
new Observable<ResolvedCodeView>(observer => {
35+
codeViews.subscribe(({ codeView, ...rest }) => {
36+
intersectionObserver.observe(codeView)
37+
codeViewStash.set(codeView, { codeView, ...rest })
38+
})
39+
40+
intersectingElements
41+
.pipe(
42+
map(element => codeViewStash.get(element)),
43+
filter(codeView => !!codeView)
44+
)
45+
.subscribe(observer)
46+
})
47+
}
48+
49+
/**
50+
* findCodeViews finds all the code views on a page given a CodeHost. It emits code views
51+
* that are lazily loaded as well.
52+
*/
53+
export const findCodeViews = (codeHost: CodeHost, watchChildrenModifications = true) => (
54+
containers: Observable<Element>
55+
) => {
56+
const codeViewsFromList: Observable<ResolvedCodeView> = containers.pipe(
57+
filter(() => !!codeHost.codeViews),
58+
mergeMap(container =>
59+
from(codeHost.codeViews!).pipe(
60+
map(({ selector, ...info }) => ({
61+
info,
62+
matches: container.querySelectorAll<HTMLElement>(selector),
63+
}))
64+
)
65+
),
66+
mergeMap(({ info, matches }) =>
67+
of(...matches).pipe(
68+
map(codeView => ({
69+
...info,
70+
codeView,
71+
}))
72+
)
73+
)
74+
)
75+
76+
const codeViewsFromResolver: Observable<ResolvedCodeView> = containers.pipe(
77+
filter(() => !!codeHost.codeViewResolver),
78+
map(container => ({
79+
resolveCodeView: codeHost.codeViewResolver!.resolveCodeView,
80+
matches: container.querySelectorAll<HTMLElement>(codeHost.codeViewResolver!.selector),
81+
})),
82+
mergeMap(({ resolveCodeView, matches }) =>
83+
of(...matches).pipe(
84+
map(codeView => ({
85+
...resolveCodeView(codeView),
86+
codeView,
87+
}))
88+
)
89+
)
90+
)
91+
92+
const obs = [codeViewsFromList, codeViewsFromResolver]
93+
94+
if (watchChildrenModifications) {
95+
const possibleLazilyLoadedContainers = new Subject<HTMLElement>()
96+
97+
const mutationObserver = new MutationObserver(mutations => {
98+
for (const mutation of mutations) {
99+
if (mutation.type === 'childList' && mutation.target instanceof HTMLElement) {
100+
const { target } = mutation
101+
102+
possibleLazilyLoadedContainers.next(target)
103+
}
104+
}
105+
})
106+
107+
containers.subscribe(container =>
108+
mutationObserver.observe(container, {
109+
childList: true,
110+
subtree: true,
111+
})
112+
)
113+
114+
const lazilyLoadedCodeViews = possibleLazilyLoadedContainers.pipe(findCodeViews(codeHost, false))
115+
116+
obs.push(lazilyLoadedCodeViews)
117+
}
118+
119+
return merge(...obs).pipe(
120+
emitWhenIntersecting(250),
121+
filter(({ codeView }) => !codeView.classList.contains('sg-mounted'))
122+
)
123+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './code_intelligence'

0 commit comments

Comments
 (0)