deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,31 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - MESSAGES EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/messages` extension provides the ability to display user notifications in the application.
## Additional Information
- [API documentation for `@theia/messages`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_messages.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,51 @@
{
"name": "@theia/messages",
"version": "1.68.0",
"description": "Theia - Messages Extension",
"dependencies": {
"@theia/core": "1.68.0",
"react-perfect-scrollbar": "^1.5.3",
"ts-md5": "^1.2.2",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/messages-frontend-module",
"backend": "lib/node/messages-backend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,43 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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 '../../src/browser/style/index.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import { MessageClient } from '@theia/core/lib/common';
import { NotificationManager } from './notifications-manager';
import { bindNotificationPreferences } from '../common/notification-preferences';
import { NotificationsRenderer } from './notifications-renderer';
import { NotificationsContribution } from './notifications-contribution';
import { FrontendApplicationContribution, KeybindingContribution, StylingParticipant } from '@theia/core/lib/browser';
import { CommandContribution, MenuContribution } from '@theia/core';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { NotificationContentRenderer } from './notification-content-renderer';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(NotificationContentRenderer).toSelf().inSingletonScope();
bind(NotificationsRenderer).toSelf().inSingletonScope();
bind(NotificationsContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationsContribution);
bind(CommandContribution).toService(NotificationsContribution);
bind(KeybindingContribution).toService(NotificationsContribution);
bind(ColorContribution).toService(NotificationsContribution);
bind(StylingParticipant).toService(NotificationsContribution);
bind(MenuContribution).toService(NotificationsContribution);
bind(NotificationManager).toSelf().inSingletonScope();
rebind(MessageClient).toService(NotificationManager);
bindNotificationPreferences(bind);
});

View File

@@ -0,0 +1,97 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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 * as React from '@theia/core/shared/react';
import { DisposableCollection } from '@theia/core';
import { NotificationManager, NotificationUpdateEvent } from './notifications-manager';
import { NotificationComponent } from './notification-component';
import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common/nls';
const PerfectScrollbar = require('react-perfect-scrollbar');
export interface NotificationCenterComponentProps {
readonly manager: NotificationManager;
readonly contextMenuRenderer: ContextMenuRenderer;
}
type NotificationCenterComponentState = Pick<NotificationUpdateEvent, Exclude<keyof NotificationUpdateEvent, 'toasts'>>;
export class NotificationCenterComponent extends React.Component<NotificationCenterComponentProps, NotificationCenterComponentState> {
constructor(props: NotificationCenterComponentProps) {
super(props);
this.state = {
notifications: [],
visibilityState: 'hidden'
};
}
protected readonly toDisposeOnUnmount = new DisposableCollection();
override async componentDidMount(): Promise<void> {
this.toDisposeOnUnmount.push(
this.props.manager.onUpdated(({ notifications, visibilityState }) => {
this.setState({
notifications: notifications,
visibilityState
});
})
);
}
override componentWillUnmount(): void {
this.toDisposeOnUnmount.dispose();
}
override render(): React.ReactNode {
const empty = this.state.notifications.length === 0;
const title = empty
? nls.localizeByDefault('No New Notifications')
: nls.localizeByDefault('Notifications');
return (
<div className={`theia-notifications-container theia-notification-center ${this.state.visibilityState === 'center' ? 'open' : 'closed'}`}>
<div className='theia-notification-center-header'>
<div className='theia-notification-center-header-title'>{title}</div>
<div className='theia-notification-center-header-actions'>
<ul className='theia-notification-actions'>
<li className={codicon('clear-all', true)} title={nls.localizeByDefault('Clear All Notifications')}
onClick={this.onClearAll} />
<li className={codicon('chevron-down', true)} title={nls.localizeByDefault('Hide Notifications')}
onClick={this.onHide} />
</ul>
</div>
</div>
<PerfectScrollbar className='theia-notification-list-scroll-container'>
<div className='theia-notification-list'>
{this.state.notifications.map(notification =>
<NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager}
contextMenuRenderer={this.props.contextMenuRenderer} />
)}
</div>
</PerfectScrollbar>
</div>
);
}
protected onHide = () => {
this.props.manager.hideCenter();
};
protected onClearAll = () => {
this.props.manager.clearAll();
};
}

View File

@@ -0,0 +1,143 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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 * as React from '@theia/core/shared/react';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { NotificationManager, Notification } from './notifications-manager';
import { codicon, ContextMenuRenderer } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common/nls';
import { NOTIFICATION_CONTEXT_MENU } from './notifications-commands';
export interface NotificationComponentProps {
readonly manager: NotificationManager;
readonly notification: Notification;
readonly contextMenuRenderer?: ContextMenuRenderer;
}
export class NotificationComponent extends React.Component<NotificationComponentProps> {
constructor(props: NotificationComponentProps) {
super(props);
this.state = {};
}
protected onClear = (event: React.MouseEvent) => {
if (event.target instanceof HTMLElement) {
const messageId = event.target.dataset.messageId;
if (messageId) {
this.props.manager.clear(messageId);
}
}
};
protected onToggleExpansion = (event: React.MouseEvent) => {
if (event.target instanceof HTMLElement) {
const messageId = event.target.dataset.messageId;
if (messageId) {
this.props.manager.toggleExpansion(messageId);
}
}
};
protected onAction = (event: React.MouseEvent) => {
if (event.target instanceof HTMLElement) {
const messageId = event.target.dataset.messageId;
const action = event.target.dataset.action;
if (messageId && action) {
this.props.manager.accept(messageId, action);
}
}
};
protected onMessageClick = (event: React.MouseEvent) => {
if (event.target instanceof HTMLAnchorElement) {
event.stopPropagation();
event.preventDefault();
const link = event.target.href;
this.props.manager.openLink(link);
}
};
protected onContextMenu = (event: React.MouseEvent<HTMLElement>) => {
if (this.props.contextMenuRenderer) {
event.preventDefault();
event.stopPropagation();
this.props.contextMenuRenderer.render({
menuPath: NOTIFICATION_CONTEXT_MENU,
anchor: { x: event.clientX, y: event.clientY },
args: [this.props.notification],
context: event.currentTarget
});
}
};
override render(): React.ReactNode {
const { messageId, message, type, progress, collapsed, expandable, source, actions } = this.props.notification;
const isProgress = type === 'progress' || typeof progress === 'number';
const icon = type === 'progress' ? 'info' : type;
return (<div key={messageId} className='theia-notification-list-item-container' onContextMenu={this.onContextMenu}>
<div className='theia-notification-list-item' tabIndex={0}>
<div className={`theia-notification-list-item-content ${collapsed ? 'collapsed' : ''}`}>
<div className='theia-notification-list-item-content-main'>
<div className={`theia-notification-icon ${codicon(icon)} ${icon}`} />
<div className='theia-notification-message'>
<span
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(message, {
ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs
})
}}
onClick={this.onMessageClick}
/>
</div>
<ul className='theia-notification-actions'>
{expandable && (
<li className={codicon('chevron-down', true) + (collapsed ? ' expand' : ' collapse')} title={collapsed ? 'Expand' : 'Collapse'}
data-message-id={messageId} onClick={this.onToggleExpansion} />
)}
{!isProgress && (<li className={codicon('close', true)} title={nls.localizeByDefault('Clear')} data-message-id={messageId}
onClick={this.onClear} />)}
</ul>
</div>
{(source || !!actions.length) && (
<div className='theia-notification-list-item-content-bottom'>
<div className='theia-notification-source'>
{source && (<span>{source}</span>)}
</div>
<div className='theia-notification-buttons'>
{actions && actions.map((action, index) => (
<button key={messageId + `-action-${index}`} className='theia-button'
data-message-id={messageId} data-action={action}
onClick={this.onAction}>
{action}
</button>
))}
</div>
</div>
)}
</div>
{isProgress && (
<div className='theia-notification-item-progress'>
<div className={`theia-notification-item-progressbar ${progress ? 'determinate' : 'indeterminate'}`}
style={{ width: `${progress ?? '100'}%` }} />
</div>
)}
</div>
</div>);
}
}

View File

@@ -0,0 +1,73 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox 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 { expect } from 'chai';
import { NotificationContentRenderer } from './notification-content-renderer';
describe('notification-content-renderer', () => {
const contentRenderer = new NotificationContentRenderer();
it('should remove new lines', () => {
expectRenderedContent('foo\nbar', 'foo bar');
expectRenderedContent('foo\n\n\nbar', 'foo bar');
});
it('should render links', () => {
expectRenderedContent(
'Link to [theia](https://github.com/eclipse-theia/theia)!',
'Link to <a href="https://github.com/eclipse-theia/theia">theia</a>!'
);
expectRenderedContent(
'Link to [theia](https://github.com/eclipse-theia/theia "title on hover")!',
'Link to <a href="https://github.com/eclipse-theia/theia" title="title on hover">theia</a>!'
);
expectRenderedContent(
'Click [here](command:my-command-id) to open stuff!',
'Click <a href="command:my-command-id">here</a> to open stuff!'
);
expectRenderedContent(
'Click [here](javascript:window.alert();) to open stuff!',
'Click [here](javascript:window.alert();) to open stuff!'
);
});
it('should render markdown', () => {
expectRenderedContent(
'*italic*',
'<em>italic</em>'
);
expectRenderedContent(
'**bold**',
'<strong>bold</strong>'
);
});
it('should not render html', () => {
expectRenderedContent(
'<script>document.getElementById("demo").innerHTML = "Hello JavaScript!";</script>',
'&lt;script&gt;document.getElementById(&quot;demo&quot;).innerHTML = &quot;Hello JavaScript!&quot;;&lt;/script&gt;'
);
expectRenderedContent(
'<a href="javascript:window.alert();">foobar</a>',
'&lt;a href=&quot;javascript:window.alert();&quot;&gt;foobar&lt;/a&gt;'
);
});
const expectRenderedContent = (input: string, output: string) =>
expect(contentRenderer.renderMessage(input)).to.be.equal(output);
});

View File

@@ -0,0 +1,31 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox 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 * as markdownit from '@theia/core/shared/markdown-it';
import { injectable } from '@theia/core/shared/inversify';
@injectable()
export class NotificationContentRenderer {
protected readonly mdEngine = markdownit({ html: false });
renderMessage(content: string): string {
// in alignment with vscode, new lines aren't supported
const contentWithoutNewlines = content.replace(/((\r)?\n)+/gm, ' ');
return this.mdEngine.renderInline(contentWithoutNewlines);
}
}

View File

@@ -0,0 +1,70 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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 * as React from '@theia/core/shared/react';
import { DisposableCollection } from '@theia/core';
import { NotificationManager, NotificationUpdateEvent } from './notifications-manager';
import { NotificationComponent } from './notification-component';
import { CorePreferences } from '@theia/core/lib/common';
import { ContextMenuRenderer } from '@theia/core/lib/browser';
export interface NotificationToastsComponentProps {
readonly manager: NotificationManager;
readonly corePreferences: CorePreferences;
readonly contextMenuRenderer: ContextMenuRenderer;
}
type NotificationToastsComponentState = Pick<NotificationUpdateEvent, Exclude<keyof NotificationUpdateEvent, 'notifications'>>;
export class NotificationToastsComponent extends React.Component<NotificationToastsComponentProps, NotificationToastsComponentState> {
constructor(props: NotificationToastsComponentProps) {
super(props);
this.state = {
toasts: [],
visibilityState: 'hidden'
};
}
protected readonly toDisposeOnUnmount = new DisposableCollection();
override async componentDidMount(): Promise<void> {
this.toDisposeOnUnmount.push(
this.props.manager.onUpdated(({ toasts, visibilityState }) => {
visibilityState = this.props.corePreferences['workbench.silentNotifications'] ? 'hidden' : visibilityState;
this.setState({
toasts: toasts.slice(-3),
visibilityState
});
})
);
}
override componentWillUnmount(): void {
this.toDisposeOnUnmount.dispose();
}
override render(): React.ReactNode {
return (
<div className={`theia-notifications-container theia-notification-toasts ${this.state.visibilityState === 'toasts' ? 'open' : 'closed'}`}>
<div className='theia-notification-list'>
{this.state.toasts.map(notification => <NotificationComponent key={notification.messageId} notification={notification} manager={this.props.manager}
contextMenuRenderer={this.props.contextMenuRenderer} />)}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,58 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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 { Command, nls } from '@theia/core';
import { codicon } from '@theia/core/lib/browser';
export namespace NotificationsCommands {
const NOTIFICATIONS_CATEGORY = 'Notifications';
const NOTIFICATIONS_CATEGORY_KEY = nls.getDefaultKey(NOTIFICATIONS_CATEGORY);
export const TOGGLE = Command.toLocalizedCommand({
id: 'notifications.commands.toggle',
category: NOTIFICATIONS_CATEGORY,
iconClass: codicon('list-unordered'),
label: 'Toggle Notifications'
}, 'theia/messages/toggleNotifications', NOTIFICATIONS_CATEGORY_KEY);
export const SHOW = Command.toDefaultLocalizedCommand({
id: 'notifications.commands.show',
category: NOTIFICATIONS_CATEGORY,
label: 'Show Notifications'
});
export const HIDE = Command.toDefaultLocalizedCommand({
id: 'notifications.commands.hide',
category: NOTIFICATIONS_CATEGORY,
label: 'Hide Notifications'
});
export const CLEAR_ALL = Command.toDefaultLocalizedCommand({
id: 'notifications.commands.clearAll',
category: NOTIFICATIONS_CATEGORY,
iconClass: codicon('clear-all'),
label: 'Clear All Notifications'
});
export const COPY_MESSAGE = Command.toDefaultLocalizedCommand({
id: 'notifications.commands.copyMessage',
category: NOTIFICATIONS_CATEGORY,
label: 'Copy Message'
});
}
export const NOTIFICATION_CONTEXT_MENU: string[] = ['notification-context-menu'];

View File

@@ -0,0 +1,250 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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, inject } from '@theia/core/shared/inversify';
import {
FrontendApplicationContribution, StatusBar, FrontendApplication, StatusBarAlignment,
KeybindingContribution, KeybindingRegistry, StylingParticipant, ColorTheme, CssStyleCollector
} from '@theia/core/lib/browser';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { NotificationsCommands, NOTIFICATION_CONTEXT_MENU } from './notifications-commands';
import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core';
import { Notification, NotificationManager } from './notifications-manager';
import { NotificationsRenderer } from './notifications-renderer';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { Color } from '@theia/core/lib/common/color';
import { nls } from '@theia/core/lib/common/nls';
import { isHighContrast } from '@theia/core/lib/common/theme';
@injectable()
export class NotificationsContribution implements FrontendApplicationContribution, CommandContribution,
KeybindingContribution, ColorContribution, StylingParticipant, MenuContribution {
protected readonly id = 'theia-notification-center';
@inject(NotificationManager)
protected readonly manager: NotificationManager;
@inject(NotificationsRenderer)
protected readonly notificationsRenderer: NotificationsRenderer; // required for initialization
@inject(StatusBar)
protected readonly statusBar: StatusBar;
@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;
onStart(_app: FrontendApplication): void {
this.createStatusBarItem();
}
protected createStatusBarItem(): void {
this.updateStatusBarItem();
this.manager.onUpdated(e => this.updateStatusBarItem(e.notifications.length));
}
protected updateStatusBarItem(count: number = 0): void {
this.statusBar.setElement(this.id, {
text: this.getStatusBarItemText(count),
alignment: StatusBarAlignment.RIGHT,
priority: -900,
command: NotificationsCommands.TOGGLE.id,
tooltip: this.getStatusBarItemTooltip(count),
accessibilityInformation: {
label: this.getStatusBarItemTooltip(count)
}
});
}
protected getStatusBarItemText(count: number): string {
return `$(${count ? 'codicon-bell-dot' : 'codicon-bell'})${count ? ` ${count}` : ''}`;
}
protected getStatusBarItemTooltip(count: number): string {
if (this.manager.centerVisible) {
return nls.localizeByDefault('Hide Notifications');
}
return count === 0
? nls.localizeByDefault('No Notifications')
: count === 1
? nls.localizeByDefault('1 New Notification')
: nls.localizeByDefault('{0} New Notifications', count.toString());
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(NotificationsCommands.TOGGLE, {
isEnabled: () => true,
execute: () => this.manager.toggleCenter()
});
commands.registerCommand(NotificationsCommands.SHOW, {
isEnabled: () => true,
execute: () => this.manager.showCenter()
});
commands.registerCommand(NotificationsCommands.HIDE, {
execute: () => this.manager.hide()
});
commands.registerCommand(NotificationsCommands.CLEAR_ALL, {
execute: () => this.manager.clearAll()
});
commands.registerCommand(NotificationsCommands.COPY_MESSAGE, {
execute: (notification?: Notification) => {
if (!notification || !notification.message) {
return;
}
const plainText = this.notificationToPlainText(notification.message);
if (plainText) {
this.clipboardService.writeText(plainText);
}
},
isEnabled: (notification?: Notification) => !!notification && !!notification.message
});
}
protected notificationToPlainText(html: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
return doc.body.textContent || '';
}
registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(
[...NOTIFICATION_CONTEXT_MENU, '_copy'],
{
commandId: NotificationsCommands.COPY_MESSAGE.id
}
);
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: NotificationsCommands.HIDE.id,
when: 'notificationsVisible',
keybinding: 'esc'
});
}
registerColors(colors: ColorRegistry): void {
colors.register(
{
id: 'notificationCenter.border', defaults: {
hcDark: 'contrastBorder',
hcLight: 'contrastBorder'
}, description: 'Notifications center border color. Notifications slide in from the bottom right of the window.'
},
{
id: 'notificationToast.border', defaults: {
hcDark: 'contrastBorder',
hcLight: 'contrastBorder'
}, description: 'Notification toast border color. Notifications slide in from the bottom right of the window.'
},
{
id: 'notifications.foreground', defaults: {
dark: 'editorWidget.foreground',
light: 'editorWidget.foreground',
hcDark: 'editorWidget.foreground',
hcLight: 'editorWidget.foreground'
}, description: 'Notifications foreground color. Notifications slide in from the bottom right of the window.'
},
{
id: 'notifications.background', defaults: {
dark: 'editorWidget.background',
light: 'editorWidget.background',
hcDark: 'editorWidget.background',
hcLight: 'editorWidget.background'
}, description: 'Notifications background color. Notifications slide in from the bottom right of the window.'
},
{
id: 'notificationLink.foreground', defaults: {
dark: 'textLink.foreground',
light: 'textLink.foreground',
hcDark: 'textLink.foreground',
hcLight: 'textLink.foreground'
}, description: 'Notification links foreground color. Notifications slide in from the bottom right of the window.'
},
{
id: 'notificationCenterHeader.foreground',
description: 'Notifications center header foreground color. Notifications slide in from the bottom right of the window.'
},
{
id: 'notificationCenterHeader.background', defaults: {
dark: Color.lighten('notifications.background', 0.3),
light: Color.darken('notifications.background', 0.05),
hcDark: 'notifications.background',
hcLight: 'notifications.background'
}, description: 'Notifications center header background color. Notifications slide in from the bottom right of the window.'
},
{
id: 'notifications.border', defaults: {
dark: 'notificationCenterHeader.background',
light: 'notificationCenterHeader.background',
hcDark: 'notificationCenterHeader.background',
hcLight: 'notificationCenterHeader.background'
// eslint-disable-next-line max-len
}, description: 'Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.'
},
{
id: 'notificationsErrorIcon.foreground', defaults: {
dark: 'editorError.foreground',
light: 'editorError.foreground',
hcDark: 'editorError.foreground',
hcLight: 'editorError.foreground'
}, description: 'The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.'
},
{
id: 'notificationsWarningIcon.foreground', defaults: {
dark: 'editorWarning.foreground',
light: 'editorWarning.foreground',
hcDark: 'editorWarning.foreground',
hcLight: 'editorWarning.foreground'
}, description: 'The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.'
},
{
id: 'notificationsInfoIcon.foreground', defaults: {
dark: 'editorInfo.foreground',
light: 'editorInfo.foreground',
hcDark: 'editorInfo.foreground',
hcLight: 'editorInfo.foreground'
}, description: 'The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.'
}
);
}
registerThemeStyle(theme: ColorTheme, collector: CssStyleCollector): void {
const notificationsBackground = theme.getColor('notifications.background');
if (notificationsBackground) {
collector.addRule(`
.theia-notification-list-item-container {
background-color: ${notificationsBackground};
}
`);
}
const notificationHover = theme.getColor('list.hoverBackground');
if (notificationHover) {
collector.addRule(`
.theia-notification-list-item:hover:not(:focus) {
background-color: ${notificationHover};
}
`);
}
const focusBorder = theme.getColor('focusBorder');
if (focusBorder && isHighContrast(theme.type)) {
collector.addRule(`
.theia-notification-list-item:hover:not(:focus) {
outline: 1px dashed ${focusBorder};
outline-offset: -2px;
}
`);
}
}
}

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// Copyright (C) 2026 STMicroelectronics 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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
let disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import { expect } from 'chai';
import { Message } from '@theia/core/lib/common';
import { NotificationManager } from './notifications-manager';
import { NotificationPreferences } from '../common/notification-preferences';
import { NotificationContentRenderer } from './notification-content-renderer';
disableJSDOM();
describe('NotificationManager', () => {
const DEFAULT_TIMEOUT = 30000;
class TestableNotificationManager extends NotificationManager {
public testGetTimeout(plainMessage: Message): number {
return this.getTimeout(plainMessage);
}
}
function createNotificationManager(preferenceTimeout: number = DEFAULT_TIMEOUT): TestableNotificationManager {
const manager = new TestableNotificationManager();
(manager as unknown as { preferences: Partial<NotificationPreferences> }).preferences = {
'notification.timeout': preferenceTimeout
};
(manager as unknown as { contentRenderer: NotificationContentRenderer }).contentRenderer = new NotificationContentRenderer();
return manager;
}
before(() => {
disableJSDOM = enableJSDOM();
});
after(() => {
disableJSDOM();
});
describe('getTimeout', () => {
let manager: TestableNotificationManager = createNotificationManager();
it('should return preference timeout when no options are provided', () => {
const message: Message = { text: 'Test message' };
expect(manager.testGetTimeout(message)).to.equal(DEFAULT_TIMEOUT);
});
it('should return preference timeout when options are provided without timeout', () => {
const message: Message = { text: 'Test message', options: {} };
expect(manager.testGetTimeout(message)).to.equal(DEFAULT_TIMEOUT);
});
it('should return explicit timeout when provided', () => {
const message: Message = { text: 'Test message', options: { timeout: 5000 } };
expect(manager.testGetTimeout(message)).to.equal(5000);
});
it('should return 0 when timeout is explicitly set to 0', () => {
const message: Message = { text: 'Test message', options: { timeout: 0 } };
expect(manager.testGetTimeout(message)).to.equal(0);
});
it('should return 0 when actions are present regardless of timeout', () => {
const message: Message = { text: 'Test message', actions: ['OK'], options: { timeout: 5000 } };
expect(manager.testGetTimeout(message)).to.equal(0);
});
it('should return 0 when actions are present and no timeout is set', () => {
const message: Message = { text: 'Test message', actions: ['OK', 'Cancel'] };
expect(manager.testGetTimeout(message)).to.equal(0);
});
it('should return negative timeout when explicitly set', () => {
const message: Message = { text: 'Test message', options: { timeout: -1 } };
expect(manager.testGetTimeout(message)).to.equal(-1);
});
it('should return explicit timeout even if custom preference timeout is available', () => {
const customTimeout = 60000;
manager = createNotificationManager(customTimeout);
const message: Message = { text: 'Test message', options: { timeout: 5000 } };
expect(manager.testGetTimeout(message)).to.equal(5000);
});
it('should return custom preference timeout if no timeout is set', () => {
const customTimeout = 60000;
manager = createNotificationManager(customTimeout);
const message: Message = { text: 'Test message' };
expect(manager.testGetTimeout(message)).to.equal(customTimeout);
});
});
});

View File

@@ -0,0 +1,310 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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, inject, postConstruct } from '@theia/core/shared/inversify';
import { MessageClient, MessageType, Message as PlainMessage, ProgressMessage, ProgressUpdate, CancellationToken } from '@theia/core/lib/common';
import { deepClone } from '@theia/core/lib/common/objects';
import { Emitter } from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Md5 } from 'ts-md5';
import throttle = require('@theia/core/shared/lodash.throttle');
import { NotificationPreferences } from '../common/notification-preferences';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
import { OpenerService } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { NotificationContentRenderer } from './notification-content-renderer';
export interface NotificationUpdateEvent {
readonly notifications: Notification[];
readonly toasts: Notification[];
readonly visibilityState: Notification.Visibility;
}
export interface Notification {
messageId: string;
message: string;
source?: string;
expandable: boolean;
collapsed: boolean;
type: Notification.Type;
actions: string[];
progress?: number;
}
export namespace Notification {
export type Visibility = 'hidden' | 'toasts' | 'center';
export type Type = 'info' | 'warning' | 'error' | 'progress';
}
@injectable()
export class NotificationManager extends MessageClient {
@inject(NotificationPreferences)
protected readonly preferences: NotificationPreferences;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(OpenerService)
protected readonly openerService: OpenerService;
@inject(NotificationContentRenderer)
protected readonly contentRenderer: NotificationContentRenderer;
protected readonly onUpdatedEmitter = new Emitter<NotificationUpdateEvent>();
readonly onUpdated = this.onUpdatedEmitter.event;
protected readonly fireUpdatedEvent = throttle(() => {
const notifications = deepClone(Array.from(this.notifications.values()).filter((notification: Notification) =>
notification.message
));
const toasts = deepClone(Array.from(this.toasts.values()).filter((toast: Notification) =>
toast.message
));
const visibilityState = this.visibilityState;
this.onUpdatedEmitter.fire({ notifications, toasts, visibilityState });
}, 250, { leading: true, trailing: true });
protected readonly deferredResults = new Map<string, Deferred<string | undefined>>();
protected readonly notifications = new Map<string, Notification>();
protected readonly toasts = new Map<string, Notification>();
protected notificationToastsVisibleKey: ContextKey<boolean>;
protected notificationCenterVisibleKey: ContextKey<boolean>;
protected notificationsVisible: ContextKey<boolean>;
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
this.notificationToastsVisibleKey = this.contextKeyService.createKey<boolean>('notificationToastsVisible', false);
this.notificationCenterVisibleKey = this.contextKeyService.createKey<boolean>('notificationCenterVisible', false);
this.notificationsVisible = this.contextKeyService.createKey<boolean>('notificationsVisible', false);
}
protected updateContextKeys(): void {
this.notificationToastsVisibleKey.set(this.toastsVisible);
this.notificationCenterVisibleKey.set(this.centerVisible);
this.notificationsVisible.set(this.toastsVisible || this.centerVisible);
}
get toastsVisible(): boolean {
return this.visibilityState === 'toasts';
}
get centerVisible(): boolean {
return this.visibilityState === 'center';
}
protected visibilityState: Notification.Visibility = 'hidden';
protected setVisibilityState(newState: Notification.Visibility): void {
const changed = this.visibilityState !== newState;
this.visibilityState = newState;
if (changed) {
this.fireUpdatedEvent();
this.updateContextKeys();
}
}
hideCenter(): void {
this.setVisibilityState('hidden');
}
showCenter(): void {
this.setVisibilityState('center');
}
toggleCenter(): void {
this.setVisibilityState(this.centerVisible ? 'hidden' : 'center');
}
accept(notification: Notification | string, action: string | undefined): void {
const messageId = this.getId(notification);
if (!messageId) {
return;
}
this.notifications.delete(messageId);
this.toasts.delete(messageId);
const result = this.deferredResults.get(messageId);
if (!result) {
return;
}
this.deferredResults.delete(messageId);
if ((this.centerVisible && !this.notifications.size) || (this.toastsVisible && !this.toasts.size)) {
this.setVisibilityState('hidden');
}
result.resolve(action);
this.fireUpdatedEvent();
}
protected find(notification: Notification | string): Notification | undefined {
return typeof notification === 'string' ? this.notifications.get(notification) : notification;
}
protected getId(notification: Notification | string): string {
return typeof notification === 'string' ? notification : notification.messageId;
}
hide(): void {
if (this.toastsVisible) {
this.toasts.clear();
}
this.setVisibilityState('hidden');
}
clearAll(): void {
this.setVisibilityState('hidden');
Array.from(this.notifications.values()).forEach(n => this.clear(n));
}
clear(notification: Notification | string): void {
this.accept(notification, undefined);
}
toggleExpansion(notificationId: string): void {
const notification = this.find(notificationId);
if (!notification) {
return;
}
notification.collapsed = !notification.collapsed;
this.fireUpdatedEvent();
}
override showMessage(plainMessage: PlainMessage): Promise<string | undefined> {
const messageId = this.getMessageId(plainMessage);
this.toasts.delete(messageId);
this.notifications.delete(messageId);
const existingDeferred = this.deferredResults.get(messageId);
if (existingDeferred) {
this.deferredResults.delete(messageId);
existingDeferred.resolve(undefined);
}
const message = this.contentRenderer.renderMessage(plainMessage.text);
const type = this.toNotificationType(plainMessage.type);
const actions = Array.from(new Set(plainMessage.actions));
const source = plainMessage.source;
const expandable = this.isExpandable(message, source, actions);
const collapsed = expandable;
const notification = { messageId, message, type, actions, expandable, collapsed };
this.notifications.set(messageId, notification);
const result = new Deferred<string | undefined>();
this.deferredResults.set(messageId, result);
if (!this.centerVisible) {
this.toasts.delete(messageId);
this.toasts.set(messageId, notification);
this.startHideTimeout(messageId, this.getTimeout(plainMessage));
this.setVisibilityState('toasts');
}
this.fireUpdatedEvent();
return result.promise;
}
protected hideTimeouts = new Map<string, number>();
protected startHideTimeout(messageId: string, timeout: number): void {
if (timeout > 0) {
this.hideTimeouts.set(messageId, window.setTimeout(() => {
this.hideToast(messageId);
}, timeout));
}
}
protected hideToast(messageId: string): void {
this.toasts.delete(messageId);
if (this.toastsVisible && !this.toasts.size) {
this.setVisibilityState('hidden');
} else {
this.fireUpdatedEvent();
}
}
protected getTimeout(plainMessage: PlainMessage): number {
if (plainMessage.actions && plainMessage.actions.length > 0) {
// Ignore the timeout if at least one action is set, and we wait for user interaction.
return 0;
}
return plainMessage.options?.timeout !== undefined ? plainMessage.options.timeout : this.preferences['notification.timeout'];
}
protected isExpandable(message: string, source: string | undefined, actions: string[]): boolean {
if (!actions.length && source) {
return true;
}
return message.length > 500;
}
protected toNotificationType(type?: MessageType): Notification.Type {
switch (type) {
case MessageType.Error:
return 'error';
case MessageType.Warning:
return 'warning';
case MessageType.Progress:
return 'progress';
default:
return 'info';
}
}
protected getMessageId(m: PlainMessage): string {
return String(Md5.hashStr(`[${m.type}] ${m.text} : ${(m.actions || []).join(' | ')};`));
}
override async showProgress(messageId: string, plainMessage: ProgressMessage, cancellationToken: CancellationToken): Promise<string | undefined> {
let notification = this.notifications.get(messageId);
if (!notification) {
const message = this.contentRenderer.renderMessage(plainMessage.text);
const type = this.toNotificationType(plainMessage.type);
const actions = Array.from(new Set(plainMessage.actions));
const source = plainMessage.source;
const expandable = this.isExpandable(message, source, actions);
const collapsed = expandable;
notification = { messageId, message, type, actions, expandable, collapsed };
this.notifications.set(messageId, notification);
notification.progress = 0;
cancellationToken.onCancellationRequested(() => {
this.accept(messageId, ProgressMessage.Cancel);
});
}
const result = this.deferredResults.get(messageId) || new Deferred<string | undefined>();
this.deferredResults.set(messageId, result);
if (!this.centerVisible) {
this.toasts.set(messageId, notification);
this.setVisibilityState('toasts');
}
this.fireUpdatedEvent();
return result.promise;
}
override async reportProgress(messageId: string, update: ProgressUpdate, originalMessage: ProgressMessage, cancellationToken: CancellationToken): Promise<void> {
const notification = this.find(messageId);
if (!notification) {
return;
}
if (cancellationToken.isCancellationRequested) {
this.clear(messageId);
} else {
const textMessage = originalMessage.text && update.message ? `${originalMessage.text}: ${update.message}` : originalMessage.text || update?.message;
if (textMessage) {
notification.message = this.contentRenderer.renderMessage(textMessage);
}
notification.progress = this.toPlainProgress(update) || notification.progress;
}
this.fireUpdatedEvent();
}
protected toPlainProgress(update: ProgressUpdate): number | undefined {
return update.work && Math.min(update.work.done / update.work.total * 100, 100);
}
async openLink(link: string): Promise<void> {
const uri = new URI(link);
const opener = await this.openerService.getOpener(uri);
opener.open(uri);
}
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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 * as React from '@theia/core/shared/react';
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ApplicationShell, ContextMenuRenderer } from '@theia/core/lib/browser';
import { NotificationManager } from './notifications-manager';
import { NotificationCenterComponent } from './notification-center-component';
import { NotificationToastsComponent } from './notification-toasts-component';
import { CorePreferences } from '@theia/core';
@injectable()
export class NotificationsRenderer {
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(NotificationManager)
protected readonly manager: NotificationManager;
@inject(CorePreferences)
protected readonly corePreferences: CorePreferences;
@inject(ContextMenuRenderer)
protected readonly contextMenuRenderer: ContextMenuRenderer;
protected containerRoot: Root;
@postConstruct()
protected init(): void {
this.createOverlayContainer();
this.render();
}
protected container: HTMLDivElement;
protected createOverlayContainer(): void {
this.container = window.document.createElement('div');
this.container.className = 'theia-notifications-overlay';
if (window.document.body) {
window.document.body.appendChild(this.container);
}
this.containerRoot = createRoot(this.container);
}
protected render(): void {
this.containerRoot.render(<div>
<NotificationToastsComponent manager={this.manager} corePreferences={this.corePreferences} contextMenuRenderer={this.contextMenuRenderer} />
<NotificationCenterComponent manager={this.manager} contextMenuRenderer={this.contextMenuRenderer} />
</div>);
}
}

View File

@@ -0,0 +1,17 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox 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 "./notifications.css";

View File

@@ -0,0 +1,283 @@
/********************************************************************************
* Copyright (C) 2019 TypeFox 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
********************************************************************************/
/* Container */
.theia-notifications-overlay {
height: 0px;
}
.theia-notifications-container {
position: absolute;
bottom: 36px;
right: 16px;
width: 500px;
user-select: none;
z-index: 1111;
}
.theia-notifications-container.closed {
display: none;
}
.theia-notifications-container > * {
position: relative;
}
/* Toasts */
.theia-notifications-container.theia-notification-toasts .theia-notification-list-item-container {
border-radius: 4px;
margin-top: 6px;
}
.theia-notifications-container.theia-notification-toasts .theia-notification-list-item {
box-shadow: 0px 0px 4px 0px var(--theia-widget-shadow);
border: 1px solid var(--theia-notificationToast-border);
border-radius: 4px;
}
/* Center */
.theia-notifications-container.theia-notification-center {
background-color: var(--theia-notifications-background);
border: 1px solid var(--theia-notificationCenter-border);
border-radius: 4px;
overflow: hidden;
box-shadow: 0px 0px 6px 0px var(--theia-widget-shadow);
}
/* Center > Header */
.theia-notification-center-header {
color: var(--theia-notificationCenterHeader-foreground);
background-color: var(--theia-notificationCenterHeader-background);
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
min-height: 30px;
align-items: center;
}
.theia-notification-center-header-title {
font-size: calc(var(--theia-ui-font-size1) / 1.1);
font-family: var(--theia-ui-font-family);
margin: 8px;
flex-grow: 2;
}
.theia-notification-center-header-actions {
margin: 8px;
}
/* List */
.theia-notification-list-scroll-container {
max-height: 300px;
overflow: auto;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
.theia-notification-list {
display: flex;
flex-direction: column-reverse;
flex-wrap: nowrap;
}
/* List > Item */
.theia-notification-list-item {
background-color: var(--theia-notifications-background);
width: 100%;
cursor: pointer;
flex-grow: 1;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: space-between;
}
.theia-notification-list-item:focus {
border-color: var(--theia-focusBorder);
}
.theia-notification-center .theia-notification-list-item:not(:last-child) {
border-top: 1px var(--theia-notifications-border) solid;
}
.theia-notification-list-item-content {
margin: 6px;
flex-grow: 3;
}
.theia-notification-list-item-content.collapsed
.theia-notification-list-item-content-bottom {
display: none;
}
.theia-notification-list-item-content.collapsed .theia-notification-message {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.theia-notification-list-item-content-main {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
padding: 5px 4px 7px 4px;
}
.theia-notification-message {
font-size: var(--theia-ui-font-size1);
font-family: var(--theia-ui-font-family);
overflow-wrap: break-word;
box-sizing: border-box;
flex-basis: 0%;
flex-grow: 1;
flex-shrink: 1;
display: block;
overflow: hidden;
user-select: text;
margin-top: 3px;
}
.theia-notification-message a {
border: none;
color: var(--theia-notificationLink-foreground);
outline: 0;
text-decoration: none;
}
.theia-notification-message a:focus {
outline-color: var(--theia-focusBorder);
}
.theia-notification-icon {
margin-top: 2px;
margin-right: var(--theia-ui-padding);
}
.theia-notification-icon:before {
font-size: 18px;
}
.theia-notification-icon.info:before {
color: var(--theia-notificationsInfoIcon-foreground);
}
.theia-notification-icon.warning:before {
color: var(--theia-notificationsWarningIcon-foreground);
}
.theia-notification-icon.error:before {
color: var(--theia-notificationsErrorIcon-foreground);
}
.theia-notification-actions {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
margin: 0px;
padding: 0px;
}
.theia-notification-actions > li {
display: inline-block;
height: 16px;
width: 16px;
cursor: pointer;
margin-left: 4px;
}
.theia-notification-actions > .expand {
transform: rotate(180deg);
}
.theia-notification-list-item-content-bottom {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.theia-notification-source {
font-size: var(--theia-ui-font-size0);
font-family: var(--theia-ui-font-family);
overflow-wrap: break-word;
box-sizing: border-box;
flex-grow: 1;
padding: 4px;
display: block;
overflow: hidden;
}
.theia-notification-buttons {
flex-grow: 2;
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-wrap: wrap;
}
.theia-notification-buttons > button {
margin: 4px;
max-width: 160px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.theia-notification-item-progress {
display: block;
}
.theia-notification-item-progressbar {
height: 2px;
background-color: var(--theia-progressBar-background);
width: 66%;
}
.theia-notification-item-progressbar.indeterminate {
/* `progress-animation` is defined in `packages/core/src/browser/style/progress-bar.css` */
animation: progress-animation 1.3s 0s infinite
cubic-bezier(0.645, 0.045, 0.355, 1);
}
/* Perfect scrollbar */
.theia-notification-list-scroll-container .ps__rail-y {
width: var(--theia-scrollbar-rail-width);
}
.theia-notification-list-scroll-container .ps__rail-y:hover > .ps__thumb-y,
.theia-notification-list-scroll-container .ps__rail-y:focus > .ps__thumb-y,
.theia-notification-list-scroll-container
.ps__rail-y.ps--clicking
.ps__thumb-y {
right: calc(
(var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2
);
width: var(--theia-scrollbar-width);
}
.theia-notification-list-scroll-container .ps__rail-y > .ps__thumb-y {
width: var(--theia-scrollbar-width);
right: calc(
(var(--theia-scrollbar-rail-width) - var(--theia-scrollbar-width)) / 2
);
background: var(--theia-scrollbarSlider-background);
border-radius: 0px;
}

View File

@@ -0,0 +1,57 @@
// *****************************************************************************
// Copyright (C) 2018 Ericsson 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 { interfaces } from '@theia/core/shared/inversify';
import {
createPreferenceProxy,
PreferenceContribution,
PreferenceProxy,
PreferenceSchema,
PreferenceService,
} from '@theia/core/lib/common/preferences';
import { nls } from '@theia/core/lib/common/nls';
export const NotificationConfigSchema: PreferenceSchema = {
'properties': {
'notification.timeout': {
'type': 'number',
'description': nls.localize('theia/messages/notificationTimeout', 'Informative notifications will be hidden after this timeout.'),
'default': 30 * 1000 // `0` and negative values are treated as no timeout.
}
}
};
export interface NotificationConfiguration {
'notification.timeout': number
}
export const NotificationPreferenceContribution = Symbol('NotificationPreferenceContribution');
export const NotificationPreferences = Symbol('NotificationPreferences');
export type NotificationPreferences = PreferenceProxy<NotificationConfiguration>;
export function createNotificationPreferences(preferences: PreferenceService, schema: PreferenceSchema = NotificationConfigSchema): NotificationPreferences {
return createPreferenceProxy(preferences, schema);
}
export function bindNotificationPreferences(bind: interfaces.Bind): void {
bind(NotificationPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(NotificationPreferenceContribution);
return createNotificationPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(NotificationPreferenceContribution).toConstantValue({ schema: NotificationConfigSchema });
bind(PreferenceContribution).toService(NotificationPreferenceContribution);
}

View File

@@ -0,0 +1,22 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics 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 { ContainerModule } from '@theia/core/shared/inversify';
import { bindNotificationPreferences } from '../common/notification-preferences';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bindNotificationPreferences(bind);
});

View File

@@ -0,0 +1,16 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
}
]
}