Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**********************************************************************
* Copyright (c) 2026 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
***********************************************************************/
/* eslint-disable header/header */

import { Schemas } from '../../../../base/common/network.js';
import { Mutable } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { IEnvironmentService } from '../../../environment/common/environment.js';
import { IFileService } from '../../../files/common/files.js';
import { ILogService } from '../../../log/common/log.js';
import { IUriIdentityService } from '../../../uriIdentity/common/uriIdentity.js';
import { IUserDataProfile, IUserDataProfilesService, UserDataProfilesObject } from '../../common/userDataProfile.js';
import { BrowserUserDataProfilesService } from '../userDataProfile.js';
import { IBrowserWorkbenchEnvironmentService } from '../../../../workbench/services/environment/browser/environmentService.js';

/**
* Extends BrowserUserDataProfilesService to redirect keybindingsResource
* from IndexedDB (vscode-userdata scheme) to the server filesystem
* (vscodeRemote scheme) when cheKeybindingsPath is configured.
*/
export class CheUserDataProfilesService extends BrowserUserDataProfilesService implements IUserDataProfilesService {

constructor(
@IEnvironmentService environmentService: IEnvironmentService,
@IFileService fileService: IFileService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@ILogService logService: ILogService,
) {
super(environmentService, fileService, uriIdentityService, logService);
}

protected override get profilesObject(): UserDataProfilesObject {
const result = super.profilesObject;
const cheKeybindingsPath = (this.environmentService as IBrowserWorkbenchEnvironmentService).options?.cheKeybindingsPath;
if (cheKeybindingsPath && result.profiles.length > 0 && result.profiles[0].isDefault) {
(result.profiles[0] as Mutable<IUserDataProfile>).keybindingsResource = URI.file(cheKeybindingsPath).with({ scheme: Schemas.vscodeRemote });
}
return result;
}

}
17 changes: 16 additions & 1 deletion code/src/vs/server/node/che/webClientServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**********************************************************************
* Copyright (c) 2021-2022 Red Hat, Inc.
* Copyright (c) 2021-2026 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -9,9 +9,24 @@
***********************************************************************/
/* eslint-disable header/header */

import { existsSync } from 'fs';
import * as http from 'http';
import { join } from '../../../base/common/path.js';
import * as url from 'url';

const CHE_CONFIG_KEYBINDINGS_PATH = '/checode-config/keybindings.json';

/**
* Returns the server filesystem path for keybindings.json if the admin
* provided keybindings via ConfigMap. Otherwise returns undefined.
*/
export function getCheKeybindingsPath(userDataPath: string): string | undefined {
if (existsSync(CHE_CONFIG_KEYBINDINGS_PATH)) {
return join(userDataPath, 'User', 'keybindings.json');
}
return undefined;
}

