mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
Refactoring the contentHoverController
file (#214325)
* refactoring content hover controller file * polishing the code
This commit is contained in:
parent
5f646b8e67
commit
b304df66db
|
@ -31,7 +31,7 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
private _colorWidget: IEditorHoverColorPickerWidget | null = null;
|
||||
|
||||
private readonly _computer: ContentHoverComputer;
|
||||
private readonly _widget: ContentHoverWidget;
|
||||
private readonly _contentHoverWidget: ContentHoverWidget;
|
||||
private readonly _participants: IEditorHoverParticipant[];
|
||||
// TODO@aiday-mar make array of participants, dispatch between them
|
||||
private readonly _markdownHoverParticipant: MarkdownHoverParticipant | undefined;
|
||||
|
@ -46,7 +46,7 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
) {
|
||||
super();
|
||||
this._widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor));
|
||||
this._contentHoverWidget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor));
|
||||
const initializedParticipants = this._initializeHoverParticipants();
|
||||
this._participants = initializedParticipants.participants;
|
||||
this._markdownHoverParticipant = initializedParticipants.markdownHoverParticipant;
|
||||
|
@ -78,13 +78,13 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
const messages = (result.hasLoadingMessage ? this._addLoadingMessage(result.value) : result.value);
|
||||
this._withResult(new HoverResult(this._computer.anchor, messages, result.isComplete));
|
||||
}));
|
||||
this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => {
|
||||
this._register(dom.addStandardDisposableListener(this._contentHoverWidget.getDomNode(), 'keydown', (e) => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.hide();
|
||||
}
|
||||
}));
|
||||
this._register(TokenizationRegistry.onDidChange(() => {
|
||||
if (this._widget.position && this._currentResult) {
|
||||
if (this._contentHoverWidget.position && this._currentResult) {
|
||||
this._setCurrentResult(this._currentResult); // render again
|
||||
}
|
||||
}));
|
||||
|
@ -100,61 +100,52 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
focus: boolean,
|
||||
mouseEvent: IEditorMouseEvent | null
|
||||
): boolean {
|
||||
|
||||
if (!this._widget.position || !this._currentResult) {
|
||||
// The hover is not visible
|
||||
const contentHoverIsVisible = this._contentHoverWidget.position && this._currentResult;
|
||||
if (!contentHoverIsVisible) {
|
||||
if (anchor) {
|
||||
this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// The hover is currently visible
|
||||
const isHoverSticky = this._editor.getOption(EditorOption.hover).sticky;
|
||||
const isGettingCloser = (
|
||||
isHoverSticky
|
||||
&& mouseEvent
|
||||
&& this._widget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy)
|
||||
);
|
||||
|
||||
if (isGettingCloser) {
|
||||
// The mouse is getting closer to the hover, so we will keep the hover untouched
|
||||
// But we will kick off a hover update at the new anchor, insisting on keeping the hover visible.
|
||||
const isMouseGettingCloser = mouseEvent && this._contentHoverWidget.isMouseGettingCloser(mouseEvent.event.posx, mouseEvent.event.posy);
|
||||
const isHoverStickyAndIsMouseGettingCloser = isHoverSticky && isMouseGettingCloser;
|
||||
// The mouse is getting closer to the hover, so we will keep the hover untouched
|
||||
// But we will kick off a hover update at the new anchor, insisting on keeping the hover visible.
|
||||
if (isHoverStickyAndIsMouseGettingCloser) {
|
||||
if (anchor) {
|
||||
this._startHoverOperationIfNecessary(anchor, mode, source, focus, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If mouse is not getting closer and anchor not defined, hide the hover
|
||||
if (!anchor) {
|
||||
this._setCurrentResult(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (anchor && this._currentResult.anchor.equals(anchor)) {
|
||||
// The widget is currently showing results for the exact same anchor, so no update is needed
|
||||
// If mouse if not getting closer and anchor is defined, and the new anchor is the same as the previous anchor
|
||||
const currentAnchorEqualsPreviousAnchor = this._currentResult!.anchor.equals(anchor);
|
||||
if (currentAnchorEqualsPreviousAnchor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!anchor.canAdoptVisibleHover(this._currentResult.anchor, this._widget.position)) {
|
||||
// The new anchor is not compatible with the previous anchor
|
||||
// If mouse if not getting closer and anchor is defined, and the new anchor is not compatible with the previous anchor
|
||||
const currentAnchorCompatibleWithPreviousAnchor = anchor.canAdoptVisibleHover(this._currentResult!.anchor, this._contentHoverWidget.position);
|
||||
if (!currentAnchorCompatibleWithPreviousAnchor) {
|
||||
this._setCurrentResult(null);
|
||||
this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// We aren't getting any closer to the hover, so we will filter existing results
|
||||
// and keep those which also apply to the new anchor.
|
||||
this._setCurrentResult(this._currentResult.filter(anchor));
|
||||
this._setCurrentResult(this._currentResult!.filter(anchor));
|
||||
this._startHoverOperationIfNecessary(anchor, mode, source, focus, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private _startHoverOperationIfNecessary(anchor: HoverAnchor, mode: HoverStartMode, source: HoverStartSource, focus: boolean, insistOnKeepingHoverVisible: boolean): void {
|
||||
|
||||
if (this._computer.anchor && this._computer.anchor.equals(anchor)) {
|
||||
// We have to start a hover operation at the exact same anchor as before, so no work is needed
|
||||
const currentAnchorEqualToPreviousHover = this._computer.anchor && this._computer.anchor.equals(anchor);
|
||||
if (currentAnchorEqualToPreviousHover) {
|
||||
return;
|
||||
}
|
||||
this._hoverOperation.cancel();
|
||||
|
@ -166,56 +157,62 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
}
|
||||
|
||||
private _setCurrentResult(hoverResult: HoverResult | null): void {
|
||||
|
||||
if (this._currentResult === hoverResult) {
|
||||
// avoid updating the DOM to avoid resetting the user selection
|
||||
let currentHoverResult = hoverResult;
|
||||
const currentResultEqualToPreviousResult = this._currentResult === currentHoverResult;
|
||||
if (currentResultEqualToPreviousResult) {
|
||||
return;
|
||||
}
|
||||
if (hoverResult && hoverResult.hoverParts.length === 0) {
|
||||
hoverResult = null;
|
||||
const currentHoverResultIsEmpty = currentHoverResult && currentHoverResult.hoverParts.length === 0;
|
||||
if (currentHoverResultIsEmpty) {
|
||||
currentHoverResult = null;
|
||||
}
|
||||
this._currentResult = hoverResult;
|
||||
this._currentResult = currentHoverResult;
|
||||
if (this._currentResult) {
|
||||
this._showHover(this._currentResult.anchor, this._currentResult.hoverParts);
|
||||
} else {
|
||||
this._widget.hide();
|
||||
this._contentHoverWidget.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private _addLoadingMessage(result: IHoverPart[]): IHoverPart[] {
|
||||
if (this._computer.anchor) {
|
||||
for (const participant of this._participants) {
|
||||
if (participant.createLoadingMessage) {
|
||||
const loadingMessage = participant.createLoadingMessage(this._computer.anchor);
|
||||
if (loadingMessage) {
|
||||
return result.slice(0).concat([loadingMessage]);
|
||||
}
|
||||
}
|
||||
if (!this._computer.anchor) {
|
||||
return result;
|
||||
}
|
||||
for (const participant of this._participants) {
|
||||
if (!participant.createLoadingMessage) {
|
||||
continue;
|
||||
}
|
||||
const loadingMessage = participant.createLoadingMessage(this._computer.anchor);
|
||||
if (!loadingMessage) {
|
||||
continue;
|
||||
}
|
||||
return result.slice(0).concat([loadingMessage]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _withResult(hoverResult: HoverResult): void {
|
||||
if (this._widget.position && this._currentResult && this._currentResult.isComplete) {
|
||||
// The hover is visible with a previous complete result.
|
||||
|
||||
if (!hoverResult.isComplete) {
|
||||
// Instead of rendering the new partial result, we wait for the result to be complete.
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._computer.insistOnKeepingHoverVisible && hoverResult.hoverParts.length === 0) {
|
||||
// The hover would now hide normally, so we'll keep the previous messages
|
||||
return;
|
||||
}
|
||||
const previousHoverIsVisibleWithCompleteResult = this._contentHoverWidget.position && this._currentResult && this._currentResult.isComplete;
|
||||
if (!previousHoverIsVisibleWithCompleteResult) {
|
||||
this._setCurrentResult(hoverResult);
|
||||
}
|
||||
// The hover is visible with a previous complete result.
|
||||
const isCurrentHoverResultComplete = hoverResult.isComplete;
|
||||
if (!isCurrentHoverResultComplete) {
|
||||
// Instead of rendering the new partial result, we wait for the result to be complete.
|
||||
return;
|
||||
}
|
||||
const currentHoverResultIsEmpty = hoverResult.hoverParts.length === 0;
|
||||
const insistOnKeepingPreviousHoverVisible = this._computer.insistOnKeepingHoverVisible;
|
||||
const shouldKeepPreviousHoverVisible = currentHoverResultIsEmpty && insistOnKeepingPreviousHoverVisible;
|
||||
if (shouldKeepPreviousHoverVisible) {
|
||||
// The hover would now hide normally, so we'll keep the previous messages
|
||||
return;
|
||||
}
|
||||
|
||||
this._setCurrentResult(hoverResult);
|
||||
}
|
||||
|
||||
private _showHover(anchor: HoverAnchor, hoverParts: IHoverPart[]): void {
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
const disposables = this._renderHoverPartsInFragment(fragment, hoverParts);
|
||||
const fragmentHasContent = fragment.hasChildNodes();
|
||||
|
@ -232,13 +229,13 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
};
|
||||
const onContentsChanged = () => {
|
||||
this._onContentsChanged.fire();
|
||||
this._widget.onContentsChanged();
|
||||
this._contentHoverWidget.onContentsChanged();
|
||||
};
|
||||
const setColorPicker = (widget: IEditorHoverColorPickerWidget) => {
|
||||
this._colorWidget = widget;
|
||||
};
|
||||
const setMinimumDimensions = (dimensions: dom.Dimension) => {
|
||||
this._widget.setMinimumDimensions(dimensions);
|
||||
this._contentHoverWidget.setMinimumDimensions(dimensions);
|
||||
};
|
||||
const context: IEditorHoverRenderContext = { fragment, statusBar, hide, onContentsChanged, setColorPicker, setMinimumDimensions };
|
||||
return context;
|
||||
|
@ -257,7 +254,8 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
const disposables = new DisposableStore();
|
||||
for (const participant of this._participants) {
|
||||
const hoverPartsForParticipant = hoverParts.filter(hoverPart => hoverPart.owner === participant);
|
||||
if (hoverPartsForParticipant.length === 0) {
|
||||
const hasHoverPartsForParticipant = hoverPartsForParticipant.length > 0;
|
||||
if (!hasHoverPartsForParticipant) {
|
||||
continue;
|
||||
}
|
||||
disposables.add(participant.renderHoverParts(context, hoverPartsForParticipant));
|
||||
|
@ -295,7 +293,7 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
isBeforeContent,
|
||||
disposables
|
||||
);
|
||||
this._widget.showAt(fragment, contentHoverVisibleData);
|
||||
this._contentHoverWidget.showAt(fragment, contentHoverVisibleData);
|
||||
}
|
||||
|
||||
private _addEditorDecorations(highlightRange: Range | undefined, disposables: DisposableStore) {
|
||||
|
@ -317,7 +315,7 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
className: 'hoverHighlight'
|
||||
});
|
||||
|
||||
public static computeHoverRanges(editor: ICodeEditor, anchorRange: Range, messages: IHoverPart[]) {
|
||||
public static computeHoverRanges(editor: ICodeEditor, anchorRange: Range, hoverParts: IHoverPart[]) {
|
||||
|
||||
let startColumnBoundary = 1;
|
||||
if (editor.hasModel()) {
|
||||
|
@ -332,17 +330,18 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
// The anchor range is always on a single line
|
||||
const anchorLineNumber = anchorRange.startLineNumber;
|
||||
let renderStartColumn = anchorRange.startColumn;
|
||||
let highlightRange = messages[0].range;
|
||||
let highlightRange = hoverParts[0].range;
|
||||
let forceShowAtRange = null;
|
||||
|
||||
for (const msg of messages) {
|
||||
highlightRange = Range.plusRange(highlightRange, msg.range);
|
||||
if (msg.range.startLineNumber === anchorLineNumber && msg.range.endLineNumber === anchorLineNumber) {
|
||||
for (const hoverPart of hoverParts) {
|
||||
highlightRange = Range.plusRange(highlightRange, hoverPart.range);
|
||||
const hoverRangeIsWithinAnchorLine = hoverPart.range.startLineNumber === anchorLineNumber && hoverPart.range.endLineNumber === anchorLineNumber;
|
||||
if (hoverRangeIsWithinAnchorLine) {
|
||||
// this message has a range that is completely sitting on the line of the anchor
|
||||
renderStartColumn = Math.max(Math.min(renderStartColumn, msg.range.startColumn), startColumnBoundary);
|
||||
renderStartColumn = Math.max(Math.min(renderStartColumn, hoverPart.range.startColumn), startColumnBoundary);
|
||||
}
|
||||
if (msg.forceShowAtRange) {
|
||||
forceShowAtRange = msg.range;
|
||||
if (hoverPart.forceShowAtRange) {
|
||||
forceShowAtRange = hoverPart.range;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -357,45 +356,52 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
}
|
||||
|
||||
public showsOrWillShow(mouseEvent: IEditorMouseEvent): boolean {
|
||||
|
||||
if (this._widget.isResizing) {
|
||||
const isContentWidgetResizing = this._contentHoverWidget.isResizing;
|
||||
if (isContentWidgetResizing) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const anchorCandidates: HoverAnchor[] = [];
|
||||
for (const participant of this._participants) {
|
||||
if (participant.suggestHoverAnchor) {
|
||||
const anchor = participant.suggestHoverAnchor(mouseEvent);
|
||||
if (anchor) {
|
||||
anchorCandidates.push(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const target = mouseEvent.target;
|
||||
|
||||
if (target.type === MouseTargetType.CONTENT_TEXT) {
|
||||
anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));
|
||||
}
|
||||
|
||||
if (target.type === MouseTargetType.CONTENT_EMPTY) {
|
||||
const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2;
|
||||
if (
|
||||
!target.detail.isAfterLines
|
||||
&& typeof target.detail.horizontalDistanceToText === 'number'
|
||||
&& target.detail.horizontalDistanceToText < epsilon
|
||||
) {
|
||||
// Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough
|
||||
anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));
|
||||
}
|
||||
}
|
||||
|
||||
if (anchorCandidates.length === 0) {
|
||||
const anchorCandidates: HoverAnchor[] = this._findHoverAnchorCandidates(mouseEvent);
|
||||
const anchorCandidatesExist = anchorCandidates.length > 0;
|
||||
if (!anchorCandidatesExist) {
|
||||
return this._startShowingOrUpdateHover(null, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent);
|
||||
}
|
||||
const anchor = anchorCandidates[0];
|
||||
return this._startShowingOrUpdateHover(anchor, HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent);
|
||||
}
|
||||
|
||||
private _findHoverAnchorCandidates(mouseEvent: IEditorMouseEvent): HoverAnchor[] {
|
||||
const anchorCandidates: HoverAnchor[] = [];
|
||||
for (const participant of this._participants) {
|
||||
if (!participant.suggestHoverAnchor) {
|
||||
continue;
|
||||
}
|
||||
const anchor = participant.suggestHoverAnchor(mouseEvent);
|
||||
if (!anchor) {
|
||||
continue;
|
||||
}
|
||||
anchorCandidates.push(anchor);
|
||||
}
|
||||
const target = mouseEvent.target;
|
||||
switch (target.type) {
|
||||
case MouseTargetType.CONTENT_TEXT: {
|
||||
anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));
|
||||
break;
|
||||
}
|
||||
case MouseTargetType.CONTENT_EMPTY: {
|
||||
const epsilon = this._editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth / 2;
|
||||
// Let hover kick in even when the mouse is technically in the empty area after a line, given the distance is small enough
|
||||
const mouseIsWithinLinesAndCloseToHover = !target.detail.isAfterLines
|
||||
&& typeof target.detail.horizontalDistanceToText === 'number'
|
||||
&& target.detail.horizontalDistanceToText < epsilon;
|
||||
if (!mouseIsWithinLinesAndCloseToHover) {
|
||||
break;
|
||||
}
|
||||
anchorCandidates.push(new HoverRangeAnchor(0, target.range, mouseEvent.event.posx, mouseEvent.event.posy));
|
||||
break;
|
||||
}
|
||||
}
|
||||
anchorCandidates.sort((a, b) => b.priority - a.priority);
|
||||
return this._startShowingOrUpdateHover(anchorCandidates[0], HoverStartMode.Delayed, HoverStartSource.Mouse, false, mouseEvent);
|
||||
return anchorCandidates;
|
||||
}
|
||||
|
||||
public startShowingAtRange(range: Range, mode: HoverStartMode, source: HoverStartSource, focus: boolean): void {
|
||||
|
@ -419,7 +425,7 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
}
|
||||
|
||||
public getWidgetContent(): string | undefined {
|
||||
const node = this._widget.getDomNode();
|
||||
const node = this._contentHoverWidget.getDomNode();
|
||||
if (!node.textContent) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -427,43 +433,43 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
}
|
||||
|
||||
public containsNode(node: Node | null | undefined): boolean {
|
||||
return (node ? this._widget.getDomNode().contains(node) : false);
|
||||
return (node ? this._contentHoverWidget.getDomNode().contains(node) : false);
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this._widget.focus();
|
||||
this._contentHoverWidget.focus();
|
||||
}
|
||||
|
||||
public scrollUp(): void {
|
||||
this._widget.scrollUp();
|
||||
this._contentHoverWidget.scrollUp();
|
||||
}
|
||||
|
||||
public scrollDown(): void {
|
||||
this._widget.scrollDown();
|
||||
this._contentHoverWidget.scrollDown();
|
||||
}
|
||||
|
||||
public scrollLeft(): void {
|
||||
this._widget.scrollLeft();
|
||||
this._contentHoverWidget.scrollLeft();
|
||||
}
|
||||
|
||||
public scrollRight(): void {
|
||||
this._widget.scrollRight();
|
||||
this._contentHoverWidget.scrollRight();
|
||||
}
|
||||
|
||||
public pageUp(): void {
|
||||
this._widget.pageUp();
|
||||
this._contentHoverWidget.pageUp();
|
||||
}
|
||||
|
||||
public pageDown(): void {
|
||||
this._widget.pageDown();
|
||||
this._contentHoverWidget.pageDown();
|
||||
}
|
||||
|
||||
public goToTop(): void {
|
||||
this._widget.goToTop();
|
||||
this._contentHoverWidget.goToTop();
|
||||
}
|
||||
|
||||
public goToBottom(): void {
|
||||
this._widget.goToBottom();
|
||||
this._contentHoverWidget.goToBottom();
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
|
@ -473,26 +479,26 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
|
|||
}
|
||||
|
||||
public get isColorPickerVisible(): boolean {
|
||||
return this._widget.isColorPickerVisible;
|
||||
return this._contentHoverWidget.isColorPickerVisible;
|
||||
}
|
||||
|
||||
public get isVisibleFromKeyboard(): boolean {
|
||||
return this._widget.isVisibleFromKeyboard;
|
||||
return this._contentHoverWidget.isVisibleFromKeyboard;
|
||||
}
|
||||
|
||||
public get isVisible(): boolean {
|
||||
return this._widget.isVisible;
|
||||
return this._contentHoverWidget.isVisible;
|
||||
}
|
||||
|
||||
public get isFocused(): boolean {
|
||||
return this._widget.isFocused;
|
||||
return this._contentHoverWidget.isFocused;
|
||||
}
|
||||
|
||||
public get isResizing(): boolean {
|
||||
return this._widget.isResizing;
|
||||
return this._contentHoverWidget.isResizing;
|
||||
}
|
||||
|
||||
public get widget() {
|
||||
return this._widget;
|
||||
return this._contentHoverWidget;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue