Path: blob/main/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../../base/browser/dom.js';6import { IMouseEvent } from '../../../../../base/browser/mouseEvent.js';7import { Orientation } from '../../../../../base/browser/ui/sash/sash.js';8import { Sizing, SplitView } from '../../../../../base/browser/ui/splitview/splitview.js';9import { Color } from '../../../../../base/common/color.js';10import { Emitter, Event } from '../../../../../base/common/event.js';11import { FuzzyScore } from '../../../../../base/common/filters.js';12import { KeyCode } from '../../../../../base/common/keyCodes.js';13import { DisposableStore, dispose, IDisposable, IReference } from '../../../../../base/common/lifecycle.js';14import { Schemas } from '../../../../../base/common/network.js';15import { basenameOrAuthority, dirname } from '../../../../../base/common/resources.js';16import './referencesWidget.css';17import { ICodeEditor } from '../../../../browser/editorBrowser.js';18import { EmbeddedCodeEditorWidget } from '../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js';19import { IEditorOptions } from '../../../../common/config/editorOptions.js';20import { IRange, Range } from '../../../../common/core/range.js';21import { ScrollType } from '../../../../common/editorCommon.js';22import { IModelDeltaDecoration, TrackedRangeStickiness } from '../../../../common/model.js';23import { ModelDecorationOptions, TextModel } from '../../../../common/model/textModel.js';24import { Location } from '../../../../common/languages.js';25import { PLAINTEXT_LANGUAGE_ID } from '../../../../common/languages/modesRegistry.js';26import { ITextEditorModel, ITextModelService } from '../../../../common/services/resolverService.js';27import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, IdentityProvider, OneReferenceRenderer, StringRepresentationProvider, TreeElement } from './referencesTree.js';28import * as peekView from '../../../peekView/browser/peekView.js';29import * as nls from '../../../../../nls.js';30import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';31import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';32import { ILabelService } from '../../../../../platform/label/common/label.js';33import { IWorkbenchAsyncDataTreeOptions, WorkbenchAsyncDataTree } from '../../../../../platform/list/browser/listService.js';34import { IColorTheme, IThemeService } from '../../../../../platform/theme/common/themeService.js';35import { FileReferences, OneReference, ReferencesModel } from '../referencesModel.js';36import { ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js';37import { DataTransfers, IDragAndDropData } from '../../../../../base/browser/dnd.js';38import { ElementsDragAndDropData } from '../../../../../base/browser/ui/list/listView.js';39import { withSelection } from '../../../../../platform/opener/common/opener.js';4041class DecorationsManager implements IDisposable {4243private static readonly DecorationOptions = ModelDecorationOptions.register({44description: 'reference-decoration',45stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,46className: 'reference-decoration'47});4849private _decorations = new Map<string, OneReference>();50private _decorationIgnoreSet = new Set<string>();51private readonly _callOnDispose = new DisposableStore();52private readonly _callOnModelChange = new DisposableStore();5354constructor(private _editor: ICodeEditor, private _model: ReferencesModel) {55this._callOnDispose.add(this._editor.onDidChangeModel(() => this._onModelChanged()));56this._onModelChanged();57}5859dispose(): void {60this._callOnModelChange.dispose();61this._callOnDispose.dispose();62this.removeDecorations();63}6465private _onModelChanged(): void {66this._callOnModelChange.clear();67const model = this._editor.getModel();68if (!model) {69return;70}71for (const ref of this._model.references) {72if (ref.uri.toString() === model.uri.toString()) {73this._addDecorations(ref.parent);74return;75}76}77}7879private _addDecorations(reference: FileReferences): void {80if (!this._editor.hasModel()) {81return;82}83this._callOnModelChange.add(this._editor.getModel().onDidChangeDecorations(() => this._onDecorationChanged()));8485const newDecorations: IModelDeltaDecoration[] = [];86const newDecorationsActualIndex: number[] = [];8788for (let i = 0, len = reference.children.length; i < len; i++) {89const oneReference = reference.children[i];90if (this._decorationIgnoreSet.has(oneReference.id)) {91continue;92}93if (oneReference.uri.toString() !== this._editor.getModel().uri.toString()) {94continue;95}96newDecorations.push({97range: oneReference.range,98options: DecorationsManager.DecorationOptions99});100newDecorationsActualIndex.push(i);101}102103this._editor.changeDecorations((changeAccessor) => {104const decorations = changeAccessor.deltaDecorations([], newDecorations);105for (let i = 0; i < decorations.length; i++) {106this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]);107}108});109}110111private _onDecorationChanged(): void {112const toRemove: string[] = [];113114const model = this._editor.getModel();115if (!model) {116return;117}118119for (const [decorationId, reference] of this._decorations) {120121const newRange = model.getDecorationRange(decorationId);122123if (!newRange) {124continue;125}126127let ignore = false;128if (Range.equalsRange(newRange, reference.range)) {129continue;130131}132133if (Range.spansMultipleLines(newRange)) {134ignore = true;135136} else {137const lineLength = reference.range.endColumn - reference.range.startColumn;138const newLineLength = newRange.endColumn - newRange.startColumn;139140if (lineLength !== newLineLength) {141ignore = true;142}143}144145if (ignore) {146this._decorationIgnoreSet.add(reference.id);147toRemove.push(decorationId);148} else {149reference.range = newRange;150}151}152153for (let i = 0, len = toRemove.length; i < len; i++) {154this._decorations.delete(toRemove[i]);155}156this._editor.removeDecorations(toRemove);157}158159removeDecorations(): void {160this._editor.removeDecorations([...this._decorations.keys()]);161this._decorations.clear();162}163}164165export class LayoutData {166ratio: number = 0.7;167heightInLines: number = 18;168169static fromJSON(raw: string): LayoutData {170let ratio: number | undefined;171let heightInLines: number | undefined;172try {173const data = <LayoutData>JSON.parse(raw);174ratio = data.ratio;175heightInLines = data.heightInLines;176} catch {177//178}179return {180ratio: ratio || 0.7,181heightInLines: heightInLines || 18182};183}184}185186export interface SelectionEvent {187readonly kind: 'goto' | 'show' | 'side' | 'open';188readonly source: 'editor' | 'tree' | 'title';189readonly element?: Location;190}191192class ReferencesTree extends WorkbenchAsyncDataTree<ReferencesModel | FileReferences, TreeElement, FuzzyScore> { }193194class ReferencesDragAndDrop implements ITreeDragAndDrop<TreeElement> {195196private readonly disposables = new DisposableStore();197198constructor(@ILabelService private readonly labelService: ILabelService) { }199200getDragURI(element: TreeElement): string | null {201if (element instanceof FileReferences) {202return element.uri.toString();203} else if (element instanceof OneReference) {204return withSelection(element.uri, element.range).toString();205}206return null;207}208209getDragLabel(elements: TreeElement[]): string | undefined {210if (elements.length === 0) {211return undefined;212}213const labels = elements.map(e => this.labelService.getUriBasenameLabel(e.uri));214return labels.join(', ');215}216217onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {218if (!originalEvent.dataTransfer) {219return;220}221222const elements = (data as ElementsDragAndDropData<TreeElement, TreeElement[]>).elements;223const resources = elements.map(e => this.getDragURI(e)).filter(Boolean);224225if (resources.length) {226// Apply resources as resource-list227originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(resources));228229// Also add as plain text for outside consumers230originalEvent.dataTransfer.setData(DataTransfers.TEXT, resources.join('\n'));231}232}233234onDragOver(): boolean | ITreeDragOverReaction { return false; }235drop(): void { }236dispose(): void { this.disposables.dispose(); }237}238239/**240* ZoneWidget that is shown inside the editor241*/242export class ReferenceWidget extends peekView.PeekViewWidget {243244private _model?: ReferencesModel;245private _decorationsManager?: DecorationsManager;246247private readonly _disposeOnNewModel = new DisposableStore();248private readonly _callOnDispose = new DisposableStore();249250private readonly _onDidSelectReference = new Emitter<SelectionEvent>();251readonly onDidSelectReference = this._onDidSelectReference.event;252253private _tree!: ReferencesTree;254private _treeContainer!: HTMLElement;255private _splitView!: SplitView;256private _preview!: ICodeEditor;257private _previewModelReference!: IReference<ITextEditorModel>;258private _previewNotAvailableMessage!: TextModel;259private _previewContainer!: HTMLElement;260private _messageContainer!: HTMLElement;261private _dim = new dom.Dimension(0, 0);262private _isClosing = false; // whether or not a dispose is already in progress263264constructor(265editor: ICodeEditor,266private _defaultTreeKeyboardSupport: boolean,267public layoutData: LayoutData,268@IThemeService themeService: IThemeService,269@ITextModelService private readonly _textModelResolverService: ITextModelService,270@IInstantiationService private readonly _instantiationService: IInstantiationService,271@peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService,272@ILabelService private readonly _uriLabel: ILabelService,273@IKeybindingService private readonly _keybindingService: IKeybindingService,274) {275super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true, supportOnTitleClick: true }, _instantiationService);276277this._applyTheme(themeService.getColorTheme());278this._callOnDispose.add(themeService.onDidColorThemeChange(this._applyTheme.bind(this)));279this._peekViewService.addExclusiveWidget(editor, this);280this.create();281}282283get isClosing() {284return this._isClosing;285}286287override dispose(): void {288this._isClosing = true;289this.setModel(undefined);290this._callOnDispose.dispose();291this._disposeOnNewModel.dispose();292dispose(this._preview);293dispose(this._previewNotAvailableMessage);294dispose(this._tree);295dispose(this._previewModelReference);296this._splitView.dispose();297super.dispose();298}299300private _applyTheme(theme: IColorTheme) {301const borderColor = theme.getColor(peekView.peekViewBorder) || Color.transparent;302this.style({303arrowColor: borderColor,304frameColor: borderColor,305headerBackgroundColor: theme.getColor(peekView.peekViewTitleBackground) || Color.transparent,306primaryHeadingColor: theme.getColor(peekView.peekViewTitleForeground),307secondaryHeadingColor: theme.getColor(peekView.peekViewTitleInfoForeground)308});309}310311override show(where: IRange) {312super.show(where, this.layoutData.heightInLines || 18);313}314315focusOnReferenceTree(): void {316this._tree.domFocus();317}318319focusOnPreviewEditor(): void {320this._preview.focus();321}322323isPreviewEditorFocused(): boolean {324return this._preview.hasTextFocus();325}326327protected override _onTitleClick(e: IMouseEvent): void {328if (this._preview && this._preview.getModel()) {329this._onDidSelectReference.fire({330element: this._getFocusedReference(),331kind: e.ctrlKey || e.metaKey || e.altKey ? 'side' : 'open',332source: 'title'333});334}335}336337protected _fillBody(containerElement: HTMLElement): void {338this.setCssClass('reference-zone-widget');339340// message pane341this._messageContainer = dom.append(containerElement, dom.$('div.messages'));342dom.hide(this._messageContainer);343344this._splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL });345346// editor347this._previewContainer = dom.append(containerElement, dom.$('div.preview.inline'));348const options: IEditorOptions = {349scrollBeyondLastLine: false,350scrollbar: {351verticalScrollbarSize: 14,352horizontal: 'auto',353useShadows: true,354verticalHasArrows: false,355horizontalHasArrows: false,356alwaysConsumeMouseWheel: true357},358overviewRulerLanes: 2,359fixedOverflowWidgets: true,360minimap: {361enabled: false362}363};364this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, {}, this.editor);365dom.hide(this._previewContainer);366this._previewNotAvailableMessage = this._instantiationService.createInstance(TextModel, nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null);367368// tree369this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline'));370const treeOptions: IWorkbenchAsyncDataTreeOptions<TreeElement, FuzzyScore> = {371keyboardSupport: this._defaultTreeKeyboardSupport,372accessibilityProvider: new AccessibilityProvider(),373keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider),374identityProvider: new IdentityProvider(),375openOnSingleClick: true,376selectionNavigation: true,377overrideStyles: {378listBackground: peekView.peekViewResultsBackground379},380dnd: this._instantiationService.createInstance(ReferencesDragAndDrop)381};382if (this._defaultTreeKeyboardSupport) {383// the tree will consume `Escape` and prevent the widget from closing384this._callOnDispose.add(dom.addStandardDisposableListener(this._treeContainer, 'keydown', (e) => {385if (e.equals(KeyCode.Escape)) {386this._keybindingService.dispatchEvent(e, e.target);387e.stopPropagation();388}389}, true));390}391this._tree = this._instantiationService.createInstance(392ReferencesTree,393'ReferencesWidget',394this._treeContainer,395new Delegate(),396[397this._instantiationService.createInstance(FileReferencesRenderer),398this._instantiationService.createInstance(OneReferenceRenderer),399],400this._instantiationService.createInstance(DataSource),401treeOptions,402);403404// split stuff405this._splitView.addView({406onDidChange: Event.None,407element: this._previewContainer,408minimumSize: 200,409maximumSize: Number.MAX_VALUE,410layout: (width) => {411this._preview.layout({ height: this._dim.height, width });412}413}, Sizing.Distribute);414415this._splitView.addView({416onDidChange: Event.None,417element: this._treeContainer,418minimumSize: 100,419maximumSize: Number.MAX_VALUE,420layout: (width) => {421this._treeContainer.style.height = `${this._dim.height}px`;422this._treeContainer.style.width = `${width}px`;423this._tree.layout(this._dim.height, width);424}425}, Sizing.Distribute);426427this._disposables.add(this._splitView.onDidSashChange(() => {428if (this._dim.width) {429this.layoutData.ratio = this._splitView.getViewSize(0) / this._dim.width;430}431}, undefined));432433// listen on selection and focus434const onEvent = (element: any, kind: 'show' | 'goto' | 'side') => {435if (element instanceof OneReference) {436if (kind === 'show') {437this._revealReference(element, false);438}439this._onDidSelectReference.fire({ element, kind, source: 'tree' });440}441};442this._disposables.add(this._tree.onDidOpen(e => {443if (e.sideBySide) {444onEvent(e.element, 'side');445} else if (e.editorOptions.pinned) {446onEvent(e.element, 'goto');447} else {448onEvent(e.element, 'show');449}450}));451452dom.hide(this._treeContainer);453}454455protected override _onWidth(width: number) {456if (this._dim) {457this._doLayoutBody(this._dim.height, width);458}459}460461protected override _doLayoutBody(heightInPixel: number, widthInPixel: number): void {462super._doLayoutBody(heightInPixel, widthInPixel);463this._dim = new dom.Dimension(widthInPixel, heightInPixel);464this.layoutData.heightInLines = this._viewZone ? this._viewZone.heightInLines : this.layoutData.heightInLines;465this._splitView.layout(widthInPixel);466this._splitView.resizeView(0, widthInPixel * this.layoutData.ratio);467}468469setSelection(selection: OneReference): Promise<any> {470return this._revealReference(selection, true).then(() => {471if (!this._model) {472// disposed473return;474}475// show in tree476this._tree.setSelection([selection]);477this._tree.setFocus([selection]);478});479}480481setModel(newModel: ReferencesModel | undefined): Promise<any> {482// clean up483this._disposeOnNewModel.clear();484this._model = newModel;485if (this._model) {486return this._onNewModel();487}488return Promise.resolve();489}490491private _onNewModel(): Promise<any> {492if (!this._model) {493return Promise.resolve(undefined);494}495496if (this._model.isEmpty) {497this.setTitle('');498this._messageContainer.innerText = nls.localize('noResults', "No results");499dom.show(this._messageContainer);500return Promise.resolve(undefined);501}502503dom.hide(this._messageContainer);504this._decorationsManager = new DecorationsManager(this._preview, this._model);505this._disposeOnNewModel.add(this._decorationsManager);506507// listen on model changes508this._disposeOnNewModel.add(this._model.onDidChangeReferenceRange(reference => this._tree.rerender(reference)));509510// listen on editor511this._disposeOnNewModel.add(this._preview.onMouseDown(e => {512const { event, target } = e;513if (event.detail !== 2) {514return;515}516const element = this._getFocusedReference();517if (!element) {518return;519}520this._onDidSelectReference.fire({521element: { uri: element.uri, range: target.range! },522kind: (event.ctrlKey || event.metaKey || event.altKey) ? 'side' : 'open',523source: 'editor'524});525}));526527// make sure things are rendered528this.container!.classList.add('results-loaded');529dom.show(this._treeContainer);530dom.show(this._previewContainer);531this._splitView.layout(this._dim.width);532this.focusOnReferenceTree();533534// pick input and a reference to begin with535return this._tree.setInput(this._model.groups.length === 1 ? this._model.groups[0] : this._model);536}537538private _getFocusedReference(): OneReference | undefined {539const [element] = this._tree.getFocus();540if (element instanceof OneReference) {541return element;542} else if (element instanceof FileReferences) {543if (element.children.length > 0) {544return element.children[0];545}546}547return undefined;548}549550async revealReference(reference: OneReference): Promise<void> {551await this._revealReference(reference, false);552this._onDidSelectReference.fire({ element: reference, kind: 'goto', source: 'tree' });553}554555private _revealedReference?: OneReference;556557private async _revealReference(reference: OneReference, revealParent: boolean): Promise<void> {558559// check if there is anything to do...560if (this._revealedReference === reference) {561return;562}563this._revealedReference = reference;564565// Update widget header566if (reference.uri.scheme !== Schemas.inMemory) {567this.setTitle(basenameOrAuthority(reference.uri), this._uriLabel.getUriLabel(dirname(reference.uri)));568} else {569this.setTitle(nls.localize('peekView.alternateTitle', "References"));570}571572const promise = this._textModelResolverService.createModelReference(reference.uri);573574if (this._tree.getInput() === reference.parent) {575this._tree.reveal(reference);576} else {577if (revealParent) {578this._tree.reveal(reference.parent);579}580await this._tree.expand(reference.parent);581this._tree.reveal(reference);582}583584const ref = await promise;585586if (!this._model) {587// disposed588ref.dispose();589return;590}591592dispose(this._previewModelReference);593594// show in editor595const model = ref.object;596if (model) {597const scrollType = this._preview.getModel() === model.textEditorModel ? ScrollType.Smooth : ScrollType.Immediate;598const sel = Range.lift(reference.range).collapseToStart();599this._previewModelReference = ref;600this._preview.setModel(model.textEditorModel);601this._preview.setSelection(sel);602this._preview.revealRangeInCenter(sel, scrollType);603} else {604this._preview.setModel(this._previewNotAvailableMessage);605ref.dispose();606}607}608}609610611