// ***************************************************************************** // Copyright (C) 2025 EclipseSource GmbH and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { ArrayUtils, Disposable, Emitter, Event, nls, URI } from '@theia/core'; import { SerializableChangeSetElement } from './chat-model-serialization'; export interface ChangeSetElement { readonly uri: URI; onDidChange?: Event readonly name?: string; readonly icon?: string; readonly additionalInfo?: string; readonly state?: 'pending' | 'applied' | 'stale'; readonly type?: 'add' | 'modify' | 'delete'; readonly data?: { [key: string]: unknown }; /** Called when an element is shown in the UI */ onShow?(): void; /** Called when an element is hidden in the UI */ onHide?(): void; open?(): Promise; openChange?(): Promise; apply?(): Promise; revert?(): Promise; dispose?(): void; /** * Serializes this element to a format suitable for persistence. * Each element type is responsible for serializing its own data. * Optional - elements without this method will be excluded from serialization. */ toSerializable?(): SerializableChangeSetElement; } export interface ChatUpdateChangeSetEvent { kind: 'updateChangeSet'; elements?: ChangeSetElement[]; title?: string; } export interface ChangeSetChangeEvent { title?: string; added?: URI[], removed?: URI[], modified?: URI[], /** Fired when only the state of a given element changes, not its contents */ state?: URI[], } export interface ChangeSet extends Disposable { onDidChange: Event; readonly title: string; setTitle(title: string): void; getElements(): ChangeSetElement[]; /** * Find an element by URI. * @param uri The URI to look for. * @returns The element with the given URI, or undefined if not found. */ getElementByURI(uri: URI): ChangeSetElement | undefined; /** @returns true if addition produces a change; false otherwise. */ addElements(...elements: ChangeSetElement[]): boolean; setElements(...elements: ChangeSetElement[]): void; /** @returns true if deletion produces a change; false otherwise. */ removeElements(...uris: URI[]): boolean; dispose(): void; } export class ChangeSetImpl implements ChangeSet { /** @param changeSets ordered from tip to root. */ static combine(changeSets: Iterable): Map { const result = new Map(); for (const next of changeSets) { next._elements.forEach((value, key) => !result.has(key) && result.set(key, value)); // Break at the first element whose values were set, not just changed through addition and deletion. if (next.hasBeenSet) { break; } } return result; } protected readonly _onDidChangeEmitter = new Emitter(); onDidChange: Event = this._onDidChangeEmitter.event; protected readonly _onDidChangeContentsEmitter = new Emitter(); onDidChangeContents: Event = this._onDidChangeContentsEmitter.event; protected hasBeenSet = false; protected _elements = new Map(); protected _title = nls.localize('theia/ai/chat/changeSetDefaultTitle', 'Suggested Changes'); get title(): string { return this._title; } constructor(elements: ChangeSetElement[] = []) { this.addElements(...elements); } getElements(): ChangeSetElement[] { return ArrayUtils.coalesce(Array.from(this._elements.values())); } /** Will replace any element that is already present, using URI as identity criterion. */ addElements(...elements: ChangeSetElement[]): boolean { const added: URI[] = []; const modified: URI[] = []; elements.forEach(element => { if (this.doAdd(element)) { modified.push(element.uri); } else { added.push(element.uri); } }); this.notifyChange({ added, modified }); return !!(added.length || modified.length); } setTitle(title: string): void { this._title = title; this.notifyChange({ title }); } protected doAdd(element: ChangeSetElement): boolean { const id = element.uri.toString(); const existing = this._elements.get(id); existing?.dispose?.(); this._elements.set(id, element); element.onDidChange?.(() => this.notifyChange({ state: [element.uri] })); return !!existing; } setElements(...elements: ChangeSetElement[]): void { this.hasBeenSet = true; const added = []; const modified = []; const removed = []; const toHandle = new Set(this._elements.keys()); for (const element of elements) { toHandle.delete(element.uri.toString()); if (this.doAdd(element)) { added.push(element.uri); } else { modified.push(element.uri); } } for (const toDelete of toHandle) { const uri = new URI(toDelete); if (this.doDelete(uri)) { removed.push(uri); } } this.notifyChange({ added, modified, removed }); } removeElements(...uris: URI[]): boolean { const removed: URI[] = []; for (const uri of uris) { if (this.doDelete(uri)) { removed.push(uri); } } this.notifyChange({ removed }); return !!removed.length; } getElementByURI(uri: URI): ChangeSetElement | undefined { return this._elements.get(uri.toString()); } protected doDelete(uri: URI): boolean { const id = uri.toString(); const delendum = this._elements.get(id); if (delendum) { delendum.dispose?.(); } this._elements.set(id, undefined); return !!delendum; } protected notifyChange(change: ChangeSetChangeEvent): void { this._onDidChangeContentsEmitter.fire(change); this._onDidChangeEmitter.fire({ kind: 'updateChangeSet', elements: this.getElements(), title: this.title }); } dispose(): void { this._onDidChangeEmitter.dispose(); this._elements.forEach(element => element?.dispose?.()); } }