Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions build/vite/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,43 @@ function injectBuiltinExtensionsPlugin(): Plugin {
function createHotClassSupport(): Plugin {
return {
name: 'createHotClassSupport',
transform(code, id) {
if (id.endsWith('.ts')) {
if (code.includes('createHotClass')) {
code = code + `\n
transform: {
order: 'pre',
handler: (code, id) => {
if (id.endsWith('.ts')) {
let needsHMRAccept = false;
const hasCreateHotClass = code.includes('createHotClass');
const hasDomWidget = code.includes('DomWidget');

if (!hasCreateHotClass && !hasDomWidget) {
return undefined;
}

if (hasCreateHotClass) {
needsHMRAccept = true;
}

if (hasDomWidget) {
const matches = code.matchAll(/class\s+([a-zA-Z0-9_]+)\s+extends\s+DomWidget/g);
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern /class\s+([a-zA-Z0-9_]+)\s+extends\s+DomWidget/g may not correctly handle all cases. It won't match classes that have TypeScript generic parameters (e.g., class MyWidget<T> extends DomWidget) or those with multiple spaces/line breaks. Consider using a more robust pattern or AST-based parsing for reliability.

Suggested change
const matches = code.matchAll(/class\s+([a-zA-Z0-9_]+)\s+extends\s+DomWidget/g);
const matches = code.matchAll(/class\s+([a-zA-Z0-9_]+)\s*(<[^>]*>)?\s*extends\s*DomWidget/smg);

Copilot uses AI. Check for mistakes.
/// @ts-ignore
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /// @ts-ignore comment should be replaced with // @ts-expect-error for better type checking. When using @ts-expect-error, TypeScript will error if the suppression becomes unnecessary, helping maintain code quality over time.

Suggested change
/// @ts-ignore
// @ts-expect-error

Copilot uses AI. Check for mistakes.
for (const match of matches) {
const className = match[1];
code = code + `\n${className}.registerWidgetHotReplacement(${JSON.stringify(id + '#' + className)});`;
needsHMRAccept = true;
}
}

if (needsHMRAccept) {
code = code + `\n
if (import.meta.hot) {
import.meta.hot.accept();
}`;
}
return code;
}
return code;
}
return undefined;
},
return undefined;
},
}
};
}

Expand Down
166 changes: 166 additions & 0 deletions src/vs/platform/ui/browser/domWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { isHotReloadEnabled } from '../../../base/common/hotReload.js';
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
import { ISettableObservable, IObservable, autorun, constObservable, derived, observableValue } from '../../../base/common/observable.js';
import { IInstantiationService, GetLeadingNonServiceArgs } from '../../instantiation/common/instantiation.js';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find platform/ui not very specific, lets try to find folder names in platform that are explanatory of what is going on inside. The original intent was to make it easy to understand what kind of service is provided.


/**
* The DomWidget class provides a standard to define reusable UI components.
* It is disposable and defines a single root element of type HTMLElement.
* It also provides static helper methods to create and append widgets to the DOM,
* with support for hot module replacement during development.
*/
export abstract class DomWidget extends Disposable {
/**
* Appends the widget to the provided DOM element.
*/
public static createAppend<TArgs extends unknown[], T extends DomWidget>(this: DomWidgetCtor<TArgs, T>, dom: HTMLElement, store: DisposableStore, ...params: TArgs): void {
if (!isHotReloadEnabled()) {
const widget = new this(...params);
dom.appendChild(widget.element);
store.add(widget);
return;
}

const observable = this.createObservable(store, ...params);
store.add(autorun((reader) => {
const widget = observable.read(reader);
dom.appendChild(widget.element);
reader.store.add(toDisposable(() => widget.element.remove()));
reader.store.add(widget);
}));
}

/**
* Creates the widget in a new div element with "display: contents".
*/
public static createInContents<TArgs extends unknown[], T extends DomWidget>(this: DomWidgetCtor<TArgs, T>, store: DisposableStore, ...params: TArgs): HTMLDivElement {
const div = document.createElement('div');
div.style.display = 'contents';
this.createAppend(div, store, ...params);
return div;
}

/**
* Creates an observable instance of the widget.
* The observable will change when hot module replacement occurs.
*/
public static createObservable<TArgs extends unknown[], T extends DomWidget>(this: DomWidgetCtor<TArgs, T>, store: DisposableStore, ...params: TArgs): IObservable<T> {
if (!isHotReloadEnabled()) {
return constObservable(new this(...params));
}

const id = (this as unknown as HotReloadable)[_hotReloadId];
const observable = id ? hotReloadedWidgets.get(id) : undefined;

if (!observable) {
return constObservable(new this(...params));
}

return derived(reader => {
const Ctor = observable.read(reader);
return new Ctor(...params) as T;
});
}

/**
* Appends the widget to the provided DOM element.
*/
public static instantiateAppend<TArgs extends unknown[], T extends DomWidget>(this: DomWidgetCtor<TArgs, T>, instantiationService: IInstantiationService, dom: HTMLElement, store: DisposableStore, ...params: GetLeadingNonServiceArgs<TArgs>): void {
if (!isHotReloadEnabled()) {
const widget = instantiationService.createInstance(this as unknown as new (...args: unknown[]) => T, ...params);
dom.appendChild(widget.element);
store.add(widget);
return;
}

const observable = this.instantiateObservable(instantiationService, store, ...params);
let lastWidget: DomWidget | undefined = undefined;
store.add(autorun((reader) => {
const widget = observable.read(reader);
if (lastWidget) {
lastWidget.element.replaceWith(widget.element);
} else {
dom.appendChild(widget.element);
}
lastWidget = widget;

reader.delayedStore.add(widget);
}));
}

/**
* Creates the widget in a new div element with "display: contents".
* If possible, prefer `instantiateAppend`, as it avoids an extra div in the DOM.
*/
public static instantiateInContents<TArgs extends unknown[], T extends DomWidget>(this: DomWidgetCtor<TArgs, T>, instantiationService: IInstantiationService, store: DisposableStore, ...params: GetLeadingNonServiceArgs<TArgs>): HTMLDivElement {
const div = document.createElement('div');
div.style.display = 'contents';
this.instantiateAppend(instantiationService, div, store, ...params);
return div;
}

/**
* Creates an observable instance of the widget.
* The observable will change when hot module replacement occurs.
*/
public static instantiateObservable<TArgs extends unknown[], T extends DomWidget>(this: DomWidgetCtor<TArgs, T>, instantiationService: IInstantiationService, store: DisposableStore, ...params: GetLeadingNonServiceArgs<TArgs>): IObservable<T> {
if (!isHotReloadEnabled()) {
return constObservable(instantiationService.createInstance(this as unknown as new (...args: unknown[]) => T, ...params));
}

const id = (this as unknown as HotReloadable)[_hotReloadId];
const observable = id ? hotReloadedWidgets.get(id) : undefined;

if (!observable) {
return constObservable(instantiationService.createInstance(this as unknown as new (...args: unknown[]) => T, ...params));
}

return derived(reader => {
const Ctor = observable.read(reader);
return instantiationService.createInstance(Ctor, ...params) as T;
});
}

/**
* @deprecated Do not call manually! Only for use by the hot reload system (a vite plugin will inject calls to this method in dev mode).
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static registerWidgetHotReplacement(this: new (...args: any[]) => DomWidget, id: string): void {
if (!isHotReloadEnabled()) {
return;
}
let observable = hotReloadedWidgets.get(id);
if (!observable) {
observable = observableValue(id, this);
hotReloadedWidgets.set(id, observable);
} else {
observable.set(this, undefined);
}
(this as unknown as HotReloadable)[_hotReloadId] = id;
}

/** Always returns the same element. */
abstract get element(): HTMLElement;
}

const _hotReloadId = Symbol('DomWidgetHotReloadId');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hotReloadedWidgets = new Map<string, ISettableObservable<new (...args: any[]) => DomWidget>>();

interface HotReloadable {
[_hotReloadId]?: string;
}

type DomWidgetCtor<TArgs extends unknown[], T extends DomWidget> = {
new(...args: TArgs): T;

createObservable(store: DisposableStore, ...params: TArgs): IObservable<T>;
instantiateObservable(instantiationService: IInstantiationService, store: DisposableStore, ...params: GetLeadingNonServiceArgs<TArgs>): IObservable<T>;
createAppend(dom: HTMLElement, store: DisposableStore, ...params: TArgs): void;
instantiateAppend(instantiationService: IInstantiationService, dom: HTMLElement, store: DisposableStore, ...params: GetLeadingNonServiceArgs<TArgs>): void;
};
Loading
Loading