/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { ISashEvent, IVerticalSashLayoutProvider, Sash } from 'vs/base/browser/ui/sash/sash'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import 'vs/css!./media/referencesWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IRange, Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/modes'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { AriaProvider, DataSource, Delegate, FileReferencesRenderer, OneReferenceRenderer, TreeElement, StringRepresentationProvider, IdentityProvider } from 'vs/editor/contrib/referenceSearch/referencesTree'; import * as nls from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { activeContrastBorder, contrastBorder, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PeekViewWidget } from './peekViewWidget'; import { FileReferences, OneReference, ReferencesModel } from './referencesModel'; import { ITreeRenderer, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IAsyncDataTreeOptions } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { FuzzyScore } from 'vs/base/common/filters'; class DecorationsManager implements IDisposable { private static readonly DecorationOptions = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'reference-decoration' }); private _decorations = new Map(); private _decorationIgnoreSet = new Set(); private _callOnDispose: IDisposable[] = []; private _callOnModelChange: IDisposable[] = []; constructor(private _editor: ICodeEditor, private _model: ReferencesModel) { this._callOnDispose.push(this._editor.onDidChangeModel(() => this._onModelChanged())); this._onModelChanged(); } public dispose(): void { this._callOnModelChange = dispose(this._callOnModelChange); this._callOnDispose = dispose(this._callOnDispose); this.removeDecorations(); } private _onModelChanged(): void { this._callOnModelChange = dispose(this._callOnModelChange); const model = this._editor.getModel(); if (model) { for (const ref of this._model.groups) { if (ref.uri.toString() === model.uri.toString()) { this._addDecorations(ref); return; } } } } private _addDecorations(reference: FileReferences): void { if (!this._editor.hasModel()) { return; } this._callOnModelChange.push(this._editor.getModel().onDidChangeDecorations((event) => this._onDecorationChanged())); const newDecorations: IModelDeltaDecoration[] = []; const newDecorationsActualIndex: number[] = []; for (let i = 0, len = reference.children.length; i < len; i++) { let oneReference = reference.children[i]; if (this._decorationIgnoreSet.has(oneReference.id)) { continue; } newDecorations.push({ range: oneReference.range, options: DecorationsManager.DecorationOptions }); newDecorationsActualIndex.push(i); } const decorations = this._editor.deltaDecorations([], newDecorations); for (let i = 0; i < decorations.length; i++) { this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]); } } private _onDecorationChanged(): void { const toRemove: string[] = []; const model = this._editor.getModel(); if (!model) { return; } this._decorations.forEach((reference, decorationId) => { const newRange = model.getDecorationRange(decorationId); if (!newRange) { return; } let ignore = false; if (Range.equalsRange(newRange, reference.range)) { return; } else if (Range.spansMultipleLines(newRange)) { ignore = true; } else { const lineLength = reference.range.endColumn - reference.range.startColumn; const newLineLength = newRange.endColumn - newRange.startColumn; if (lineLength !== newLineLength) { ignore = true; } } if (ignore) { this._decorationIgnoreSet.add(reference.id); toRemove.push(decorationId); } else { reference.range = newRange; } }); for (let i = 0, len = toRemove.length; i < len; i++) { this._decorations.delete(toRemove[i]); } this._editor.deltaDecorations(toRemove, []); } public removeDecorations(): void { let toRemove: string[] = []; this._decorations.forEach((value, key) => { toRemove.push(key); }); this._editor.deltaDecorations(toRemove, []); this._decorations.clear(); } } class VSash { private _disposables: IDisposable[] = []; private _sash: Sash; private _ratio: number; private _height: number; private _width: number; private _onDidChangePercentages = new Emitter(); constructor(container: HTMLElement, ratio: number) { this._ratio = ratio; this._sash = new Sash(container, { getVerticalSashLeft: () => this._width * this._ratio, getVerticalSashHeight: () => this._height }); // compute the current widget clientX postion since // the sash works with clientX when dragging let clientX: number; this._disposables.push(this._sash.onDidStart((e: ISashEvent) => { clientX = e.startX - (this._width * this.ratio); })); this._disposables.push(this._sash.onDidChange((e: ISashEvent) => { // compute the new position of the sash and from that // compute the new ratio that we are using let newLeft = e.currentX - clientX; if (newLeft > 20 && newLeft + 20 < this._width) { this._ratio = newLeft / this._width; this._sash.layout(); this._onDidChangePercentages.fire(this); } })); } dispose() { this._sash.dispose(); this._onDidChangePercentages.dispose(); dispose(this._disposables); } get onDidChangePercentages() { return this._onDidChangePercentages.event; } set width(value: number) { this._width = value; this._sash.layout(); } set height(value: number) { this._height = value; this._sash.layout(); } get percentages() { let left = 100 * this._ratio; let right = 100 - left; return [`${left}%`, `${right}%`]; } get ratio() { return this._ratio; } } export interface LayoutData { ratio: number; heightInLines: number; } export interface SelectionEvent { kind: 'goto' | 'show' | 'side' | 'open'; source: 'editor' | 'tree' | 'title'; element?: Location; } export const ctxReferenceWidgetSearchTreeFocused = new RawContextKey('referenceSearchTreeFocused', true); /** * ZoneWidget that is shown inside the editor */ export class ReferenceWidget extends PeekViewWidget { private _model: ReferencesModel | undefined; private _decorationsManager: DecorationsManager; private _disposeOnNewModel: IDisposable[] = []; private _callOnDispose: IDisposable[] = []; private _onDidSelectReference = new Emitter(); private _tree: WorkbenchAsyncDataTree; private _treeContainer: HTMLElement; private _sash: VSash; private _preview: ICodeEditor; private _previewModelReference: IReference; private _previewNotAvailableMessage: TextModel; private _previewContainer: HTMLElement; private _messageContainer: HTMLElement; private height: number | undefined; private width: number | undefined; constructor( editor: ICodeEditor, private _defaultTreeKeyboardSupport: boolean, public layoutData: LayoutData, @IThemeService themeService: IThemeService, @ITextModelService private readonly _textModelResolverService: ITextModelService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILabelService private readonly _uriLabel: ILabelService ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }); this._applyTheme(themeService.getTheme()); this._callOnDispose.push(themeService.onThemeChange(this._applyTheme.bind(this))); this.create(); } private _applyTheme(theme: ITheme) { const borderColor = theme.getColor(peekViewBorder) || Color.transparent; this.style({ arrowColor: borderColor, frameColor: borderColor, headerBackgroundColor: theme.getColor(peekViewTitleBackground) || Color.transparent, primaryHeadingColor: theme.getColor(peekViewTitleForeground) || undefined, secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground) || undefined }); } public dispose(): void { this.setModel(undefined); this._callOnDispose = dispose(this._callOnDispose); dispose(this._preview, this._previewNotAvailableMessage, this._tree, this._sash, this._previewModelReference); super.dispose(); } get onDidSelectReference(): Event { return this._onDidSelectReference.event; } show(where: IRange) { this.editor.revealRangeInCenterIfOutsideViewport(where, editorCommon.ScrollType.Smooth); super.show(where, this.layoutData.heightInLines || 18); } focus(): void { this._tree.domFocus(); } protected _onTitleClick(e: IMouseEvent): void { if (this._preview && this._preview.getModel()) { this._onDidSelectReference.fire({ element: this._getFocusedReference(), kind: e.ctrlKey || e.metaKey || e.altKey ? 'side' : 'open', source: 'title' }); } } protected _fillBody(containerElement: HTMLElement): void { this.setCssClass('reference-zone-widget'); // message pane this._messageContainer = dom.append(containerElement, dom.$('div.messages')); dom.hide(this._messageContainer); // editor this._previewContainer = dom.append(containerElement, dom.$('div.preview.inline')); let options: IEditorOptions = { scrollBeyondLastLine: false, scrollbar: { verticalScrollbarSize: 14, horizontal: 'auto', useShadows: true, verticalHasArrows: false, horizontalHasArrows: false }, overviewRulerLanes: 2, fixedOverflowWidgets: true, minimap: { enabled: false } }; this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, this.editor); dom.hide(this._previewContainer); this._previewNotAvailableMessage = TextModel.createFromString(nls.localize('missingPreviewMessage', "no preview available")); // sash this._sash = new VSash(containerElement, this.layoutData.ratio || 0.8); this._sash.onDidChangePercentages(() => { let [left, right] = this._sash.percentages; this._previewContainer.style.width = left; this._treeContainer.style.width = right; this._preview.layout(); this._tree.layout(this.height, this.width && this.width * (1 - this._sash.ratio)); this.layoutData.ratio = this._sash.ratio; }); // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); const renderers = [ this._instantiationService.createInstance(FileReferencesRenderer), this._instantiationService.createInstance(OneReferenceRenderer), ]; const treeOptions: IAsyncDataTreeOptions = { ariaLabel: nls.localize('treeAriaLabel', "References"), keyboardSupport: this._defaultTreeKeyboardSupport, accessibilityProvider: new AriaProvider(), keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider), identityProvider: new IdentityProvider() }; const treeDataSource = this._instantiationService.createInstance(DataSource); this._tree = this._instantiationService.createInstance, ITreeRenderer[], IAsyncDataSource, IAsyncDataTreeOptions, WorkbenchAsyncDataTree>( WorkbenchAsyncDataTree, this._treeContainer, new Delegate(), renderers, treeDataSource, treeOptions ); ctxReferenceWidgetSearchTreeFocused.bindTo(this._tree.contextKeyService); // listen on selection and focus let onEvent = (element: any, kind: 'show' | 'goto' | 'side') => { if (element instanceof OneReference) { if (kind === 'show') { this._revealReference(element, false); } this._onDidSelectReference.fire({ element, kind, source: 'tree' }); } }; this._tree.onDidChangeFocus(e => { onEvent(e.elements[0], 'show'); }); this._tree.onDidChangeSelection(e => { let aside = false; let goto = false; if (e.browserEvent instanceof KeyboardEvent) { // todo@joh make this a command goto = true; } else if (e.browserEvent instanceof MouseEvent) { aside = e.browserEvent.ctrlKey || e.browserEvent.metaKey || e.browserEvent.altKey; goto = e.browserEvent.detail === 2; } if (aside) { onEvent(e.elements[0], 'side'); } else if (goto) { onEvent(e.elements[0], 'goto'); } else { onEvent(e.elements[0], 'show'); } }); dom.hide(this._treeContainer); } protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void { super._doLayoutBody(heightInPixel, widthInPixel); this.height = heightInPixel; this.width = widthInPixel; const height = heightInPixel + 'px'; this._sash.height = heightInPixel; this._sash.width = widthInPixel; // set height/width const [left, right] = this._sash.percentages; this._previewContainer.style.height = height; this._previewContainer.style.width = left; this._treeContainer.style.height = height; this._treeContainer.style.width = right; // forward this._tree.layout(heightInPixel, widthInPixel * (1 - this._sash.ratio)); this._preview.layout(); // store layout data this.layoutData = { heightInLines: this._viewZone ? this._viewZone.heightInLines : 0, ratio: this._sash.ratio }; } public _onWidth(widthInPixel: number): void { this._sash.width = widthInPixel; this._preview.layout(); } public setSelection(selection: OneReference): Promise { return this._revealReference(selection, true).then(() => { if (!this._model) { // disposed return; } // show in tree this._tree.setSelection([selection]); this._tree.setFocus([selection]); }); } public setModel(newModel: ReferencesModel | undefined): Promise | undefined { // clean up this._disposeOnNewModel = dispose(this._disposeOnNewModel); this._model = newModel; if (this._model) { return this._onNewModel(); } return undefined; } private _onNewModel(): Promise { if (!this._model) { return Promise.resolve(undefined); } if (this._model.empty) { this.setTitle(''); this._messageContainer.innerHTML = nls.localize('noResults', "No results"); dom.show(this._messageContainer); return Promise.resolve(undefined); } dom.hide(this._messageContainer); this._decorationsManager = new DecorationsManager(this._preview, this._model); this._disposeOnNewModel.push(this._decorationsManager); // listen on model changes this._disposeOnNewModel.push(this._model.onDidChangeReferenceRange(reference => this._tree.refresh(reference))); // listen on editor this._disposeOnNewModel.push(this._preview.onMouseDown(e => { const { event, target } = e; if (event.detail !== 2) { return; } const element = this._getFocusedReference(); if (!element) { return; } this._onDidSelectReference.fire({ element: { uri: element.uri, range: target.range! }, kind: (event.ctrlKey || event.metaKey || event.altKey) ? 'side' : 'open', source: 'editor' }); })); // make sure things are rendered dom.addClass(this.container, 'results-loaded'); dom.show(this._treeContainer); dom.show(this._previewContainer); this._preview.layout(); this._tree.layout(); this.focus(); // pick input and a reference to begin with return this._tree.setInput(this._model.groups.length === 1 ? this._model.groups[0] : this._model); } private _getFocusedReference(): OneReference | undefined { const [element] = this._tree.getFocus(); if (element instanceof OneReference) { return element; } else if (element instanceof FileReferences) { if (element.children.length > 0) { return element.children[0]; } } return undefined; } private _revealedReference?: OneReference; private async _revealReference(reference: OneReference, revealParent: boolean): Promise { // check if there is anything to do... if (this._revealedReference === reference) { return; } this._revealedReference = reference; // Update widget header if (reference.uri.scheme !== Schemas.inMemory) { this.setTitle(basenameOrAuthority(reference.uri), this._uriLabel.getUriLabel(dirname(reference.uri))); } else { this.setTitle(nls.localize('peekView.alternateTitle', "References")); } const promise = this._textModelResolverService.createModelReference(reference.uri); if (this._tree.getInput() === reference.parent) { this._tree.reveal(reference); } else { if (revealParent) { this._tree.reveal(reference.parent); } await this._tree.expand(reference.parent); this._tree.reveal(reference); } const ref = await promise; if (!this._model) { // disposed ref.dispose(); return; } dispose(this._previewModelReference); // show in editor const model = ref.object; if (model) { const scrollType = this._preview.getModel() === model.textEditorModel ? editorCommon.ScrollType.Smooth : editorCommon.ScrollType.Immediate; const sel = Range.lift(reference.range).collapseToStart(); this._previewModelReference = ref; this._preview.setModel(model.textEditorModel); this._preview.setSelection(sel); this._preview.revealRangeInCenter(sel, scrollType); } else { this._preview.setModel(this._previewNotAvailableMessage); ref.dispose(); } } } // theming export const peekViewTitleBackground = registerColor('peekViewTitle.background', { dark: '#1E1E1E', light: '#FFFFFF', hc: '#0C141F' }, nls.localize('peekViewTitleBackground', 'Background color of the peek view title area.')); export const peekViewTitleForeground = registerColor('peekViewTitleLabel.foreground', { dark: '#FFFFFF', light: '#333333', hc: '#FFFFFF' }, nls.localize('peekViewTitleForeground', 'Color of the peek view title.')); export const peekViewTitleInfoForeground = registerColor('peekViewTitleDescription.foreground', { dark: '#ccccccb3', light: '#6c6c6cb3', hc: '#FFFFFF99' }, nls.localize('peekViewTitleInfoForeground', 'Color of the peek view title info.')); export const peekViewBorder = registerColor('peekView.border', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('peekViewBorder', 'Color of the peek view borders and arrow.')); export const peekViewResultsBackground = registerColor('peekViewResult.background', { dark: '#252526', light: '#F3F3F3', hc: Color.black }, nls.localize('peekViewResultsBackground', 'Background color of the peek view result list.')); export const peekViewResultsMatchForeground = registerColor('peekViewResult.lineForeground', { dark: '#bbbbbb', light: '#646465', hc: Color.white }, nls.localize('peekViewResultsMatchForeground', 'Foreground color for line nodes in the peek view result list.')); export const peekViewResultsFileForeground = registerColor('peekViewResult.fileForeground', { dark: Color.white, light: '#1E1E1E', hc: Color.white }, nls.localize('peekViewResultsFileForeground', 'Foreground color for file nodes in the peek view result list.')); export const peekViewResultsSelectionBackground = registerColor('peekViewResult.selectionBackground', { dark: '#3399ff33', light: '#3399ff33', hc: null }, nls.localize('peekViewResultsSelectionBackground', 'Background color of the selected entry in the peek view result list.')); export const peekViewResultsSelectionForeground = registerColor('peekViewResult.selectionForeground', { dark: Color.white, light: '#6C6C6C', hc: Color.white }, nls.localize('peekViewResultsSelectionForeground', 'Foreground color of the selected entry in the peek view result list.')); export const peekViewEditorBackground = registerColor('peekViewEditor.background', { dark: '#001F33', light: '#F2F8FC', hc: Color.black }, nls.localize('peekViewEditorBackground', 'Background color of the peek view editor.')); export const peekViewEditorGutterBackground = registerColor('peekViewEditorGutter.background', { dark: peekViewEditorBackground, light: peekViewEditorBackground, hc: peekViewEditorBackground }, nls.localize('peekViewEditorGutterBackground', 'Background color of the gutter in the peek view editor.')); export const peekViewResultsMatchHighlight = registerColor('peekViewResult.matchHighlightBackground', { dark: '#ea5c004d', light: '#ea5c004d', hc: null }, nls.localize('peekViewResultsMatchHighlight', 'Match highlight color in the peek view result list.')); export const peekViewEditorMatchHighlight = registerColor('peekViewEditor.matchHighlightBackground', { dark: '#ff8f0099', light: '#f5d802de', hc: null }, nls.localize('peekViewEditorMatchHighlight', 'Match highlight color in the peek view editor.')); export const peekViewEditorMatchHighlightBorder = registerColor('peekViewEditor.matchHighlightBorder', { dark: null, light: null, hc: activeContrastBorder }, nls.localize('peekViewEditorMatchHighlightBorder', 'Match highlight border in the peek view editor.')); registerThemingParticipant((theme, collector) => { const findMatchHighlightColor = theme.getColor(peekViewResultsMatchHighlight); if (findMatchHighlightColor) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .referenceMatch { background-color: ${findMatchHighlightColor}; }`); } const referenceHighlightColor = theme.getColor(peekViewEditorMatchHighlight); if (referenceHighlightColor) { collector.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { background-color: ${referenceHighlightColor}; }`); } const referenceHighlightBorder = theme.getColor(peekViewEditorMatchHighlightBorder); if (referenceHighlightBorder) { collector.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { border: 2px solid ${referenceHighlightBorder}; box-sizing: border-box; }`); } const hcOutline = theme.getColor(activeContrastBorder); if (hcOutline) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .referenceMatch { border: 1px dotted ${hcOutline}; box-sizing: border-box; }`); } const resultsBackground = theme.getColor(peekViewResultsBackground); if (resultsBackground) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree { background-color: ${resultsBackground}; }`); } const resultsMatchForeground = theme.getColor(peekViewResultsMatchForeground); if (resultsMatchForeground) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree { color: ${resultsMatchForeground}; }`); } const resultsFileForeground = theme.getColor(peekViewResultsFileForeground); if (resultsFileForeground) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .reference-file { color: ${resultsFileForeground}; }`); } const resultsSelectedBackground = theme.getColor(peekViewResultsSelectionBackground); if (resultsSelectedBackground) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { background-color: ${resultsSelectedBackground}; }`); } const resultsSelectedForeground = theme.getColor(peekViewResultsSelectionForeground); if (resultsSelectedForeground) { collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { color: ${resultsSelectedForeground} !important; }`); } const editorBackground = theme.getColor(peekViewEditorBackground); if (editorBackground) { collector.addRule( `.monaco-editor .reference-zone-widget .preview .monaco-editor .monaco-editor-background,` + `.monaco-editor .reference-zone-widget .preview .monaco-editor .inputarea.ime-input {` + ` background-color: ${editorBackground};` + `}`); } const editorGutterBackground = theme.getColor(peekViewEditorGutterBackground); if (editorGutterBackground) { collector.addRule( `.monaco-editor .reference-zone-widget .preview .monaco-editor .margin {` + ` background-color: ${editorGutterBackground};` + `}`); } });