From 17349960b5277db2fe7d8411d6b02bf601bf78f0 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Thu, 3 Jan 2019 23:14:54 -0800 Subject: [PATCH 1/2] allow contributing actions to location results This lets extensions' location providers (definition/reference/etc. providers) associate context data with each location, and also contribute actions that are shown selectively in the file title for location matches shown in the panel. Together, these new features allow the basic-code-intel extension to add "Fuzzy" badges to defs/refs in the panel. This fixes https://github.com/sourcegraph/sourcegraph/issues/1174. It would also enable things like: - Showing a "source" badge on cross-repository reference results explaining how the reference was found (eg by looking up dependents on npm) - Showing the type of reference (eg denoting some references as "Assignments", some as "Calls", some as "References", etc.) - Adding an action to "hide" or "always ignore" the reference - Adding an action to add the match to a saved list - Adding an action for users to say "This was useful" or "This was not useful" (eg for a code examples extension) See https://github.com/sourcegraph/sourcegraph-basic-code-intel/pull/10 for an example of how extensions can use this new API. --- doc/extensions/authoring/contributions.md | 1 + .../extension-api-types/src/location.d.ts | 8 +++++ .../src/sourcegraph.d.ts | 13 ++++++- shared/src/api/client/context/context.test.ts | 21 +++++++++++ shared/src/api/client/context/context.ts | 24 +++++++++++-- shared/src/api/extension/api/types.test.ts | 16 +++++++++ shared/src/api/extension/api/types.ts | 1 + .../src/api/extension/types/location.test.ts | 7 ++++ shared/src/api/extension/types/location.ts | 10 +++--- shared/src/api/protocol/contribution.ts | 5 +++ shared/src/components/FileMatch.tsx | 22 ++++++++---- shared/src/panel/Panel.tsx | 1 + shared/src/panel/views/FileLocations.tsx | 36 +++++++++++++++++-- .../panel/views/HierarchicalLocationsView.tsx | 9 ++++- shared/src/panel/views/PanelView.tsx | 26 +++++++------- 15 files changed, 171 insertions(+), 29 deletions(-) create mode 100644 shared/src/api/extension/api/types.test.ts diff --git a/doc/extensions/authoring/contributions.md b/doc/extensions/authoring/contributions.md index 5295e812a53c..f514474cd8bc 100644 --- a/doc/extensions/authoring/contributions.md +++ b/doc/extensions/authoring/contributions.md @@ -23,6 +23,7 @@ A menu is an existing part of the user interface (of Sourcegraph or any other in * `directory/page`: A section on all pages showing a directory listing. Sometimes known as a "tree page" on code hosts. * `global/nav`: The global navigation bar, shown at the top of every page. * `panel/toolbar`: The toolbar on the panel, which is used to show references, definitions, commit history, and other information related to a file or a token/position in a file. +* `location/title`: The title bar of a location result (such as a reference result in the panel shown in response to a "Find references" action). * `help`: The help menu or page. The set of available menus is defined in the `menus` property in [`extension.schema.json`](https://sourcegraph.com/github.com/sourcegraph/sourcegraph/-/blob/shared/src/schema/extension.schema.json). diff --git a/packages/@sourcegraph/extension-api-types/src/location.d.ts b/packages/@sourcegraph/extension-api-types/src/location.d.ts index a516f730c1ba..f22cc473140c 100644 --- a/packages/@sourcegraph/extension-api-types/src/location.d.ts +++ b/packages/@sourcegraph/extension-api-types/src/location.d.ts @@ -63,4 +63,12 @@ export interface Location { /** An optional range within the document. */ readonly range?: Range + + /** + * Additional data associated with this location. The context data is available to actions + * contributed to the `location` menu. It allows extensions to contribute custom actions to + * locations shown in (e.g.) the references panel, such as an indicator that the location is + * an imprecise match or is on another branch. + */ + readonly context?: ContextValues } diff --git a/packages/sourcegraph-extension-api/src/sourcegraph.d.ts b/packages/sourcegraph-extension-api/src/sourcegraph.d.ts index fad74a6670d2..a946ff5117ed 100644 --- a/packages/sourcegraph-extension-api/src/sourcegraph.d.ts +++ b/packages/sourcegraph-extension-api/src/sourcegraph.d.ts @@ -311,13 +311,24 @@ declare module 'sourcegraph' { */ range?: Range + /** + * Additional data associated with this location. The context data is available to actions + * contributed to the `location` menu. It allows extensions to contribute custom actions to + * locations shown in (e.g.) the references panel, such as an indicator that the location is + * an imprecise match or is on another branch. + */ + readonly context?: Readonly + /** * Creates a new location object. * + * The argument values must not be mutated after the constructor is called. + * * @param uri The resource identifier. * @param rangeOrPosition The range or position. Positions will be converted to an empty range. + * @param context Optional context data associated with this location. */ - constructor(uri: URI, rangeOrPosition?: Range | Position) + constructor(uri: URI, rangeOrPosition?: Range | Position, context?: ContextValues) } /** diff --git a/shared/src/api/client/context/context.test.ts b/shared/src/api/client/context/context.test.ts index f48ab6e1d122..8ca41cd3c515 100644 --- a/shared/src/api/client/context/context.test.ts +++ b/shared/src/api/client/context/context.test.ts @@ -218,6 +218,27 @@ describe('getComputedContextProperty', () => { )) }) + describe('location', () => { + test('scoped context shadows outer context', () => + expect( + getComputedContextProperty(EMPTY_MODEL, EMPTY_SETTINGS_CASCADE, { a: 1 }, 'a', { + type: 'location', + location: { uri: 'x', context: { a: 2 } }, + }) + ).toBe(2)) + + test('provides location.uri', () => + expect( + getComputedContextProperty(EMPTY_MODEL, EMPTY_SETTINGS_CASCADE, {}, 'location.uri', { + type: 'location', + location: { uri: 'x' }, + }) + ).toBe('x')) + + test('returns null for location.uri when there is no location', () => + expect(getComputedContextProperty(EMPTY_MODEL, EMPTY_SETTINGS_CASCADE, {}, 'location.uri')).toBe(null)) + }) + test('falls back to the context entries', () => { expect(getComputedContextProperty(EMPTY_MODEL, EMPTY_SETTINGS_CASCADE, { x: 1 }, 'x')).toBe(1) expect(getComputedContextProperty(EMPTY_MODEL, EMPTY_SETTINGS_CASCADE, {}, 'y')).toBe(undefined) diff --git a/shared/src/api/client/context/context.ts b/shared/src/api/client/context/context.ts index 77ef77c3abe3..05e42103b3d0 100644 --- a/shared/src/api/client/context/context.ts +++ b/shared/src/api/client/context/context.ts @@ -1,3 +1,4 @@ +import { Location } from '@sourcegraph/extension-api-types' import { basename, dirname, extname } from 'path' import { isSettingsValid, SettingsCascadeOrError } from '../../../settings/settings' import { Model, ViewComponentData } from '../model' @@ -28,15 +29,16 @@ export function applyContextUpdate(base: Context, update: Context): Context { */ export interface Context extends Record< - string, - string | number | boolean | null | Context | T | (string | number | boolean | null | Context | T)[] - > {} + string, + string | number | boolean | null | Context | T | (string | number | boolean | null | Context | T)[] + > {} export type ContributionScope = | (Pick & { item: Pick }) | { type: 'panelView'; id: string } + | { type: 'location'; location: Location } /** * Looks up a key in the computed context, which consists of computed context properties (with higher precedence) @@ -124,6 +126,22 @@ export function getComputedContextProperty( return component.id } } + + // Location scope's context shadows outer context. + if (component && component.type === 'location' && component.location.context && key in component.location.context) { + return component.location.context[key] + } + if (key.startsWith('location.')) { + if (!component || component.type !== 'location') { + return null + } + const prop = key.slice('location.'.length) + switch (prop) { + case 'uri': + return component.location.uri + } + } + if (key === 'context') { return context } diff --git a/shared/src/api/extension/api/types.test.ts b/shared/src/api/extension/api/types.test.ts new file mode 100644 index 000000000000..412dd8300641 --- /dev/null +++ b/shared/src/api/extension/api/types.test.ts @@ -0,0 +1,16 @@ +import { Location } from '../types/location' +import { Position } from '../types/position' +import { Range } from '../types/range' +import { URI } from '../types/uri' +import { fromLocation } from './types' + +describe('fromLocation', () => { + test('converts to location', () => + expect( + fromLocation(new Location(URI.parse('x'), new Range(new Position(1, 2), new Position(3, 4)), { a: 1 })) + ).toEqual({ + uri: 'x', + range: { start: { line: 1, character: 2 }, end: { line: 3, character: 4 } }, + context: { a: 1 }, + })) +}) diff --git a/shared/src/api/extension/api/types.ts b/shared/src/api/extension/api/types.ts index 196bf6285523..87585d97e173 100644 --- a/shared/src/api/extension/api/types.ts +++ b/shared/src/api/extension/api/types.ts @@ -21,6 +21,7 @@ export function fromLocation(location: sourcegraph.Location): clientType.Locatio return { uri: location.uri.toString(), range: fromRange(location.range), + context: location.context, } } diff --git a/shared/src/api/extension/types/location.test.ts b/shared/src/api/extension/types/location.test.ts index 9b8a98b1afb6..1764433905b2 100644 --- a/shared/src/api/extension/types/location.test.ts +++ b/shared/src/api/extension/types/location.test.ts @@ -9,10 +9,17 @@ describe('Location', () => { assertToJSON(new Location(URI.file('u.ts'), new Position(3, 4)), { uri: URI.parse('file://u.ts').toJSON(), range: { start: { line: 3, character: 4 }, end: { line: 3, character: 4 } }, + context: undefined, }) assertToJSON(new Location(URI.file('u.ts'), new Range(1, 2, 3, 4)), { uri: URI.parse('file://u.ts').toJSON(), range: { start: { line: 1, character: 2 }, end: { line: 3, character: 4 } }, + context: undefined, + }) + assertToJSON(new Location(URI.file('u.ts'), new Range(1, 2, 3, 4), { a: 1 }), { + uri: URI.parse('file://u.ts').toJSON(), + range: { start: { line: 1, character: 2 }, end: { line: 3, character: 4 } }, + context: { a: 1 }, }) }) }) diff --git a/shared/src/api/extension/types/location.ts b/shared/src/api/extension/types/location.ts index 538a6bbfd6a2..b523a7d3c33d 100644 --- a/shared/src/api/extension/types/location.ts +++ b/shared/src/api/extension/types/location.ts @@ -14,12 +14,13 @@ export class Location implements sourcegraph.Location { return Range.isRange((thing as Location).range) && URI.isURI((thing as Location).uri) } - public uri: sourcegraph.URI public range?: sourcegraph.Range - constructor(uri: sourcegraph.URI, rangeOrPosition?: sourcegraph.Range | sourcegraph.Position) { - this.uri = uri - + constructor( + public readonly uri: sourcegraph.URI, + rangeOrPosition?: sourcegraph.Range | sourcegraph.Position, + public readonly context?: sourcegraph.ContextValues + ) { if (!rangeOrPosition) { // that's OK } else if (rangeOrPosition instanceof Range) { @@ -35,6 +36,7 @@ export class Location implements sourcegraph.Location { return { uri: this.uri, range: this.range, + context: this.context, } } } diff --git a/shared/src/api/protocol/contribution.ts b/shared/src/api/protocol/contribution.ts index a7ca97cfdc81..af865d6b234c 100644 --- a/shared/src/api/protocol/contribution.ts +++ b/shared/src/api/protocol/contribution.ts @@ -207,6 +207,11 @@ export enum ContributableMenu { /** The panel toolbar. */ PanelToolbar = 'panel/toolbar', + /** + * The title bar of a location result, such as a reference or definition location in the panel. + */ + LocationTitle = 'location/title', + /** The help menu in the application. */ Help = 'help', } diff --git a/shared/src/components/FileMatch.tsx b/shared/src/components/FileMatch.tsx index 38cf5dab19aa..92a7550c9387 100644 --- a/shared/src/components/FileMatch.tsx +++ b/shared/src/components/FileMatch.tsx @@ -57,6 +57,11 @@ interface Props { */ showAllMatches: boolean + /** + * An extra React fragment to render in the header. + */ + extraHeader?: React.ReactFragment + isLightTheme: boolean allExpanded?: boolean @@ -91,12 +96,17 @@ export class FileMatch extends React.PureComponent { })) const title = ( - +
+
+ +
+ {this.props.extraHeader ?
{this.props.extraHeader}
: null} +
) let containerProps: ResultContainerProps diff --git a/shared/src/panel/Panel.tsx b/shared/src/panel/Panel.tsx index dd3afc067eae..825f05d85bdc 100644 --- a/shared/src/panel/Panel.tsx +++ b/shared/src/panel/Panel.tsx @@ -83,6 +83,7 @@ export class Panel extends React.PureComponent { location={this.props.location} isLightTheme={this.props.isLightTheme} extensionsController={this.props.extensionsController} + platformContext={this.props.platformContext} settingsCascade={this.props.settingsCascade} fetchHighlightedFileLines={this.props.fetchHighlightedFileLines} /> diff --git a/shared/src/panel/views/FileLocations.tsx b/shared/src/panel/views/FileLocations.tsx index 3ffd0747145b..ef800e82cb9d 100644 --- a/shared/src/panel/views/FileLocations.tsx +++ b/shared/src/panel/views/FileLocations.tsx @@ -1,16 +1,21 @@ import { Location } from '@sourcegraph/extension-api-types' import { LoadingSpinner } from '@sourcegraph/react-loading-spinner' +import * as H from 'history' import { upperFirst } from 'lodash' import AlertCircleIcon from 'mdi-react/AlertCircleIcon' import MapSearchIcon from 'mdi-react/MapSearchIcon' import * as React from 'react' import { Observable, Subject, Subscription } from 'rxjs' import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' +import { ActionsNavItems } from '../../actions/ActionsNavItems' +import { ContributableMenu } from '../../api/protocol' import { FetchFileCtx } from '../../components/CodeExcerpt' import { FileMatch, IFileMatch, ILineMatch } from '../../components/FileMatch' import { VirtualList } from '../../components/VirtualList' -import { asError } from '../../util/errors' +import { ExtensionsControllerProps } from '../../extensions/controller' +import { PlatformContextProps } from '../../platform/context' import { ErrorLike, isErrorLike } from '../../util/errors' +import { asError } from '../../util/errors' import { propertyIsDefined } from '../../util/types' import { parseRepoURI, toPrettyBlobURL } from '../../util/url' @@ -26,7 +31,9 @@ export const FileLocationsNotFound: React.FunctionComponent = () => ( ) -interface Props { +interface Props extends ExtensionsControllerProps, PlatformContextProps { + location: H.Location + /** * The observable that emits the locations. */ @@ -135,6 +142,31 @@ export class FileLocations extends React.PureComponent { key={i} expanded={true} result={refsToFileMatch(uri, locationsByURI.get(uri)!)} + extraHeader={ + + } icon={this.props.icon} onSelect={this.onSelect} showAllMatches={true} diff --git a/shared/src/panel/views/HierarchicalLocationsView.tsx b/shared/src/panel/views/HierarchicalLocationsView.tsx index d2a2ebbf9d42..0fa657af17ad 100644 --- a/shared/src/panel/views/HierarchicalLocationsView.tsx +++ b/shared/src/panel/views/HierarchicalLocationsView.tsx @@ -1,5 +1,6 @@ import { Location } from '@sourcegraph/extension-api-types' import { LoadingSpinner } from '@sourcegraph/react-loading-spinner' +import * as H from 'history' import * as React from 'react' import { Observable, of, Subject, Subscription } from 'rxjs' import { catchError, distinctUntilChanged, endWith, map, startWith, switchMap, tap } from 'rxjs/operators' @@ -8,6 +9,7 @@ import { RepositoryIcon } from '../../components/icons' // TODO: Switch to mdi i import { RepoLink } from '../../components/RepoLink' import { Resizable } from '../../components/Resizable' import { ExtensionsControllerProps } from '../../extensions/controller' +import { PlatformContextProps } from '../../platform/context' import { SettingsCascadeProps } from '../../settings/settings' import { ErrorLike, isErrorLike } from '../../util/errors' import { asError } from '../../util/errors' @@ -16,7 +18,7 @@ import { registerPanelToolbarContributions } from './contributions' import { FileLocations, FileLocationsError, FileLocationsNotFound } from './FileLocations' import { groupLocations } from './locations' -interface Props extends ExtensionsControllerProps, SettingsCascadeProps { +interface Props extends ExtensionsControllerProps, PlatformContextProps, SettingsCascadeProps { /** * The observable that emits the locations. */ @@ -37,6 +39,8 @@ interface Props extends ExtensionsControllerProps, SettingsCascadeProps { className?: string + location: H.Location + isLightTheme: boolean fetchHighlightedFileLines: (ctx: FetchFileCtx, force?: boolean) => Observable @@ -240,6 +244,9 @@ export class HierarchicalLocationsView extends React.PureComponent icon={RepositoryIcon} isLightTheme={this.props.isLightTheme} fetchHighlightedFileLines={this.props.fetchHighlightedFileLines} + location={this.props.location} + extensionsController={this.props.extensionsController} + platformContext={this.props.platformContext} /> ) diff --git a/shared/src/panel/views/PanelView.tsx b/shared/src/panel/views/PanelView.tsx index b8ea404d564c..766560d94f99 100644 --- a/shared/src/panel/views/PanelView.tsx +++ b/shared/src/panel/views/PanelView.tsx @@ -6,12 +6,13 @@ import { PanelViewWithComponent, ViewProviderRegistrationOptions } from '../../a import { FetchFileCtx } from '../../components/CodeExcerpt' import { Markdown } from '../../components/Markdown' import { ExtensionsControllerProps } from '../../extensions/controller' +import { PlatformContextProps } from '../../platform/context' import { SettingsCascadeProps } from '../../settings/settings' import { createLinkClickHandler } from '../../util/linkClickHandler' import { EmptyPanelView } from './EmptyPanelView' import { HierarchicalLocationsView } from './HierarchicalLocationsView' -interface Props extends ExtensionsControllerProps, SettingsCascadeProps { +interface Props extends ExtensionsControllerProps, PlatformContextProps, SettingsCascadeProps { panelView: PanelViewWithComponent & Pick repoName?: string history: H.History @@ -38,17 +39,18 @@ export class PanelView extends React.PureComponent { )} {this.props.panelView.reactElement} - {this.props.panelView.locationProvider && - this.props.repoName && ( - - )} + {this.props.panelView.locationProvider && this.props.repoName && ( + + )} {!this.props.panelView.content && !this.props.panelView.reactElement && !this.props.panelView.locationProvider && } From 1967e48d5fc7f40b76bb4981140421032268cbfb Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Tue, 8 Jan 2019 00:30:43 -0800 Subject: [PATCH 2/2] WIP, tentative, might revert - show actions on lines of CodeExcerpt2 not in FileMatch header This turned out to be more complicated than I thought. --- shared/src/components/CodeExcerpt.tsx | 7 ++- shared/src/components/FileMatch.tsx | 39 ++++++------- shared/src/panel/views/FileLocations.tsx | 71 ++++++++++++------------ 3 files changed, 60 insertions(+), 57 deletions(-) diff --git a/shared/src/components/CodeExcerpt.tsx b/shared/src/components/CodeExcerpt.tsx index b16e06d41390..bbf66e1dee9b 100644 --- a/shared/src/components/CodeExcerpt.tsx +++ b/shared/src/components/CodeExcerpt.tsx @@ -3,6 +3,7 @@ import React from 'react' import VisibilitySensor from 'react-visibility-sensor' import { combineLatest, Observable, Subject, Subscription } from 'rxjs' import { filter, switchMap } from 'rxjs/operators' +import { ActionContribution } from '../api/protocol' import { highlightNode } from '../util/dom' import { Repo } from '../util/url' @@ -20,12 +21,16 @@ interface Props extends Repo { // How many extra lines to show in the excerpt before/after the ref. context?: number highlightRanges: HighlightRange[] + + /** The actions (if any) for each line. */ + actions?: { line: number; actions: ActionContribution }[] + className?: string isLightTheme: boolean fetchHighlightedFileLines: (ctx: FetchFileCtx, force?: boolean) => Observable } -interface HighlightRange { +export interface HighlightRange { /** * The 0-based line number that this highlight appears in */ diff --git a/shared/src/components/FileMatch.tsx b/shared/src/components/FileMatch.tsx index 92a7550c9387..d7e6d33abd2b 100644 --- a/shared/src/components/FileMatch.tsx +++ b/shared/src/components/FileMatch.tsx @@ -2,10 +2,11 @@ import { flatMap } from 'lodash' import React from 'react' import { Observable } from 'rxjs' import { pluralize } from '../../../shared/src/util/strings' +import { ActionContribution } from '../api/protocol' import * as GQL from '../graphql/schema' import { SymbolIcon } from '../symbols/SymbolIcon' import { toPositionOrRangeHash } from '../util/url' -import { CodeExcerpt, FetchFileCtx } from './CodeExcerpt' +import { CodeExcerpt, FetchFileCtx, HighlightRange } from './CodeExcerpt' import { CodeExcerpt2 } from './CodeExcerpt2' import { mergeContext } from './FileMatchContext' import { Link } from './Link' @@ -20,7 +21,13 @@ export type IFileMatch = Partial> & lineMatches: ILineMatch[] } -export type ILineMatch = Pick +export type ILineMatch = Pick & { + /** + * The actions (if any) that correspond to a match. The `actions[i]` actions are associated with the match at + * `offsetAndLengths[i]`. + */ + actions?: ActionContribution[] +} interface IMatchItem { highlightRanges: { @@ -29,6 +36,9 @@ interface IMatchItem { }[] preview: string line: number + + /** The actions (if any) for the line. */ + actions?: ActionContribution[] } interface Props { @@ -57,11 +67,6 @@ interface Props { */ showAllMatches: boolean - /** - * An extra React fragment to render in the header. - */ - extraHeader?: React.ReactFragment - isLightTheme: boolean allExpanded?: boolean @@ -96,17 +101,12 @@ export class FileMatch extends React.PureComponent { })) const title = ( -
-
- -
- {this.props.extraHeader ?
{this.props.extraHeader}
: null} -
+ ) let containerProps: ResultContainerProps @@ -168,7 +168,7 @@ export class FileMatch extends React.PureComponent { // The number of lines of context to show before and after each match. const context = 1 - const groupsOfItems = mergeContext( + const groupsOfItems: HighlightRange[][] = mergeContext( context, flatMap(showItems, item => item.highlightRanges.map(range => ({ @@ -211,6 +211,7 @@ export class FileMatch extends React.PureComponent { filePath={result.file.path} context={context} highlightRanges={items} + actions={item} className="file-match__item-code-excerpt" isLightTheme={this.props.isLightTheme} fetchHighlightedFileLines={this.props.fetchHighlightedFileLines} diff --git a/shared/src/panel/views/FileLocations.tsx b/shared/src/panel/views/FileLocations.tsx index ef800e82cb9d..f5c95e039b7d 100644 --- a/shared/src/panel/views/FileLocations.tsx +++ b/shared/src/panel/views/FileLocations.tsx @@ -7,8 +7,7 @@ import MapSearchIcon from 'mdi-react/MapSearchIcon' import * as React from 'react' import { Observable, Subject, Subscription } from 'rxjs' import { catchError, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' -import { ActionsNavItems } from '../../actions/ActionsNavItems' -import { ContributableMenu } from '../../api/protocol' +import { ActionContribution } from '../../api/protocol' import { FetchFileCtx } from '../../components/CodeExcerpt' import { FileMatch, IFileMatch, ILineMatch } from '../../components/FileMatch' import { VirtualList } from '../../components/VirtualList' @@ -137,36 +136,11 @@ export class FileLocations extends React.PureComponent { ( + items={orderedURIs.map(({ uri }, i) => ( - } + result={refsToFileMatch(this.props, uri, locationsByURI.get(uri)!)} icon={this.props.icon} onSelect={this.onSelect} showAllMatches={true} @@ -190,7 +164,11 @@ export class FileLocations extends React.PureComponent { } } -function refsToFileMatch(uri: string, refs: Location[]): IFileMatch { +function refsToFileMatch( + { extensionsController }: ExtensionsControllerProps, + uri: string, + locations: Location[] +): IFileMatch { const p = parseRepoURI(uri) return { file: { @@ -210,13 +188,32 @@ function refsToFileMatch(uri: string, refs: Location[]): IFileMatch { url: toRepoURL(p.repoName), }, limitHit: false, - lineMatches: refs.filter(propertyIsDefined('range')).map( - (ref): ILineMatch => ({ - preview: '', - limitHit: false, - lineNumber: ref.range.start.line, - offsetAndLengths: [[ref.range.start.character, ref.range.end.character - ref.range.start.character]], - }) + lineMatches: locations.filter(propertyIsDefined('range')).map( + (location): ILineMatch => { + // TODO: These are fetched once and not updated continuously as the context changes. It also + // assumes they are synchronously available (which was true when this was implemented). These + // assumptions/limitations are OK for this use case, but it is not correct in general. The reason + // for these is to simplify the initial implementation. + let actions: ActionContribution[] | undefined + extensionsController.services.contribution + .getContributions({ + type: 'location', + location, + }) + .subscribe(value => { + actions = value.actions + }) + .unsubscribe() + return { + preview: '', + limitHit: false, + lineNumber: location.range.start.line, + offsetAndLengths: [ + [location.range.start.character, location.range.end.character - location.range.start.character], + ], + actions, + } + } ), } }