export function getCheRedirectLocation(req: http.IncomingMessage, newQuery: any): string {
let newLocation;
// Grab headers
Expand Down
5 changes: 3 additions & 2 deletions code/src/vs/server/node/webClientServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { createReadStream, promises } from 'fs';
import * as http from 'http';
import { getCheRedirectLocation } from './che/webClientServer.js';
import { getCheKeybindingsPath, getCheRedirectLocation } from './che/webClientServer.js';
import * as url from 'url';
import * as cookie from 'cookie';
import * as crypto from 'crypto';
Expand Down Expand Up @@ -387,7 +387,8 @@ export class WebClientServer {
folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']),
workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']),
productConfiguration,
callbackRoute: callbackRoute
callbackRoute: callbackRoute,
cheKeybindingsPath: getCheKeybindingsPath(this._environmentService.userDataPath),
};

const cookies = cookie.parse(req.headers.cookie || '');
Expand Down
7 changes: 7 additions & 0 deletions code/src/vs/workbench/browser/web.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,13 @@ export interface IWorkbenchConstructionOptions {

//#endregion

/**
* Full path to keybindings.json on the server filesystem.
* When set, keybindings are read from and written to the server
* filesystem (via vscodeRemote scheme) instead of browser IndexedDB.
*/
readonly cheKeybindingsPath?: string;

//#region Profile options

/**
Expand Down
3 changes: 2 additions & 1 deletion code/src/vs/workbench/browser/web.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { ILabelService } from '../../platform/label/common/label.js';
import { UserDataProfileService } from '../services/userDataProfile/common/userDataProfileService.js';
import { IUserDataProfileService } from '../services/userDataProfile/common/userDataProfile.js';
import { BrowserUserDataProfilesService } from '../../platform/userDataProfile/browser/userDataProfile.js';
import { CheUserDataProfilesService } from '../../platform/userDataProfile/browser/che/userDataProfile.js';
import { DeferredPromise, timeout } from '../../base/common/async.js';
import { windowLogGroup, windowLogId } from '../services/log/common/logConstants.js';
import { LogService } from '../../platform/log/common/logService.js';
Expand Down Expand Up @@ -344,7 +345,7 @@ export class BrowserMain extends Disposable {
serviceCollection.set(IUriIdentityService, uriIdentityService);

// User Data Profiles
const userDataProfilesService = new BrowserUserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
const userDataProfilesService = new CheUserDataProfilesService(environmentService, fileService, uriIdentityService, logService);
serviceCollection.set(IUserDataProfilesService, userDataProfilesService);

const currentProfile = await this.getCurrentProfile(workspace, userDataProfilesService, environmentService);
Expand Down
47 changes: 46 additions & 1 deletion launcher/src/editor-configurations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**********************************************************************
* Copyright (c) 2024-2025 Red Hat, Inc.
* Copyright (c) 2024-2026 Red Hat, Inc.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
Expand All @@ -15,11 +15,13 @@ import { mergeFirstWithSecond, parseJSON } from './json-utils.js';

const CONFIGMAP_NAME = 'vscode-editor-configurations';
const REMOTE_SETTINGS_PATH = '/checode/remote/data/Machine/settings.json';
const USER_KEYBINDINGS_PATH = '/checode/remote/data/User/keybindings.json';

const enum EditorConfigs {
Settings = 'settings.json',
Extensions = 'extensions.json',
Product = 'product.json',
Keybindings = 'keybindings.json',
}

/**
Expand All @@ -46,6 +48,7 @@ export class EditorConfigurations {
}

await this.configureSettings(configmap);
await this.configureKeybindings(configmap);
await this.configureExtensions(configmap);
await this.configureProductJSON(configmap);
} catch (error) {
Expand Down Expand Up @@ -88,6 +91,48 @@ export class EditorConfigurations {
}
}

private async configureKeybindings(configmap: k8s.V1ConfigMap): Promise<void> {
const configmapContent = configmap.data![EditorConfigs.Keybindings];
if (!configmapContent) {
return;
}

console.log(' > Configure editor keybindings...');

try {
const keybindingsFromConfigmap = parseJSON(configmapContent, {
errorMessage: 'Configmap keybindings.json content is not valid.',
});

if (!Array.isArray(keybindingsFromConfigmap)) {
console.log(' > keybindings.json must be a JSON array. Skip this step.');
return;
}

let existingKeybindings: unknown[] = [];
if (await fs.fileExists(USER_KEYBINDINGS_PATH)) {
console.log(` > Found existing keybindings file: ${USER_KEYBINDINGS_PATH}`);
const existingContent = await fs.readFile(USER_KEYBINDINGS_PATH);
const parsed = parseJSON(existingContent, {
errorMessage: 'Existing keybindings.json file is not valid.',
});
if (Array.isArray(parsed)) {
existingKeybindings = parsed;
}
} else {
console.log(` > Creating keybindings file: ${USER_KEYBINDINGS_PATH}`);
}

const mergedKeybindings = [...existingKeybindings, ...keybindingsFromConfigmap];
const json = JSON.stringify(mergedKeybindings, null, '\t');
await fs.writeFile(USER_KEYBINDINGS_PATH, json);

console.log(' > Editor keybindings have been configured.');
} catch (error) {
console.log('Failed to configure editor keybindings.', error);
}
}

private async configureExtensions(configmap: k8s.V1ConfigMap): Promise<void> {
const configmapContent = configmap.data![EditorConfigs.Extensions];
if (!configmapContent) {
Expand Down
44 changes: 44 additions & 0 deletions launcher/tests/editor-configurations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EditorConfigurations } from '../src/editor-configurations';

const DEVWORKSPACE_NAMESPACE = 'test-namespace';
const REMOTE_SETTINGS_PATH = '/checode/remote/data/Machine/settings.json';
const USER_KEYBINDINGS_PATH = '/checode/remote/data/User/keybindings.json';
const WORKSPACE_FILE_PATH = '/projects/.code-workspace';
const WORKSPACE_FILE_CONTENT =
'{\n' +
Expand Down Expand Up @@ -395,4 +396,47 @@ describe('Test applying editor configurations:', () => {
JSON.stringify(JSON.parse(mergedProductJSON), null, '\t')
);
});

it('should apply keybindings from a configmap when no existing keybindings file', async () => {
env.DEVWORKSPACE_NAMESPACE = DEVWORKSPACE_NAMESPACE;
const keybindingsContent = '[{"key": "ctrl+k", "command": "workbench.action.terminal.kill"}]';
const mockResponse = { data: { 'keybindings.json': keybindingsContent } } as V1ConfigMap;
mockCoreV1Api.readNamespacedConfigMap.mockResolvedValue(mockResponse);
fileExistsMock.mockResolvedValue(false);

await new EditorConfigurations().configure();

expect(writeFileMock).toBeCalledTimes(1);
expect(writeFileMock).toBeCalledWith(
USER_KEYBINDINGS_PATH,
JSON.stringify(JSON.parse(keybindingsContent), null, '\t')
);
});

it('should merge keybindings from configmap with existing keybindings', async () => {
env.DEVWORKSPACE_NAMESPACE = DEVWORKSPACE_NAMESPACE;
const existingKeybindings = '[{"key": "ctrl+j", "command": "workbench.action.togglePanel"}]';
const configmapKeybindings = '[{"key": "ctrl+k", "command": "workbench.action.terminal.kill"}]';
const mockResponse = { data: { 'keybindings.json': configmapKeybindings } } as V1ConfigMap;
mockCoreV1Api.readNamespacedConfigMap.mockResolvedValue(mockResponse);
fileExistsMock.mockResolvedValue(true);
readFileMock.mockResolvedValue(existingKeybindings);

await new EditorConfigurations().configure();

const merged = [...JSON.parse(existingKeybindings), ...JSON.parse(configmapKeybindings)];
expect(writeFileMock).toBeCalledTimes(1);
expect(writeFileMock).toBeCalledWith(USER_KEYBINDINGS_PATH, JSON.stringify(merged, null, '\t'));
});

it('should skip keybindings when configmap content is not a JSON array', async () => {
env.DEVWORKSPACE_NAMESPACE = DEVWORKSPACE_NAMESPACE;
const invalidKeybindings = '{"key": "ctrl+k", "command": "test"}';
const mockResponse = { data: { 'keybindings.json': invalidKeybindings } } as V1ConfigMap;
mockCoreV1Api.readNamespacedConfigMap.mockResolvedValue(mockResponse);

await new EditorConfigurations().configure();

expect(writeFileMock).not.toHaveBeenCalled();
});
});
Loading