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
8 changes: 8 additions & 0 deletions .rebase/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,3 +560,11 @@ https://github.com/che-incubator/che-code/pull/695

- code/build/npm/preinstall.ts
---

#### @RomanNikitenko
https://github.com/che-incubator/che-code/pull/754

- code/src/vs/server/node/webClientServer.ts
- code/src/vs/workbench/browser/web.api.ts
- code/src/vs/workbench/browser/web.main.ts
---
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[
{
"from": "import type * as http from 'http';",
"by": "import * as http from 'http';\nimport { getCheRedirectLocation } from './che/webClientServer.js';"
"by": "import * as http from 'http';\nimport { getCheInitialKeybindings, getCheRedirectLocation } from './che/webClientServer.js';"
},
{
"from": "\t\t\tconst newLocation = url.format({ pathname: basePath, query: newQuery });\n\t\t\tresponseHeaders['Location'] = newLocation;",
"by": "\t\t\tresponseHeaders['Location'] = getCheRedirectLocation(req, newQuery);"
},
{
"from": "callbackRoute: callbackRoute\n\t\t};",
"by": "callbackRoute: callbackRoute,\n\t\t\tcheInitialKeybindings: await getCheInitialKeybindings(),\n\t\t};"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"from": "readonly configurationDefaults?: Record<string, unknown>;\n\n\t//#endregion\n\n\t//#region Profile options",
"by": "readonly configurationDefaults?: Record<string, unknown>;\n\n\t//#endregion\n\n\t/**\n\t * Initial keybindings content from ConfigMap (JSON array string).\n\t * When set, keybindings are merged into IndexedDB on every workspace start.\n\t */\n\treadonly cheInitialKeybindings?: string;\n\n\t//#region Profile options"
}
]
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[
{
"from": "import { INotificationService, Severity } from '../../platform/notification/common/notification.js';",
"by": "import { INotificationService, Severity } from '../../platform/notification/common/notification.js';\nimport { getPolicyService } from './che/web.js';"
"by": "import { INotificationService, Severity } from '../../platform/notification/common/notification.js';\nimport { CheKeybindingsInitializer, getPolicyService } from './che/web.js';"
},
{
"from": "const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, policyService);",
"by": "const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, userDataProfileService, userDataProfilesService, fileService, remoteAgentService, uriIdentityService, logService, getPolicyService(remoteAgentService,logService, this.configuration.remoteAuthority));"
},
{
"from": "const userDataInitializationService = new UserDataInitializationService(userDataInitializers);",
"by": "if (environmentService.options.cheInitialKeybindings) {\n\t\t\tuserDataInitializers.push(new CheKeybindingsInitializer(environmentService.options.cheInitialKeybindings, fileService, userDataProfilesService, logService));\n\t\t}\n\t\tconst userDataInitializationService = new UserDataInitializationService(userDataInitializers);"
}
]
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 { promises as fs } from 'fs';
import * as http from 'http';
import * as url from 'url';

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

