mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
add app.activeWindowChanged, Window.activeEditorChanged, codeEditor.selectionChanged (#2071)
* add `app.activeWindowChanged`, `Window.activeEditorChanged`, `codeEditor.selectionChanged` * Make Subscribable in sourcegraph.d.ts compatible with rxjs^6.4.0 * use isDefined() * changed -> changes
This commit is contained in:
parent
9f2a81e10a
commit
74208f2ada
@ -18,7 +18,7 @@ export function provideMigrations(area: chrome.storage.StorageArea): MigratableS
|
||||
const migrated = migrations.pipe(
|
||||
switchMap(
|
||||
migrate =>
|
||||
new Observable(observer => {
|
||||
new Observable<void>(observer => {
|
||||
area.get(items => {
|
||||
const { newItems, keysToRemove } = migrate(items as StorageItems)
|
||||
area.remove(keysToRemove || [], () => {
|
||||
|
||||
@ -623,7 +623,7 @@ interface ResolvedDiff {
|
||||
|
||||
export function resolveDiffRev(props: ResolveDiffOpt): Observable<ResolvedDiff> {
|
||||
// TODO: Do a proper refactor and convert all of this function call and it's deps from Promises to Observables.
|
||||
return from<ResolvedDiff>(
|
||||
return from(
|
||||
new Promise<ResolvedDiff>((resolve, reject) => {
|
||||
getPropsWithInfo(props)
|
||||
.then(propsWithInfo => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { combineLatest, merge, ReplaySubject } from 'rxjs'
|
||||
import { combineLatest, merge, Observable, ReplaySubject } from 'rxjs'
|
||||
import { map, mergeMap, publishReplay, refCount, switchMap, take } from 'rxjs/operators'
|
||||
import { GraphQLResult } from '../../../../shared/src/graphql/graphql'
|
||||
import * as GQL from '../../../../shared/src/graphql/schema'
|
||||
import { PlatformContext } from '../../../../shared/src/platform/context'
|
||||
import { mutateSettings, updateSettings } from '../../../../shared/src/settings/edit'
|
||||
@ -92,14 +93,15 @@ export function createPlatformContext({ urlToFile }: Pick<CodeHost, 'urlToFile'>
|
||||
|
||||
return storage.observeSync('sourcegraphURL').pipe(
|
||||
take(1),
|
||||
mergeMap(url =>
|
||||
requestGraphQL({
|
||||
ctx: getContext({ repoKey: '', isRepoSpecific: false }),
|
||||
request,
|
||||
variables,
|
||||
url,
|
||||
requestMightContainPrivateInfo,
|
||||
})
|
||||
mergeMap(
|
||||
(url: string): Observable<GraphQLResult<any>> =>
|
||||
requestGraphQL({
|
||||
ctx: getContext({ repoKey: '', isRepoSpecific: false }),
|
||||
request,
|
||||
variables,
|
||||
url,
|
||||
requestMightContainPrivateInfo,
|
||||
})
|
||||
)
|
||||
)
|
||||
},
|
||||
|
||||
@ -204,7 +204,7 @@
|
||||
"react-stripe-elements": "^2.0.1",
|
||||
"react-visibility-sensor": "^5.0.2",
|
||||
"reactstrap": "https://registry.npmjs.org/@sqs/reactstrap/-/reactstrap-6.5.0-tmp1.tgz",
|
||||
"rxjs": "^6.3.3",
|
||||
"rxjs": "^6.4.0",
|
||||
"sanitize-html": "^1.20.0",
|
||||
"sourcegraph": "link:packages/sourcegraph-extension-api",
|
||||
"string-score": "^1.0.1",
|
||||
|
||||
@ -430,6 +430,11 @@ declare module 'sourcegraph' {
|
||||
*/
|
||||
activeViewComponent: ViewComponent | undefined
|
||||
|
||||
/**
|
||||
* An event that is fired when the active view component changes.
|
||||
*/
|
||||
activeViewComponentChanges: Subscribable<ViewComponent | undefined>
|
||||
|
||||
/**
|
||||
* Show a notification message to the user that does not require interaction or steal focus.
|
||||
*
|
||||
@ -600,6 +605,13 @@ declare module 'sourcegraph' {
|
||||
*/
|
||||
readonly selections: Selection[]
|
||||
|
||||
/**
|
||||
* An event that is fired when the selections in this text editor change.
|
||||
* The primary selection ({@link CodeEditor#selection}), if any selections exist,
|
||||
* is always at index 0 of the emitted array.
|
||||
*/
|
||||
readonly selectionsChanges: Subscribable<Selection[]>
|
||||
|
||||
/**
|
||||
* Add a set of decorations to this editor. If a set of decorations already exists with the given
|
||||
* {@link TextDocumentDecorationType}, they will be replaced.
|
||||
@ -650,6 +662,11 @@ declare module 'sourcegraph' {
|
||||
*/
|
||||
export const activeWindow: Window | undefined
|
||||
|
||||
/**
|
||||
* An event that is fired when the currently active window changes.
|
||||
*/
|
||||
export const activeWindowChanges: Subscribable<Window | undefined>
|
||||
|
||||
/**
|
||||
* All application windows that are accessible by the extension.
|
||||
*
|
||||
@ -1207,6 +1224,12 @@ declare module 'sourcegraph' {
|
||||
* the subscription to stop reacting to the stream.
|
||||
*/
|
||||
subscribe(observer?: PartialObserver<T>): Unsubscribable
|
||||
/** @deprecated Use an observer instead of a complete callback */
|
||||
subscribe(next: null | undefined, error: null | undefined, complete: () => void): Unsubscribable
|
||||
/** @deprecated Use an observer instead of an error callback */
|
||||
subscribe(next: null | undefined, error: (error: any) => void, complete?: () => void): Unsubscribable
|
||||
/** @deprecated Use an observer instead of a complete callback */
|
||||
subscribe(next: (value: T) => void, error: null | undefined, complete: () => void): Unsubscribable
|
||||
subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Unsubscribable
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import * as clientType from '@sourcegraph/extension-api-types'
|
||||
import { of } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { ClientCodeEditorAPI } from '../../client/api/codeEditor'
|
||||
import { Range } from '../types/range'
|
||||
@ -18,6 +19,8 @@ export class ExtCodeEditor implements sourcegraph.CodeEditor {
|
||||
private documents: ExtDocuments
|
||||
) {}
|
||||
|
||||
public readonly selectionsChanges = of(this.selections)
|
||||
|
||||
public readonly type = 'CodeEditor'
|
||||
|
||||
public get document(): sourcegraph.TextDocument {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Observer } from 'rxjs'
|
||||
import { BehaviorSubject, Observer, of } from 'rxjs'
|
||||
import * as sourcegraph from 'sourcegraph'
|
||||
import { asError } from '../../../util/errors'
|
||||
import { ClientCodeEditorAPI } from '../../client/api/codeEditor'
|
||||
@ -19,6 +19,8 @@ export interface WindowData {
|
||||
class ExtWindow implements sourcegraph.Window {
|
||||
constructor(private windowsProxy: ClientWindowsAPI, private readonly textEditors: ExtCodeEditor[]) {}
|
||||
|
||||
public readonly activeViewComponentChanges = of(this.activeViewComponent)
|
||||
|
||||
public get visibleViewComponents(): sourcegraph.ViewComponent[] {
|
||||
return this.textEditors
|
||||
}
|
||||
@ -95,6 +97,8 @@ export class ExtWindows implements ExtWindowsAPI {
|
||||
private documents: ExtDocuments
|
||||
) {}
|
||||
|
||||
public readonly activeWindowChanged = new BehaviorSubject<sourcegraph.Window | undefined>(this.getActive())
|
||||
|
||||
/** @internal */
|
||||
public getActive(): sourcegraph.Window | undefined {
|
||||
return this.getAll()[0]
|
||||
@ -127,5 +131,6 @@ export class ExtWindows implements ExtWindowsAPI {
|
||||
/** @internal */
|
||||
public $acceptWindowData(allWindows: WindowData[]): void {
|
||||
this.data = allWindows
|
||||
this.activeWindowChanged.next(this.getActive())
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,6 +172,7 @@ function createExtensionAPI(
|
||||
},
|
||||
|
||||
app: {
|
||||
activeWindowChanges: windows.activeWindowChanged,
|
||||
get activeWindow(): sourcegraph.Window | undefined {
|
||||
return windows.getActive()
|
||||
},
|
||||
|
||||
65
shared/src/api/integration-test/selections.test.ts
Normal file
65
shared/src/api/integration-test/selections.test.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { from } from 'rxjs'
|
||||
import { filter, switchMap } from 'rxjs/operators'
|
||||
import { isDefined } from '../../util/types'
|
||||
import { ViewComponentData } from '../client/model'
|
||||
import { assertToJSON } from '../extension/types/testHelpers'
|
||||
import { collectSubscribableValues, integrationTestContext } from './testHelpers'
|
||||
|
||||
const withSelections = (...selections: { start: number; end: number }[]): ViewComponentData => ({
|
||||
type: 'textEditor',
|
||||
item: { uri: 'foo', languageId: 'l1', text: 't1' },
|
||||
selections: selections.map(({ start, end }) => ({
|
||||
start: {
|
||||
line: start,
|
||||
character: 0,
|
||||
},
|
||||
end: {
|
||||
line: end,
|
||||
character: 0,
|
||||
},
|
||||
anchor: {
|
||||
line: start,
|
||||
character: 0,
|
||||
},
|
||||
active: {
|
||||
line: end,
|
||||
character: 0,
|
||||
},
|
||||
isReversed: false,
|
||||
})),
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
describe('Selections (integration)', () => {
|
||||
describe('editor.selectionsChanged', () => {
|
||||
test('reflects changes to the current selections', async () => {
|
||||
const { model, extensionHost } = await integrationTestContext(undefined, {
|
||||
roots: [],
|
||||
visibleViewComponents: [],
|
||||
})
|
||||
const selectionChanges = from(extensionHost.app.activeWindowChanges).pipe(
|
||||
filter(isDefined),
|
||||
switchMap(window => window.activeViewComponentChanges),
|
||||
filter(isDefined),
|
||||
switchMap(editor => editor.selectionsChanges)
|
||||
)
|
||||
const selectionValues = collectSubscribableValues(selectionChanges)
|
||||
const testValues = [
|
||||
[{ start: 3, end: 5 }],
|
||||
[{ start: 1, end: 10 }, { start: 25, end: 40 }, { start: 56, end: 57 }],
|
||||
[],
|
||||
]
|
||||
for (const selections of testValues) {
|
||||
model.next({
|
||||
...model.value,
|
||||
visibleViewComponents: [withSelections(...selections)],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
}
|
||||
assertToJSON(
|
||||
selectionValues.map(selections => selections.map(s => ({ start: s.start.line, end: s.end.line }))),
|
||||
testValues
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -56,7 +56,8 @@ const NOOP_MOCKS: Mocks = {
|
||||
* @internal
|
||||
*/
|
||||
export async function integrationTestContext(
|
||||
partialMocks: Partial<Mocks> = NOOP_MOCKS
|
||||
partialMocks: Partial<Mocks> = NOOP_MOCKS,
|
||||
initModel: Model = FIXTURE_MODEL
|
||||
): Promise<
|
||||
TestContext & {
|
||||
model: Subscribable<Model> & { value: Model } & NextObserver<Model>
|
||||
@ -87,7 +88,7 @@ export async function integrationTestContext(
|
||||
)
|
||||
)
|
||||
|
||||
services.model.model.next(FIXTURE_MODEL)
|
||||
services.model.model.next(initModel)
|
||||
|
||||
await (await extensionHost.__testAPI).internal.sync()
|
||||
return {
|
||||
|
||||
@ -19,6 +19,51 @@ describe('Windows (integration)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('app.activeWindowChanged', () => {
|
||||
test('reflects changes to the active window', async () => {
|
||||
const { extensionHost, model } = await integrationTestContext(undefined, {
|
||||
roots: [],
|
||||
visibleViewComponents: [],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
const values = collectSubscribableValues(extensionHost.app.activeWindowChanges)
|
||||
model.next({
|
||||
...model.value,
|
||||
visibleViewComponents: [
|
||||
{
|
||||
type: 'textEditor',
|
||||
item: { uri: 'foo', languageId: 'l1', text: 't1' },
|
||||
selections: [],
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
model.next({
|
||||
...model.value,
|
||||
visibleViewComponents: [],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
model.next({
|
||||
...model.value,
|
||||
visibleViewComponents: [
|
||||
{
|
||||
type: 'textEditor',
|
||||
item: { uri: 'bar', languageId: 'l2', text: 't2' },
|
||||
selections: [],
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
await extensionHost.internal.sync()
|
||||
assertToJSON(values.map(w => w && w.activeViewComponent && w.activeViewComponent.document.uri), [
|
||||
null,
|
||||
'foo',
|
||||
null,
|
||||
'bar',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('app.windows', () => {
|
||||
test('lists windows', async () => {
|
||||
const { extensionHost } = await integrationTestContext()
|
||||
|
||||
@ -92,7 +92,7 @@ export class HierarchicalLocationsView extends React.PureComponent<Props, State>
|
||||
locationsOrError.length > 0,
|
||||
})
|
||||
}),
|
||||
endWith({ locationsComplete: true })
|
||||
endWith({ ...this.state, locationsComplete: true })
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { parseBrowserRepoURL, toTreeURL } from './url'
|
||||
import { lprToSelectionsZeroIndexed, parseBrowserRepoURL, toTreeURL } from './url'
|
||||
|
||||
/**
|
||||
* Asserts deep object equality using node's assert.deepEqual, except it (1) ignores differences in the
|
||||
@ -170,3 +170,126 @@ describe('parseBrowserRepoURL', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('lprToSelectionsZeroIndexed', () => {
|
||||
test('converts an LPR with only a start line', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 5,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
end: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
anchor: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
active: {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('converts an LPR with a line and a character', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 5,
|
||||
character: 45,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
end: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
anchor: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
active: {
|
||||
line: 4,
|
||||
character: 44,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('converts an LPR with a start and end line', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 12,
|
||||
endLine: 15,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 11,
|
||||
character: 0,
|
||||
},
|
||||
end: {
|
||||
line: 14,
|
||||
character: 0,
|
||||
},
|
||||
anchor: {
|
||||
line: 11,
|
||||
character: 0,
|
||||
},
|
||||
active: {
|
||||
line: 14,
|
||||
character: 0,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('converts an LPR with a start and end line and characters', () => {
|
||||
assertDeepStrictEqual(
|
||||
lprToSelectionsZeroIndexed({
|
||||
line: 12,
|
||||
character: 30,
|
||||
endLine: 15,
|
||||
endCharacter: 60,
|
||||
}),
|
||||
[
|
||||
{
|
||||
start: {
|
||||
line: 11,
|
||||
character: 29,
|
||||
},
|
||||
end: {
|
||||
line: 14,
|
||||
character: 59,
|
||||
},
|
||||
anchor: {
|
||||
line: 11,
|
||||
character: 29,
|
||||
},
|
||||
active: {
|
||||
line: 14,
|
||||
character: 59,
|
||||
},
|
||||
isReversed: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -157,8 +157,10 @@ export function lprToSelectionsZeroIndexed(lpr: LineOrPositionOrRange): Selectio
|
||||
if (range === undefined) {
|
||||
return []
|
||||
}
|
||||
const start: Position = { line: range.start.line - 1, character: range.start.character - 1 }
|
||||
const end: Position = { line: range.end.line - 1, character: range.end.character - 1 }
|
||||
// `lprToRange` sets character to 0 if it's undefined. Only - 1 the character if it's not 0.
|
||||
const characterZeroIndexed = (character: number) => (character === 0 ? character : character - 1)
|
||||
const start: Position = { line: range.start.line - 1, character: characterZeroIndexed(range.start.character) }
|
||||
const end: Position = { line: range.end.line - 1, character: characterZeroIndexed(range.end.character) }
|
||||
return [
|
||||
{
|
||||
start,
|
||||
|
||||
@ -13092,10 +13092,10 @@ rxjs-tslint-rules@^4.12.0:
|
||||
tslib "^1.8.0"
|
||||
tsutils "^3.0.0"
|
||||
|
||||
rxjs@^6.0.0, rxjs@^6.1.0, rxjs@^6.3.2, rxjs@^6.3.3:
|
||||
version "6.3.3"
|
||||
resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55"
|
||||
integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==
|
||||
rxjs@^6.0.0, rxjs@^6.1.0, rxjs@^6.3.2, rxjs@^6.3.3, rxjs@^6.4.0:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504"
|
||||
integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user