diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index 182dda59e5..8b180ac759 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -5,7 +5,7 @@ import { IColorContrastCache } from 'browser/Types'; import { DIM_OPACITY, TEXT_BASELINE } from './Constants'; -import { tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer'; +import { loadCustomGlyphDefinitions, tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer'; import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from './Types'; import { NULL_COLOR, channels, color, rgba } from 'common/Color'; @@ -100,6 +100,10 @@ export class TextureAtlas implements ITextureAtlas { alpha: this._config.allowTransparency, willReadFrequently: true })); + // Start loading custom glyph definitions asynchronously if custom glyphs are enabled + if (this._config.customGlyphs !== false) { + loadCustomGlyphDefinitions(); + } } public dispose(): void { diff --git a/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts b/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts index 6302757fc1..eb73117e51 100644 --- a/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts +++ b/addons/addon-webgl/src/customGlyphs/CustomGlyphRasterizer.ts @@ -4,8 +4,38 @@ */ import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; -import { customGlyphDefinitions } from './CustomGlyphDefinitions'; -import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types'; +import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphCharacterDefinition, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types'; + +/** + * Cached custom glyph definitions. This is loaded dynamically to support code splitting. + */ +let customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefinition | undefined } | undefined; + +/** + * Promise that resolves when custom glyph definitions are loaded. + */ +let loadingPromise: Promise | undefined; + +/** + * Begins loading the custom glyph definitions. This should be called as early as possible + * to ensure the definitions are ready when needed. + * @returns A promise that resolves when the definitions are loaded. + */ +export function loadCustomGlyphDefinitions(): Promise { + if (customGlyphDefinitions) { + return Promise.resolve(); + } + if (!loadingPromise) { + loadingPromise = import('./CustomGlyphDefinitions').then(module => { + customGlyphDefinitions = module.customGlyphDefinitions; + }).catch(err => { + // Reset the loading promise on failure to allow retry + loadingPromise = undefined; + throw err; + }); + } + return loadingPromise; +} /** * Try drawing a custom block element or box drawing character, returning whether it was @@ -24,6 +54,11 @@ export function tryDrawCustomGlyph( devicePixelRatio: number, backgroundColor?: string ): boolean { + // If definitions are not loaded yet, return false to fall back to font rendering + if (!customGlyphDefinitions) { + return false; + } + const unifiedCharDefinition = customGlyphDefinitions[c]; if (unifiedCharDefinition) { // Normalize to array for uniform handling diff --git a/bin/esbuild.mjs b/bin/esbuild.mjs index 5e0a370cad..b84cfd1cdc 100644 --- a/bin/esbuild.mjs +++ b/bin/esbuild.mjs @@ -26,7 +26,10 @@ const commonOptions = { target: 'es2021', sourcemap: true, treeShaking: true, + splitting: true, + chunkNames: 'chunks/[name]-[hash]', logLevel: 'warning', + outExtension: { '.js': '.mjs' }, }; /** @type {esbuild.BuildOptions} */ @@ -103,7 +106,8 @@ if (config.addon) { bundleConfig = { ...bundleConfig, entryPoints: [`addons/addon-${config.addon}/src/${getAddonEntryPoint(config.addon)}.ts`], - outfile: `addons/addon-${config.addon}/lib/addon-${config.addon}.mjs`, + outdir: `addons/addon-${config.addon}/lib`, + entryNames: `addon-${config.addon}`, }; outConfig = { ...outConfig, @@ -128,7 +132,8 @@ if (config.addon) { bundleConfig = { ...bundleConfig, entryPoints: [`demo/client/client.ts`], - outfile: 'demo/dist/client-bundle.js', + outdir: 'demo/dist', + entryNames: 'client-bundle', external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'], alias: { // Library ESM imports @@ -164,6 +169,10 @@ if (config.addon) { format: 'cjs', platform: 'node', external: ['node-pty'], + // Splitting is not compatible with cjs format + splitting: false, + // Override the .mjs extension from commonOptions + outExtension: {}, } skipOut = true; skipOutTest = true; @@ -171,7 +180,8 @@ if (config.addon) { bundleConfig = { ...bundleConfig, entryPoints: [`src/headless/public/Terminal.ts`], - outfile: `headless/lib-headless/xterm-headless.mjs` + outdir: `headless/lib-headless`, + entryNames: 'xterm-headless', }; outConfig = { ...outConfig, @@ -183,7 +193,8 @@ if (config.addon) { bundleConfig = { ...bundleConfig, entryPoints: [`src/browser/public/Terminal.ts`], - outfile: `lib/xterm.mjs` + outdir: `lib`, + entryNames: 'xterm', }; outConfig = { ...outConfig, diff --git a/demo/index.html b/demo/index.html index 9c8eec666a..56a6c2a497 100644 --- a/demo/index.html +++ b/demo/index.html @@ -23,6 +23,6 @@ - + diff --git a/demo/test.html b/demo/test.html index d0e1e1f18f..3532aed08c 100644 --- a/demo/test.html +++ b/demo/test.html @@ -18,6 +18,6 @@
- + diff --git a/src/browser/CoreBrowserTerminal.ts b/src/browser/CoreBrowserTerminal.ts index c26e670515..e5515e90d4 100644 --- a/src/browser/CoreBrowserTerminal.ts +++ b/src/browser/CoreBrowserTerminal.ts @@ -28,7 +28,7 @@ import { OscLinkProvider } from 'browser/OscLinkProvider'; import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal } from 'browser/Types'; import { Viewport } from 'browser/Viewport'; import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer'; -import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer'; +import type { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; import { IRenderer } from 'browser/renderer/shared/Types'; @@ -53,12 +53,34 @@ import { toRgbString } from 'common/input/XParseColor'; import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; import { WindowsOptionsReportType } from '../common/InputHandler'; -import { AccessibilityManager } from './AccessibilityManager'; +import type { AccessibilityManager } from './AccessibilityManager'; import { Linkifier } from './Linkifier'; import { Emitter, Event } from 'vs/base/common/event'; import { addDisposableListener } from 'vs/base/browser/dom'; import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +/** + * Lazily loaded AccessibilityManager module. + */ +let accessibilityManagerModule: Promise | undefined; +function loadAccessibilityManager(): Promise { + if (!accessibilityManagerModule) { + accessibilityManagerModule = import('./AccessibilityManager'); + } + return accessibilityManagerModule; +} + +/** + * Lazily loaded OverviewRulerRenderer module. + */ +let overviewRulerRendererModule: Promise | undefined; +function loadOverviewRulerRenderer(): Promise { + if (!overviewRulerRendererModule) { + overviewRulerRendererModule = import('browser/decorations/OverviewRulerRenderer'); + } + return overviewRulerRendererModule; +} + export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { public textarea: HTMLTextAreaElement | undefined; public element: HTMLElement | undefined; @@ -277,7 +299,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { private _handleScreenReaderModeOptionChange(value: boolean): void { if (value) { if (!this._accessibilityManager.value && this._renderService) { - this._accessibilityManager.value = this._instantiationService.createInstance(AccessibilityManager, this); + loadAccessibilityManager().then(module => { + // Check again after async load in case option changed + if (this.options.screenReaderMode && !this._accessibilityManager.value && this._renderService) { + this._accessibilityManager.value = this._instantiationService.createInstance(module.AccessibilityManager, this); + } + }).catch(() => { + // Failed to load accessibility manager module + }); } } else { this._accessibilityManager.clear(); @@ -588,16 +617,34 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal { if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to // ensure the correct order of the dprchange event - this._accessibilityManager.value = this._instantiationService.createInstance(AccessibilityManager, this); + loadAccessibilityManager().then(module => { + if (this.options.screenReaderMode && !this._accessibilityManager.value && this._renderService) { + this._accessibilityManager.value = this._instantiationService.createInstance(module.AccessibilityManager, this); + } + }).catch(() => { + // Failed to load accessibility manager module + }); } this._register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e))); if (this.options.overviewRuler.width) { - this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement)); + loadOverviewRulerRenderer().then(module => { + if (this.options.overviewRuler.width && !this._overviewRulerRenderer && this._viewportElement && this.screenElement) { + this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(module.OverviewRulerRenderer, this._viewportElement, this.screenElement)); + } + }).catch(() => { + // Failed to load overview ruler renderer module + }); } this.optionsService.onSpecificOptionChange('overviewRuler', value => { if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) { - this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement)); + loadOverviewRulerRenderer().then(module => { + if (!this._overviewRulerRenderer && this.options.overviewRuler.width && this._viewportElement && this.screenElement) { + this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(module.OverviewRulerRenderer, this._viewportElement, this.screenElement)); + } + }).catch(() => { + // Failed to load overview ruler renderer module + }); } }); // Measure the character size