Refactoring the contentHoverController file (#214325)

* refactoring content hover controller file

* polishing the code
This commit is contained in:
Aiday Marlen Kyzy 2024-06-06 09:22:05 +02:00 committed by GitHub
parent 5f646b8e67
commit b304df66db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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;
}
}