diff --git a/.github/workflows/pull-request-check-licenses.yml b/.github/workflows/pull-request-check-licenses.yml index cee0f50f5047..ea283f450e45 100644 --- a/.github/workflows/pull-request-check-licenses.yml +++ b/.github/workflows/pull-request-check-licenses.yml @@ -59,3 +59,8 @@ jobs: docker run --rm -t \ -v $PWD/code/extensions/che-terminal:/workspace/project \ quay.io/che-incubator/dash-licenses:next --check + - name: Check che-keybindings dependencies usage restrictions + run: | + docker run --rm -t \ + -v $PWD/code/extensions/che-keybindings:/workspace/project \ + quay.io/che-incubator/dash-licenses:next --check diff --git a/code/build/gulpfile.extensions.ts b/code/build/gulpfile.extensions.ts index 83b07f4be8f7..03ec1e48c4b6 100644 --- a/code/build/gulpfile.extensions.ts +++ b/code/build/gulpfile.extensions.ts @@ -62,6 +62,7 @@ const compilations = [ 'extensions/che-terminal/tsconfig.json', 'extensions/che-telemetry/tsconfig.json', 'extensions/che-github-authentication/tsconfig.json', + 'extensions/che-keybindings/tsconfig.json', 'extensions/configuration-editing/tsconfig.json', 'extensions/css-language-features/client/tsconfig.json', 'extensions/css-language-features/server/tsconfig.json', diff --git a/code/build/npm/dirs.ts b/code/build/npm/dirs.ts index 7525ed864e28..073c206aa558 100644 --- a/code/build/npm/dirs.ts +++ b/code/build/npm/dirs.ts @@ -22,6 +22,7 @@ export const dirs = [ 'extensions/che-resource-monitor', 'extensions/che-terminal', 'extensions/che-github-authentication', + 'extensions/che-keybindings', 'extensions/configuration-editing', 'extensions/copilot', 'extensions/css-language-features', diff --git a/code/extensions/che-keybindings/.vscodeignore b/code/extensions/che-keybindings/.vscodeignore new file mode 100644 index 000000000000..2b335b58dc58 --- /dev/null +++ b/code/extensions/che-keybindings/.vscodeignore @@ -0,0 +1,8 @@ +build/** +src/** +tests/** +coverage/** +out/** +tsconfig.json +extension.webpack.config.js +yarn.lock diff --git a/code/extensions/che-keybindings/README.md b/code/extensions/che-keybindings/README.md new file mode 100644 index 000000000000..923c41a64801 --- /dev/null +++ b/code/extensions/che-keybindings/README.md @@ -0,0 +1,7 @@ +# Che Workspace Key Bindings for Visual Studio Code + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +This extension handles Key Bindings of Che workspace on Visual Studio Code diff --git a/code/extensions/che-keybindings/esbuild.mts b/code/extensions/che-keybindings/esbuild.mts new file mode 100644 index 000000000000..2678a6515efa --- /dev/null +++ b/code/extensions/che-keybindings/esbuild.mts @@ -0,0 +1,30 @@ +/********************************************************************** + * 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 + ***********************************************************************/ +/* + * This file was generated using AI assistance (Cursor AI) + * and reviewed by the maintainers. + */ +import * as path from 'node:path'; +import { run } from '../esbuild-extension-common.mts'; + +const srcDir = path.join(import.meta.dirname, 'src'); +const outDir = path.join(import.meta.dirname, 'dist'); + +run({ + platform: 'node', + entryPoints: { + 'extension': path.join(srcDir, 'extension.ts'), + }, + srcDir, + outdir: outDir, + additionalOptions: { + external: ['vscode', 'bufferutil', 'utf-8-validate'], + }, +}, process.argv); diff --git a/code/extensions/che-keybindings/images/eclipse-che-logo.png b/code/extensions/che-keybindings/images/eclipse-che-logo.png new file mode 100644 index 000000000000..0571dcdf997e Binary files /dev/null and b/code/extensions/che-keybindings/images/eclipse-che-logo.png differ diff --git a/code/extensions/che-keybindings/package-lock.json b/code/extensions/che-keybindings/package-lock.json new file mode 100644 index 000000000000..904941d77472 --- /dev/null +++ b/code/extensions/che-keybindings/package-lock.json @@ -0,0 +1,153 @@ +{ + "name": "keybindings", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "keybindings", + "version": "0.0.1", + "license": "EPL-2.0", + "dependencies": { + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "@types/vscode": "^1.85.0", + "typescript": "^5.3.3" + }, + "engines": { + "vscode": "^1.88.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + } + } +} diff --git a/code/extensions/che-keybindings/package.json b/code/extensions/che-keybindings/package.json new file mode 100644 index 000000000000..3d452b47f45f --- /dev/null +++ b/code/extensions/che-keybindings/package.json @@ -0,0 +1,48 @@ +{ + "name": "keybindings", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "eclipse-che", + "license": "EPL-2.0", + "version": "0.0.1", + "engines": { + "vscode": "^1.88.0" + }, + "icon": "images/eclipse-che-logo.png", + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "main": "./out/extension.js", + "scripts": { + "compile": "gulp compile-extension:che-keybindings", + "watch": "gulp watch-extension:che-keybindings", + "vscode:prepublish": "npm run compile", + "lint:fix": "eslint --fix --cache=true --no-error-on-unmatched-pattern=true \"{src,tests}/**/*.{ts,tsx}\"", + "license:generate": "podman run --rm -t -v $PWD:/workspace/project quay.io/che-incubator/dash-licenses:next" + }, + "repository": { + "type": "git", + "url": "https://github.com/che-incubator/che-code.git" + }, + "extensionDependencies": [ + "eclipse-che.api", + "eclipse-che.terminal" + ], + "dependencies": { + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "typescript": "^5.3.3", + "@types/vscode": "^1.85.0" + } +} diff --git a/code/extensions/che-keybindings/package.nls.json b/code/extensions/che-keybindings/package.nls.json new file mode 100644 index 000000000000..7a0d6416523a --- /dev/null +++ b/code/extensions/che-keybindings/package.nls.json @@ -0,0 +1,4 @@ +{ + "displayName": "Eclipse Che Keybindings", + "description": "Provides Keybindings support" +} diff --git a/code/extensions/che-keybindings/src/cheAPi.ts b/code/extensions/che-keybindings/src/cheAPi.ts new file mode 100644 index 000000000000..df167cfcffe9 --- /dev/null +++ b/code/extensions/che-keybindings/src/cheAPi.ts @@ -0,0 +1,90 @@ +/********************************************************************** + * Copyright (c) 2025 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 * as vscode from "vscode"; + +export interface KeybindingsResponse { + keybindingsJson: string; + + meta?: { + clientId?: string; + resourceVersion?: string; + }; +} + +const API = "/api"; + +export async function getKeybindings( + namespace: string, + output?: vscode.OutputChannel, +): Promise { + output?.appendLine(`[API] GET ${API}/namespace/${namespace}/keybindings`); + + const res = await fetch(`${API}/namespace/${namespace}/keybindings`, { + credentials: "include", + }); + + output?.appendLine(`[API] GET Status: ${res.status}`); + + if (!res.ok) { + output?.appendLine(`[API] GET Failed: ${await res.text()}`); + return null; + } + + const data = (await res.json()) as KeybindingsResponse; + + output?.appendLine( + `[API] GET Success - Received ${data.keybindingsJson.length} bytes`, + ); + + if (data.meta) { + output?.appendLine( + `[API] Meta => clientId=${data.meta.clientId ?? "N/A"}, resourceVersion=${data.meta.resourceVersion ?? "N/A"}`, + ); + } + + return data; +} + +export async function patchKeybindings( + namespace: string, + keybindingsJson: string, + clientId: string, + output?: vscode.OutputChannel, +): Promise { + output?.appendLine(`[API] PATCH ${API}/namespace/${namespace}/keybindings`); + output?.appendLine(`[API] Payload Size: ${keybindingsJson.length} bytes`); + output?.appendLine(`[API] Client ID: ${clientId}`); + + const res = await fetch(`${API}/namespace/${namespace}/keybindings`, { + method: "PATCH", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + keybindingsJson, + meta: { + clientId, + }, + }), + }); + + output?.appendLine(`[API] PATCH Status: ${res.status}`); + + if (!res.ok) { + const error = await res.text(); + output?.appendLine(`[API] PATCH Failed: ${error}`); + throw new Error(error); + } + + output?.appendLine("[API] PATCH Successful."); +} diff --git a/code/extensions/che-keybindings/src/extension.ts b/code/extensions/che-keybindings/src/extension.ts new file mode 100644 index 000000000000..0da656384466 --- /dev/null +++ b/code/extensions/che-keybindings/src/extension.ts @@ -0,0 +1,77 @@ +/********************************************************************** + * 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 * as vscode from "vscode"; +import { USER_KEYBINDINGS_URI } from "./keybindings"; + +export async function activate(context: vscode.ExtensionContext) { + const output = vscode.window.createOutputChannel("Che Keybindings Sync"); + context.subscriptions.push(output); + + output.show(true); + output.appendLine("======================================"); + output.appendLine("Che Keybindings Investigation"); + output.appendLine(`Activated: ${new Date().toISOString()}`); + output.appendLine("======================================"); + + try { + const doc = await vscode.workspace.openTextDocument(USER_KEYBINDINGS_URI); + + output.appendLine("Successfully opened keybindings.json"); + output.appendLine(`URI : ${doc.uri.toString()}`); + output.appendLine(`Scheme : ${doc.uri.scheme}`); + output.appendLine(`Path : ${doc.uri.path}`); + output.appendLine(`Length : ${doc.getText().length}`); + + output.appendLine("----- Current Content -----"); + output.appendLine(doc.getText()); + output.appendLine("---------------------------"); + } catch (err) { + output.appendLine(`Failed to open keybindings.json: ${err}`); + } + + // Fires whenever keybindings.json is saved + context.subscriptions.push( + vscode.workspace.onDidSaveTextDocument(doc => { + output.appendLine(""); + output.appendLine("===== onDidSaveTextDocument ====="); + output.appendLine(`Saved: ${doc.uri.toString()}`); + + if (doc.uri.toString() === USER_KEYBINDINGS_URI.toString()) { + output.appendLine("Detected keybindings.json save"); + output.appendLine(`Length: ${doc.getText().length}`); + output.appendLine(doc.getText()); + } + }), + ); + + // Fires whenever any text document changes + context.subscriptions.push( + vscode.workspace.onDidChangeTextDocument(event => { + if (event.document.uri.toString() === USER_KEYBINDINGS_URI.toString()) { + output.appendLine(""); + output.appendLine("===== onDidChangeTextDocument ====="); + output.appendLine(`Changes: ${event.contentChanges.length}`); + + for (const change of event.contentChanges) { + output.appendLine( + `Range: ${change.rangeOffset}-${change.rangeOffset + change.rangeLength}`, + ); + output.appendLine(`Inserted: ${JSON.stringify(change.text)}`); + } + } + }), + ); + + output.appendLine("Listening for keybindings changes..."); +} + +export function deactivate() {} diff --git a/code/extensions/che-keybindings/src/keybindings.ts b/code/extensions/che-keybindings/src/keybindings.ts new file mode 100644 index 000000000000..4a480c23618e --- /dev/null +++ b/code/extensions/che-keybindings/src/keybindings.ts @@ -0,0 +1,86 @@ +/********************************************************************** + * Copyright (c) 2025 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 * as vscode from "vscode"; + +export const USER_KEYBINDINGS_URI = vscode.Uri.parse( + "vscode-userdata:/User/keybindings.json", +); + +export async function readUserKeybindings( + output?: vscode.OutputChannel, +): Promise { + output?.appendLine("[Keybindings] Reading user keybindings..."); + + try { + const doc = await vscode.workspace.openTextDocument(USER_KEYBINDINGS_URI); + + output?.appendLine( + `[Keybindings] Opened: ${USER_KEYBINDINGS_URI.toString()}`, + ); + + const text = doc.getText(); + + output?.appendLine(`[Keybindings] Read successful (${text.length} bytes).`); + + return text; + } catch (err) { + output?.appendLine(`[Keybindings] Failed to read keybindings: ${err}`); + + return "[]"; + } +} + +export async function writeUserKeybindings( + text: string, + output?: vscode.OutputChannel, +): Promise { + output?.appendLine("[Keybindings] Writing user keybindings..."); + + const doc = await vscode.workspace.openTextDocument(USER_KEYBINDINGS_URI); + + const current = doc.getText(); + + output?.appendLine(`[Keybindings] Current size: ${current.length} bytes`); + output?.appendLine(`[Keybindings] New size: ${text.length} bytes`); + + if (current === text) { + output?.appendLine("[Keybindings] No changes detected. Skipping write."); + return; + } + + const edit = new vscode.WorkspaceEdit(); + + edit.replace( + USER_KEYBINDINGS_URI, + new vscode.Range(doc.positionAt(0), doc.positionAt(current.length)), + text, + ); + + output?.appendLine("[Keybindings] Applying workspace edit..."); + + const applied = await vscode.workspace.applyEdit(edit); + + output?.appendLine(`[Keybindings] Workspace edit applied: ${applied}`); + + if (!applied) { + throw new Error("Failed to apply workspace edit."); + } + + output?.appendLine("[Keybindings] Saving keybindings.json..."); + + const saved = await doc.save(); + + output?.appendLine(`[Keybindings] Save completed: ${saved}`); + + output?.appendLine("[Keybindings] Write completed successfully."); +} diff --git a/code/extensions/che-keybindings/src/syncManager.ts b/code/extensions/che-keybindings/src/syncManager.ts new file mode 100644 index 000000000000..790fb7eb59a3 --- /dev/null +++ b/code/extensions/che-keybindings/src/syncManager.ts @@ -0,0 +1,166 @@ +/********************************************************************** + * 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 * as vscode from "vscode"; +import { getKeybindings, patchKeybindings } from "./cheAPi"; +import { + readUserKeybindings, + writeUserKeybindings, + USER_KEYBINDINGS_URI, +} from "./keybindings"; +import { getOrCreateClientId } from "./utils"; + +export class SyncManager implements vscode.Disposable { + private suppressPush = false; + + private clientId: string; + + private namespace: string; + + private lastSynced = ""; + + private timer?: NodeJS.Timeout; + + constructor( + private readonly context: vscode.ExtensionContext, + private readonly output: vscode.OutputChannel, + ) { + const cfg = vscode.workspace.getConfiguration("cheKeybindingsSync"); + + this.namespace = cfg.get("namespace", "default"); + this.clientId = getOrCreateClientId(context.globalState); + + this.output.appendLine("===== SyncManager Created ====="); + this.output.appendLine(`Namespace : ${this.namespace}`); + this.output.appendLine(`Client ID : ${this.clientId}`); + } + + async initialize() { + this.output.appendLine("Initializing SyncManager..."); + + try { + await this.pull(); + this.output.appendLine("Initial pull completed."); + } catch (err) { + this.output.appendLine(`Initial pull failed: ${err}`); + } + + this.context.subscriptions.push( + vscode.workspace.onDidSaveTextDocument(this.onSave, this), + ); + + this.output.appendLine("Registered onDidSaveTextDocument listener."); + + this.timer = setInterval(() => { + this.output.appendLine("Polling backend for remote keybindings..."); + this.pull().catch((err) => + this.output.appendLine(`Polling failed: ${err}`), + ); + }, 10000); + + this.output.appendLine("Polling started (every 10 seconds)."); + } + + dispose() { + this.output.appendLine("Disposing SyncManager..."); + + if (this.timer) { + clearInterval(this.timer); + this.output.appendLine("Polling timer stopped."); + } + } + + private async onSave(doc: vscode.TextDocument) { + this.output.appendLine(`Document saved: ${doc.uri.toString()}`); + + if (doc.uri.toString() !== USER_KEYBINDINGS_URI.toString()) { + this.output.appendLine("Not keybindings.json. Ignoring."); + return; + } + + this.output.appendLine("Detected keybindings.json save."); + + if (this.suppressPush) { + this.output.appendLine("Push suppressed (local update from backend)."); + this.suppressPush = false; + return; + } + + const text = doc.getText(); + + if (text === this.lastSynced) { + this.output.appendLine("No changes detected. Skipping push."); + return; + } + + this.output.appendLine( + `Pushing keybindings to backend (${text.length} bytes)...`, + ); + + try { + await patchKeybindings(this.namespace, text, this.clientId); + + this.lastSynced = text; + + this.output.appendLine("Push completed successfully."); + } catch (err) { + this.output.appendLine(`Push failed: ${err}`); + } + } + + async pull() { + this.output.appendLine("Pulling keybindings from backend..."); + + try { + const remote = await getKeybindings(this.namespace); + + if (!remote) { + this.output.appendLine("No response from backend."); + return; + } + + this.output.appendLine( + `Received remote keybindings (${remote.keybindingsJson.length} bytes).`, + ); + + if (remote.meta?.clientId === this.clientId) { + this.output.appendLine( + "Remote update originated from this client. Ignoring.", + ); + return; + } + + const local = await readUserKeybindings(); + + this.output.appendLine( + `Local size: ${local.length}, Remote size: ${remote.keybindingsJson.length}`, + ); + + if (local === remote.keybindingsJson) { + this.output.appendLine("Local keybindings already up-to-date."); + this.lastSynced = local; + return; + } + + this.output.appendLine("Applying remote keybindings..."); + + this.suppressPush = true; + + await writeUserKeybindings(remote.keybindingsJson); + + this.lastSynced = remote.keybindingsJson; + + this.output.appendLine("Remote keybindings applied successfully."); + } catch (err) { + this.output.appendLine(`Pull failed: ${err}`); + } + } +} diff --git a/code/extensions/che-keybindings/src/utils.ts b/code/extensions/che-keybindings/src/utils.ts new file mode 100644 index 000000000000..3253ea18eeb4 --- /dev/null +++ b/code/extensions/che-keybindings/src/utils.ts @@ -0,0 +1,37 @@ +/********************************************************************** + * Copyright (c) 2025 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 * as vscode from "vscode"; + +export function getOrCreateClientId( + state: vscode.Memento, + output?: vscode.OutputChannel, +): string { + output?.appendLine("[Utils] Retrieving client ID..."); + + let id = state.get("clientId"); + + if (!id) { + output?.appendLine( + "[Utils] No existing client ID found. Generating a new one...", + ); + + id = Math.random().toString(36).slice(2); + + state.update("clientId", id); + + output?.appendLine(`[Utils] Generated new client ID: ${id}`); + } else { + output?.appendLine(`[Utils] Existing client ID found: ${id}`); + } + + return id; +} diff --git a/code/extensions/che-keybindings/tsconfig.json b/code/extensions/che-keybindings/tsconfig.json new file mode 100644 index 000000000000..9b38bf350c87 --- /dev/null +++ b/code/extensions/che-keybindings/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "skipLibCheck": true, + "types": [ + "node" + ] + }, + "include": [ + "src/**/*", + "../../src/vscode-dts/vscode.d.ts", + ] +}