/**
* Reads keybindings.json content from the ConfigMap mount path.
* Returns the raw JSON string if the file exists, undefined otherwise.
*/
export async function getCheInitialKeybindings(): Promise<string | undefined> {
try {
return await fs.readFile(CHE_CONFIG_KEYBINDINGS_PATH, 'utf-8');
} catch {
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 { getCheInitialKeybindings, 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,
cheInitialKeybindings: await getCheInitialKeybindings(),
};

const cookies = cookie.parse(req.headers.cookie || '');
Expand Down
83 changes: 82 additions & 1 deletion code/src/vs/workbench/browser/che/web.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**********************************************************************
* Copyright (c) 2025 Red Hat, Inc.
* Copyright (c) 2025-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,10 +9,91 @@
***********************************************************************/
/* eslint-disable header/header */

import { VSBuffer } from '../../../base/common/buffer.js';
import * as json from '../../../base/common/json.js';
import { IFileService } from '../../../platform/files/common/files.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { IPolicyService, NullPolicyService } from '../../../platform/policy/common/policy.js';
import { PolicyChannelClient } from '../../../platform/policy/common/policyIpc.js';
import { IUserDataProfilesService } from '../../../platform/userDataProfile/common/userDataProfile.js';
import { IRemoteAgentService } from '../../services/remote/common/remoteAgentService.js';
import { IUserDataInitializer } from '../../services/userData/browser/userDataInit.js';
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';

const CONFIGMAP_SOURCE_MARKER = 'configmap';

interface KeybindingEntry {
key: string;
command: string;
when?: string;
args?: unknown;
_source?: string;
}

/**
* Merges keybindings from ConfigMap into IndexedDB on every workspace start.
*
* Strategy: admin keybindings (from ConfigMap) are appended at the end of the
* array so they take priority over user keybindings (VS Code processes
* keybindings top-to-bottom, later entries win). Admin entries are tagged with
* `"_source": "configmap"` so they can be identified and replaced on the next
* start when the ConfigMap content changes.
*/
export class CheKeybindingsInitializer implements IUserDataInitializer {

constructor(
private readonly initialKeybindings: string,
private readonly fileService: IFileService,
private readonly userDataProfilesService: IUserDataProfilesService,
private readonly logService: ILogService,
) { }

async requiresInitialization(): Promise<boolean> {
return true;
}

async whenInitializationFinished(): Promise<void> { }

async initializeRequiredResources(): Promise<void> {
const resource = this.userDataProfilesService.defaultProfile.keybindingsResource;

let configmapEntries: KeybindingEntry[];
try {
const parsed = json.parse(this.initialKeybindings);
if (!Array.isArray(parsed)) {
this.logService.warn('[Che] ConfigMap keybindings.json is not a JSON array, skipping.');
return;
}
configmapEntries = parsed;
} catch (e) {
this.logService.warn('[Che] ConfigMap keybindings.json is not valid JSON, skipping.', e);
return;
}

const markedEntries: KeybindingEntry[] = configmapEntries.map(entry => ({ ...entry, _source: CONFIGMAP_SOURCE_MARKER }));

let userEntries: KeybindingEntry[] = [];
if (await this.fileService.exists(resource)) {
try {
const content = (await this.fileService.readFile(resource)).value.toString();
const existing = json.parse(content);
if (Array.isArray(existing)) {
userEntries = existing.filter((e: KeybindingEntry) => e._source !== CONFIGMAP_SOURCE_MARKER);
}
} catch (e) {
this.logService.warn('[Che] Failed to parse existing keybindings. Aborting to prevent data loss.', e);
return;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const merged = [...userEntries, ...markedEntries];
this.logService.info(`[Che] Merging keybindings: ${userEntries.length} user + ${markedEntries.length} admin (ConfigMap).`);
await this.fileService.writeFile(resource, VSBuffer.fromString(JSON.stringify(merged, null, '\t')));
}

async initializeInstalledExtensions(_instantiationService: IInstantiationService): Promise<void> { }
async initializeOtherResources(_instantiationService: IInstantiationService): Promise<void> { }
}

// Get policy service from remote agent if available, otherwise use NullPolicyService
export function getPolicyService(remoteAgentService: IRemoteAgentService, logService: ILogService, remoteAuthority?: string): IPolicyService {
Expand Down
6 changes: 6 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,12 @@ export interface IWorkbenchConstructionOptions {

//#endregion

/**
* Initial keybindings content from ConfigMap (JSON array string).
* When set, keybindings are merged into IndexedDB on every workspace start.
*/
readonly cheInitialKeybindings?: string;

//#region Profile options

/**
Expand Down
5 changes: 4 additions & 1 deletion code/src/vs/workbench/browser/web.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ import { ISecretStorageService } from '../../platform/secrets/common/secrets.js'
import { TunnelSource } from '../services/remote/common/tunnelModel.js';
import { mainWindow } from '../../base/browser/window.js';
import { INotificationService, Severity } from '../../platform/notification/common/notification.js';
import { getPolicyService } from './che/web.js';
import { CheKeybindingsInitializer, getPolicyService } from './che/web.js';
import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js';
import { DefaultAccountService } from '../services/accounts/browser/defaultAccount.js';
import { AccountPolicyService } from '../services/policies/common/accountPolicyService.js';
Expand Down Expand Up @@ -439,6 +439,9 @@ export class BrowserMain extends Disposable {
if (environmentService.options.profile) {
userDataInitializers.push(new UserDataProfileInitializer(environmentService, fileService, userDataProfileService, storageService, logService, uriIdentityService, requestService));
}
if (environmentService.options.cheInitialKeybindings) {
userDataInitializers.push(new CheKeybindingsInitializer(environmentService.options.cheInitialKeybindings, fileService, userDataProfilesService, logService));
}
const userDataInitializationService = new UserDataInitializationService(userDataInitializers);
serviceCollection.set(IUserDataInitializationService, userDataInitializationService);

Expand Down
2 changes: 2 additions & 0 deletions rebase.sh
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,8 @@ resolve_conflicts() {
apply_changes_multi_line "$conflictingFile"
elif [[ "$conflictingFile" == "code/src/vs/workbench/browser/web.main.ts" ]]; then
apply_changes_multi_line "$conflictingFile"
elif [[ "$conflictingFile" == "code/src/vs/workbench/browser/web.api.ts" ]]; then
apply_changes_multi_line "$conflictingFile"
elif [[ "$conflictingFile" == "code/src/vs/server/node/serverServices.ts" ]]; then
apply_changes_multi_line "$conflictingFile"
elif [[ "$conflictingFile" == "code/src/vs/server/node/serverEnvironmentService.ts" ]]; then
Expand Down