Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ContentHoverWidget respects Theia styles #14836

74 changes: 74 additions & 0 deletions packages/monaco/src/browser/content-hover-widget-patcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// *****************************************************************************
// Copyright (C) 2025 and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { IPosition } from '@theia/monaco-editor-core/esm/vs/editor/common/core/position';
import { ContentHoverWidget } from '@theia/monaco-editor-core/esm/vs/editor/contrib/hover/browser/contentHoverWidget';

// https://github.com/microsoft/vscode/blob/1430e1845cbf5ec29a2fc265f12c7fb5c3d685c3/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts#L13-L14
const VSCODE_TOP_HEIGHT = 30;
const VSCODE_BOTTOM_HEIGHT = 24;

export interface SetActualHeightForContentHoverWidgetParams {
topHeight?: number;
bottomHeight?: number;
}

export interface ContentHoverWidgetPatcher {
setActualHeightForContentHoverWidget(params: SetActualHeightForContentHoverWidgetParams): void;
}

export function createContentHoverWidgetPatcher(): ContentHoverWidgetPatcher {
let actualTopDiff: number | undefined;
let actualBottomDiff: number | undefined;

const originalAvailableVerticalSpaceAbove = ContentHoverWidget.prototype['_availableVerticalSpaceAbove'];
ContentHoverWidget.prototype['_availableVerticalSpaceAbove'] = function (position: IPosition): number | undefined {
const originalValue = originalAvailableVerticalSpaceAbove.call(this, position);
if (typeof originalValue !== 'number' || !actualTopDiff) {
return originalValue;
}
// The original implementation deducts the height of the top panel from the total available space.
// https://github.com/microsoft/vscode/blob/1430e1845cbf5ec29a2fc265f12c7fb5c3d685c3/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts#L71
// However, in Theia, the top panel has generally different size (especially when the toolbar is visible).
// This additional height must be further subtracted from the computed height for accurate positioning.
return originalValue - actualTopDiff;
};

const originalAvailableVerticalSpaceBelow = ContentHoverWidget.prototype['_availableVerticalSpaceBelow'];
ContentHoverWidget.prototype['_availableVerticalSpaceBelow'] = function (position: IPosition): number | undefined {
const originalValue = originalAvailableVerticalSpaceBelow.call(this, position);
if (typeof originalValue !== 'number' || !actualBottomDiff) {
return originalValue;
}
// The original method subtracts the height of the bottom panel from the overall available height.
// https://github.com/microsoft/vscode/blob/1430e1845cbf5ec29a2fc265f12c7fb5c3d685c3/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts#L83
// In Theia, the status bar has different height than in VS Code, which means this difference
// should be also removed to ensure the calculated available space is accurate.
// Note that removing negative value will increase the available space.
return originalValue - actualBottomDiff;
};

return {
setActualHeightForContentHoverWidget(params): void {
if (typeof params.topHeight === 'number') {
actualTopDiff = params.topHeight - VSCODE_TOP_HEIGHT;
}
if (typeof params.bottomHeight === 'number') {
actualBottomDiff = params.bottomHeight - VSCODE_BOTTOM_HEIGHT;
}
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// *****************************************************************************
// Copyright (C) 2025 and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable } from '@theia/core/shared/inversify';
import { ApplicationShell, FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { SetActualHeightForContentHoverWidgetParams } from './content-hover-widget-patcher';
import { contentHoverWidgetPatcher } from './monaco-init';

@injectable()
export class DefaultContentHoverWidgetPatcher implements FrontendApplicationContribution {
onStart(app: FrontendApplication): void {
const shell = app.shell;

this.updateContentHoverWidgetHeight({
topHeight: this.getTopPanelHeight(shell),
bottomHeight: this.getStatusBarHeight(shell)
});

shell['statusBar'].onDidChangeVisibility(() => {
this.updateContentHoverWidgetHeight({
bottomHeight: this.getStatusBarHeight(shell)
});
});
}

protected updateContentHoverWidgetHeight(params: SetActualHeightForContentHoverWidgetParams): void {
contentHoverWidgetPatcher.setActualHeightForContentHoverWidget(params);
}

protected getTopPanelHeight(shell: ApplicationShell): number {
return shell.topPanel.node.getBoundingClientRect().height;
}

protected getStatusBarHeight(shell: ApplicationShell): number {
return shell['statusBar'].node.getBoundingClientRect().height;
}
}
4 changes: 4 additions & 0 deletions packages/monaco/src/browser/monaco-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/co
import { IThemeService } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService.js';
import { ActiveMonacoUndoRedoHandler, FocusedMonacoUndoRedoHandler } from './monaco-undo-redo-handler';
import { ILogService } from '@theia/monaco-editor-core/esm/vs/platform/log/common/log';
import { DefaultContentHoverWidgetPatcher } from './default-content-hover-widget-patcher';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(MonacoThemingService).toSelf().inSingletonScope();
Expand Down Expand Up @@ -184,6 +185,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ActiveMonacoUndoRedoHandler).toSelf().inSingletonScope();
bind(UndoRedoHandler).toService(FocusedMonacoUndoRedoHandler);
bind(UndoRedoHandler).toService(ActiveMonacoUndoRedoHandler);

bind(DefaultContentHoverWidgetPatcher).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(DefaultContentHoverWidgetPatcher);
});

export const MonacoConfigurationService = Symbol('MonacoConfigurationService');
Expand Down
7 changes: 5 additions & 2 deletions packages/monaco/src/browser/monaco-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

/*
* The code in this file is responsible for overriding service implementations in the Monaco editor with our own Theia-based implementations.
* Since we only get a single chance to call `StandaloneServies.initialize()` with our overrides, we need to make sure that intialize is called before the first call to
* `StandaloneServices.get()` or `StandaloneServies.initialize()`. As we do not control the mechanics of Inversify instance constructions, the approach here is to call
* Since we only get a single chance to call `StandaloneServices.initialize()` with our overrides, we need to make sure that initialize is called before the first call to
* `StandaloneServices.get()` or `StandaloneServices.initialize()`. As we do not control the mechanics of Inversify instance constructions, the approach here is to call
* `MonacoInit.init()` from the `index.js` file after all container modules are loaded, but before the first object is fetched from it.
* `StandaloneServices.initialize()` is called with service descriptors, not service instances. This lets us finish all overrides before any inversify object is constructed and
* might call `initialize()` while being constructed.
Expand Down Expand Up @@ -50,6 +50,9 @@ import { MonacoQuickInputImplementation } from './monaco-quick-input-service';
import { IQuickInputService } from '@theia/monaco-editor-core/esm/vs/platform/quickinput/common/quickInput';
import { IStandaloneThemeService } from '@theia/monaco-editor-core/esm/vs/editor/standalone/common/standaloneTheme';
import { MonacoStandaloneThemeService } from './monaco-standalone-theme-service';
import { createContentHoverWidgetPatcher } from './content-hover-widget-patcher';

export const contentHoverWidgetPatcher = createContentHoverWidgetPatcher();

class MonacoEditorServiceConstructor {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// *****************************************************************************
// Copyright (C) 2025 and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { ApplicationShell, FrontendApplication } from '@theia/core/lib/browser';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import { DefaultContentHoverWidgetPatcher } from '@theia/monaco/lib/browser/default-content-hover-widget-patcher';
import { ApplicationShellWithToolbarOverride } from './application-shell-with-toolbar-override';

@injectable()
export class ToolbarContentHoverWidgetPatcher extends DefaultContentHoverWidgetPatcher {

override onStart(app: FrontendApplication): void {
super.onStart(app);
const shell = app.shell;
if (shell instanceof ApplicationShellWithToolbarOverride) {
shell['toolbar'].onDidChangeVisibility(() => {
this.updateContentHoverWidgetHeight({
topHeight: this.getTopPanelHeight(shell)
});
});
}
}

protected override getTopPanelHeight(shell: ApplicationShell): number {
const defaultHeight = shell.topPanel.node.getBoundingClientRect().height;
if (shell instanceof ApplicationShellWithToolbarOverride) {
const toolbarHeight = shell['toolbar'].node.getBoundingClientRect().height;
return defaultHeight + toolbarHeight;
}
return defaultHeight;
}
}

export const bindToolbarContentHoverWidgetPatcher = (bind: interfaces.Bind, rebind: interfaces.Rebind, unbind: interfaces.Unbind): void => {
bind(ToolbarContentHoverWidgetPatcher).toSelf().inSingletonScope();
rebind(DefaultContentHoverWidgetPatcher).toService(ToolbarContentHoverWidgetPatcher);
};

2 changes: 2 additions & 0 deletions packages/toolbar/src/browser/toolbar-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import '../../src/browser/style/toolbar.css';
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { bindToolbarApplicationShell } from './application-shell-with-toolbar-override';
import { bindToolbar } from './toolbar-command-contribution';
import { bindToolbarContentHoverWidgetPatcher } from './toolbar-content-hover-widget-patcher';

export default new ContainerModule((
bind: interfaces.Bind,
Expand All @@ -27,4 +28,5 @@ export default new ContainerModule((
) => {
bindToolbarApplicationShell(bind, rebind, unbind);
bindToolbar(bind);
bindToolbarContentHoverWidgetPatcher(bind, rebind, unbind);
});
Loading