diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 090b8aaa83968..41b7cb1783354 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -21,7 +21,7 @@ import { EditorInput } from '../../../common/editor/editorInput.js'; import { IEditableData } from '../../../common/views.js'; import { ITerminalStatusList } from './terminalStatusList.js'; import { XtermTerminal } from './xterm/xtermTerminal.js'; -import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfiguration, ITerminalFont, ITerminalProcessExtHostProxy, ITerminalProcessInfo } from '../common/terminal.js'; +import { IRegisterContributedProfileArgs, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalFont, ITerminalProcessExtHostProxy, ITerminalProcessInfo } from '../common/terminal.js'; import type { IMarker, ITheme, Terminal as RawXtermTerminal, IBufferRange, IMarker as IXtermMarker } from '@xterm/xterm'; import { ScrollPosition } from './xterm/markNavigationAddon.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; @@ -33,6 +33,7 @@ import type { IMenu } from '../../../../platform/actions/common/actions.js'; import type { IProgressState } from '@xterm/addon-progress'; import type { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import type { TerminalEditorInput } from './terminalEditorInput.js'; +import type { ITerminalConfiguration2 } from '../common/terminalConfiguration.js'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalConfigurationService = createDecorator('terminalConfigurationService'); @@ -462,7 +463,7 @@ export interface ITerminalConfigurationService { /** * A typed and partially validated representation of the terminal configuration. */ - readonly config: Readonly; + readonly config: ITerminalConfiguration2; /** * The default location for terminals. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts index d39f1a91159a8..43f3dc05a2769 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts @@ -10,9 +10,10 @@ import { EDITOR_FONT_DEFAULTS } from '../../../../editor/common/config/fontInfo. import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ITerminalConfigurationService, LinuxDistro } from './terminal.js'; import type { IXtermCore } from './xterm-private.js'; -import { DEFAULT_BOLD_FONT_WEIGHT, DEFAULT_FONT_WEIGHT, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, FontWeight, ITerminalConfiguration, MAXIMUM_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MINIMUM_LETTER_SPACING, TERMINAL_CONFIG_SECTION, type ITerminalFont } from '../common/terminal.js'; +import { DEFAULT_BOLD_FONT_WEIGHT, DEFAULT_FONT_WEIGHT, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, FontWeight, MAXIMUM_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MINIMUM_LETTER_SPACING, TERMINAL_CONFIG_SECTION, type ITerminalFont } from '../common/terminal.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { TerminalLocation, TerminalLocationConfigValue } from '../../../../platform/terminal/common/terminal.js'; +import type { ITerminalConfiguration2 } from '../common/terminalConfiguration.js'; // #region TerminalConfigurationService @@ -21,7 +22,7 @@ export class TerminalConfigurationService extends Disposable implements ITermina protected _fontMetrics: TerminalFontMetrics; - protected _config!: Readonly; + protected _config!: ITerminalConfiguration2; get config() { return this._config; } get defaultLocation(): TerminalLocation { @@ -53,7 +54,7 @@ export class TerminalConfigurationService extends Disposable implements ITermina getFont(w: Window, xtermCore?: IXtermCore, excludeDimensions?: boolean): ITerminalFont { return this._fontMetrics.getFont(w, xtermCore, excludeDimensions); } private _updateConfig(): void { - const configValues = { ...this._configurationService.getValue(TERMINAL_CONFIG_SECTION) }; + const configValues = { ...this._configurationService.getValue(TERMINAL_CONFIG_SECTION) }; configValues.fontWeight = this._normalizeFontWeight(configValues.fontWeight, DEFAULT_FONT_WEIGHT); configValues.fontWeightBold = this._normalizeFontWeight(configValues.fontWeightBold, DEFAULT_BOLD_FONT_WEIGHT); this._config = configValues; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 254432f6794a9..be85217e5ba99 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1875,7 +1875,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.xterm!.raw.options.screenReaderMode = this._accessibilityService.isScreenReaderOptimized(); } - private _setCommandsToSkipShell(commands: string[]): void { + private _setCommandsToSkipShell(commands: readonly string[]): void { const excludeCommands = commands.filter(command => command[0] === '-').map(command => command.slice(1)); this._skipTerminalCommands = DEFAULT_COMMANDS_TO_SKIP_SHELL.filter(defaultCommand => { return !excludeCommands.includes(defaultCommand); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 7655503b1cee0..73bd2b601c22a 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -772,7 +772,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach } this._ligaturesAddon.value = this._instantiationService.createInstance(LigaturesAddon, { fontFeatureSettings: ligaturesConfig.featureSettings, - fallbackLigatures: ligaturesConfig.fallbackLigatures, + fallbackLigatures: Array.from(ligaturesConfig.fallbackLigatures), }); this.raw.loadAddon(this._ligaturesAddon.value); shouldRecreateWebglRenderer = true; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 8a86ef4b30cf6..0147f5130fb4f 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -39,7 +39,7 @@ export const MINIMUM_FONT_WEIGHT = 1; export const MAXIMUM_FONT_WEIGHT = 1000; export const DEFAULT_FONT_WEIGHT = 'normal'; export const DEFAULT_BOLD_FONT_WEIGHT = 'bold'; -export const SUGGESTIONS_FONT_WEIGHT = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; +export const SUGGESTIONS_FONT_WEIGHT: FontWeight[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; export const ITerminalProfileResolverService = createDecorator('terminalProfileResolverService'); export interface ITerminalProfileResolverService { @@ -96,7 +96,7 @@ export interface IShellLaunchConfigResolveOptions { allowAutomationShell?: boolean; } -export type FontWeight = 'normal' | 'bold' | number; +export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; export interface ITerminalProfiles { linux: { [key: string]: ITerminalProfileObject }; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index bd3ef396fc19b..d240079ab361c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -5,17 +5,17 @@ import { Codicon } from '../../../../base/common/codicons.js'; import type { IStringDictionary } from '../../../../base/common/collections.js'; -import { IJSONSchemaSnippet } from '../../../../base/common/jsonSchema.js'; +import { IJSONSchemaSnippet, type TypeFromJsonSchema } from '../../../../base/common/jsonSchema.js'; import { isMacintosh, isWindows } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationRegistry, type IConfigurationPropertySchema } from '../../../../platform/configuration/common/configurationRegistry.js'; import product from '../../../../platform/product/common/product.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; -import { TerminalLocationConfigValue, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; +import { TerminalLocationConfigValue, TerminalSettingId, type ITerminalProfileObject } from '../../../../platform/terminal/common/terminal.js'; import { terminalColorSchema, terminalIconSchema } from '../../../../platform/terminal/common/terminalPlatformConfiguration.js'; import { ConfigurationKeyValuePairs, IConfigurationMigrationRegistry, Extensions as WorkbenchExtensions } from '../../../common/configuration.js'; import { terminalContribConfiguration, TerminalContribSettingId } from '../terminalContribExports.js'; -import { DEFAULT_COMMANDS_TO_SKIP_SHELL, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MAXIMUM_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, SUGGESTIONS_FONT_WEIGHT } from './terminal.js'; +import { DEFAULT_COMMANDS_TO_SKIP_SHELL, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MAXIMUM_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, SUGGESTIONS_FONT_WEIGHT, type FontWeight, type ITerminalProfiles } from './terminal.js'; const terminalDescriptors = '\n- ' + [ '`\${cwd}`: ' + localize("cwd", "the terminal's current working directory."), @@ -41,7 +41,7 @@ terminalDescription += terminalDescriptors; export const defaultTerminalFontSize = isMacintosh ? 12 : 14; -const terminalConfiguration: IStringDictionary = { +const terminalConfigurationConst = { [TerminalSettingId.SendKeybindingsToShell]: { markdownDescription: localize('terminal.integrated.sendKeybindingsToShell', "Dispatches most keybindings to the terminal instead of the workbench, overriding {0}, which can be used alternatively for fine tuning.", '`#terminal.integrated.commandsToSkipShell#`'), type: 'boolean', @@ -190,7 +190,7 @@ const terminalConfiguration: IStringDictionary = { [TerminalSettingId.FontLigaturesFallbackLigatures]: { markdownDescription: localize('terminal.integrated.fontLigatures.fallbackLigatures', "When {0} is enabled and the particular {1} cannot be parsed, this is the set of character sequences that will always be drawn together. This allows the use of a fixed set of ligatures even when the font isn't supported.", `\`#${TerminalSettingId.GpuAcceleration}#\``, `\`#${TerminalSettingId.FontFamily}#\``), type: 'array', - items: [{ type: 'string' }], + items: { type: 'string' }, default: [ '<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->', '<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=', @@ -254,11 +254,9 @@ const terminalConfiguration: IStringDictionary = { }, { type: 'string', - pattern: '^(normal|bold|1000|[1-9][0-9]{0,2})$' - }, - { + pattern: '^(normal|bold|1000|[1-9][0-9]{0,2})$', enum: SUGGESTIONS_FONT_WEIGHT, - } + }, ], description: localize('terminal.integrated.fontWeight', "The font weight to use within the terminal for non-bold text. Accepts \"normal\" and \"bold\" keywords or numbers between 1 and 1000."), default: 'normal' @@ -273,11 +271,9 @@ const terminalConfiguration: IStringDictionary = { }, { type: 'string', - pattern: '^(normal|bold|1000|[1-9][0-9]{0,2})$' - }, - { + pattern: '^(normal|bold|1000|[1-9][0-9]{0,2})$', enum: SUGGESTIONS_FONT_WEIGHT, - } + }, ], description: localize('terminal.integrated.fontWeightBold', "The font weight to use within the terminal for bold text. Accepts \"normal\" and \"bold\" keywords or numbers between 1 and 1000."), default: 'bold' @@ -416,9 +412,7 @@ const terminalConfiguration: IStringDictionary = { ), type: 'array', - items: { - type: 'string' - }, + items: { type: 'string' }, default: [] }, [TerminalSettingId.AllowChords]: { @@ -510,9 +504,7 @@ const terminalConfiguration: IStringDictionary = { [TerminalSettingId.AllowedLinkSchemes]: { description: localize('terminal.integrated.allowedLinkSchemes', "An array of strings containing the URI schemes that the terminal is allowed to open links for. By default, only a small subset of possible schemes are allowed for security reasons."), type: 'array', - items: { - type: 'string' - }, + items: { type: 'string' }, default: [ 'file', 'http', @@ -660,8 +652,93 @@ const terminalConfiguration: IStringDictionary = { tags: ['advanced'] }, ...terminalContribConfiguration, +} as const satisfies IStringDictionary; + +const terminalConfiguration = terminalConfigurationConst as IStringDictionary; + +// Helper type to make string arrays readonly +type MakeArraysReadonly = T extends string[] ? readonly string[] : T; + +// Helpers to convert dot notation keys to nested objects +type SplitKey = K extends `${infer First}.${infer Rest}` ? [First, Rest] : [K, never]; + +// Get all first parts of dot notation keys +type FirstParts = { + [K in keyof T]: SplitKey[0] +}[keyof T]; + +// Get all keys that start with a specific prefix +type KeysStartingWith = { + [K in keyof T]: K extends `${Prefix}.${string}` ? K : never +}[keyof T]; + +// Get all keys that exactly match (no dots) +type ExactKeys = { + [K in keyof T]: K extends `${string}.${string}` ? never : K +}[keyof T]; + +// Build nested object by grouping keys +type NestedObject = { + // Exact matches (no dots) + readonly [K in ExactKeys]: MakeArraysReadonly +} & { + // Nested objects + readonly [P in FirstParts as P extends ExactKeys ? never : P]: NestedObject<{ + [K in KeysStartingWith as K extends `${P}.${infer Rest}` ? Rest : never]: T[K] + }> }; +// Assemble config interface with flat keys, excluding terminal.integrated. +type FlatTerminalConfig = ( + Omit<{ + [K in keyof typeof terminalConfigurationConst as K extends `terminal.integrated.${infer Rest}` ? Rest : K]: TypeFromJsonSchema + }, ( + // Omit keys that resolve to never + 'env.linux' | + 'env.osx' | + 'env.windows' | + 'tabs.defaultColor' | + 'tabs.defaultIcon' | + // Omit keys that don't pull correct type + 'fontWeight' | + 'fontWeightBold' + ) + > + & + { + // Manually set keys that don't resolve correctly + 'env.linux': { [key: string]: string | null }; + 'env.osx': { [key: string]: string | null }; + 'env.windows': { [key: string]: string | null }; + 'tabs.defaultColor': null | 'terminal.ansiBlack' | 'terminal.ansiRed' | 'terminal.ansiGreen' | 'terminal.ansiYellow' | 'terminal.ansiBlue' | 'terminal.ansiMagenta' | 'terminal.ansiCyan' | 'terminal.ansiWhite'; + 'tabs.defaultIcon': string; + 'fontWeight': FontWeight | number; + 'fontWeightBold': FontWeight | number; + // Manually set keys that come from platform configuration + // TODO: Derive these from JSON + // TODO: Not all platform configs are included here + automationShell: { + linux: string | null; + osx: string | null; + windows: string | null; + }; + profiles: { + linux: { [key: string]: ITerminalProfileObject }; + osx: { [key: string]: ITerminalProfileObject }; + windows: { [key: string]: ITerminalProfileObject }; + }; + defaultProfile: { + linux: string | null; + osx: string | null; + windows: string | null; + }; + useWslProfiles: boolean; + } +); + +// Map flat keys to nested object +export type ITerminalConfiguration2 = NestedObject; + export async function registerTerminalConfiguration(getFontSnippets: () => Promise) { const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index bcf4471762a4b..85b4df4232a42 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -138,7 +138,7 @@ import { TerminalConfigurationService } from '../../contrib/terminal/browser/ter import { TerminalEditorInput } from '../../contrib/terminal/browser/terminalEditorInput.js'; import { IEnvironmentVariableService } from '../../contrib/terminal/common/environmentVariable.js'; import { EnvironmentVariableService } from '../../contrib/terminal/common/environmentVariableService.js'; -import { IRegisterContributedProfileArgs, IShellLaunchConfigResolveOptions, ITerminalProfileProvider, ITerminalProfileResolverService, ITerminalProfileService, type ITerminalConfiguration } from '../../contrib/terminal/common/terminal.js'; +import { IRegisterContributedProfileArgs, IShellLaunchConfigResolveOptions, ITerminalProfileProvider, ITerminalProfileResolverService, ITerminalProfileService } from '../../contrib/terminal/common/terminal.js'; import { IChatEntitlementService } from '../../services/chat/common/chatEntitlementService.js'; import { IDecoration, IDecorationData, IDecorationsProvider, IDecorationsService, IResourceDecorationChangeEvent } from '../../services/decorations/common/decorations.js'; import { CodeEditorService } from '../../services/editor/browser/codeEditorService.js'; @@ -186,6 +186,7 @@ import { IWorkingCopyEditorService, WorkingCopyEditorService } from '../../servi import { IWorkingCopyFileService, WorkingCopyFileService } from '../../services/workingCopy/common/workingCopyFileService.js'; import { IWorkingCopyService, WorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js'; import { TestChatEntitlementService, TestContextService, TestExtensionService, TestFileService, TestHistoryService, TestLoggerService, TestMarkerService, TestProductService, TestStorageService, TestTextResourcePropertiesService, TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from '../common/workbenchTestServices.js'; +import type { ITerminalConfiguration2 } from '../../contrib/terminal/common/terminalConfiguration.js'; // Backcompat export export { TestFileService }; @@ -1966,7 +1967,7 @@ export class TestTerminalProfileResolverService implements ITerminalProfileResol export class TestTerminalConfigurationService extends TerminalConfigurationService { get fontMetrics() { return this._fontMetrics; } // eslint-disable-next-line local/code-no-any-casts - setConfig(config: Partial) { this._config = config as any; } + setConfig(config: Partial) { this._config = config as any; } } export class TestQuickInputService implements IQuickInputService {