Path: blob/main/extensions/copilot/src/platform/inlineEdits/common/observableWorkspace.ts
13400 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { assert } from '../../../util/vs/base/common/assert';6import { Disposable, toDisposable } from '../../../util/vs/base/common/lifecycle';7import { autorunWithStore, derivedHandleChanges, derivedWithStore, IObservable, IObservableWithChange, ISettableObservable, ITransaction, observableValue, runOnChange, subtransaction } from '../../../util/vs/base/common/observableInternal';8import { URI } from '../../../util/vs/base/common/uri';9import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';10import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';11import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';12import { DiagnosticData } from './dataTypes/diagnosticData';13import { DocumentId } from './dataTypes/documentId';14import { LanguageId } from './dataTypes/languageId';15import { EditReason } from './editReason';1617export abstract class ObservableWorkspace {18abstract get openDocuments(): IObservableWithChange<readonly IObservableDocument[], { added: readonly IObservableDocument[]; removed: readonly IObservableDocument[] }>;1920abstract getWorkspaceRoot(documentId: DocumentId): URI | undefined;2122getFirstOpenDocument(): IObservableDocument | undefined {23return this.openDocuments.get()[0];24}2526getDocument(documentId: DocumentId): IObservableDocument | undefined {27return this.openDocuments.get().find(d => d.id === documentId);28}2930private _version = 0;3132/**33* Is fired when any open document changes.34*/35public readonly onDidOpenDocumentChange = derivedHandleChanges({36owner: this,37changeTracker: {38createChangeSummary: () => ({ didChange: false }),39handleChange: (ctx, changeSummary) => {40if (!ctx.didChange(this.openDocuments)) {41changeSummary.didChange = true; // A document changed42}43return true;44}45}46}, (reader, changeSummary) => {47const docs = this.openDocuments.read(reader);48for (const d of docs) {49d.value.read(reader); // add dependency50}51if (changeSummary.didChange) {52this._version++; // to force a change53}54return this._version;5556// TODO@hediet make this work:57/*58const docs = this.openDocuments.read(reader);59for (const d of docs) {60if (reader.readChangesSinceLastRun(d.value).length > 0) {61reader.reportChange(d);62}63}64return undefined;65*/66});6768public readonly lastActiveDocument = derivedWithStore((_reader, store) => {69const obs = observableValue('lastActiveDocument', undefined as IObservableDocument | undefined);70store.add(autorunWithStore((reader, store) => {71const docs = this.openDocuments.read(reader);72for (const d of docs) {73store.add(runOnChange(d.value, () => {74obs.set(d, undefined);75}));76}77}));78return obs;79}).flatten();80}8182export interface IObservableDocument {83readonly id: DocumentId;84readonly value: IObservableWithChange<StringText, StringEditWithReason>;8586/**87* Increases whenever the value changes. Is also used to reference document states from the past.88*/89readonly version: IObservable<number>;9091/**92* `selection` is an array because of `multi-cursor` support.93*/94readonly selection: IObservable<readonly OffsetRange[]>;95/**96* 0-based line number of the primary cursor.97*/98readonly primarySelectionLine: IObservable<number | undefined>;99readonly visibleRanges: IObservable<readonly OffsetRange[]>;100readonly languageId: IObservable<LanguageId>;101readonly diagnostics: IObservable<readonly DiagnosticData[]>;102}103104export class StringEditWithReason extends StringEdit {105constructor(106replacements: StringEdit['replacements'],107public readonly reason: EditReason,108) {109super(replacements);110}111}112113export class MutableObservableWorkspace extends ObservableWorkspace {114private readonly _openDocuments = observableValue<readonly IObservableDocument[], { added: readonly IObservableDocument[]; removed: readonly IObservableDocument[] }>(this, []);115public readonly openDocuments = this._openDocuments;116117private readonly _documents = new Map<DocumentId, MutableObservableDocument>();118119/**120* Dispose to remove.121*/122public addDocument(options: { id: DocumentId; workspaceRoot?: URI; initialValue?: string; initialVersionId?: number; languageId?: LanguageId }, tx: ITransaction | undefined = undefined): MutableObservableDocument {123assert(!this._documents.has(options.id));124125const document = new MutableObservableDocument(126options.id,127new StringText(options.initialValue ?? ''),128[],129options.languageId ?? LanguageId.PlainText,130() => {131this._documents.delete(options.id);132const docs = this._openDocuments.get();133const filteredDocs = docs.filter(d => d.id !== document.id);134if (filteredDocs.length !== docs.length) {135this._openDocuments.set(filteredDocs, tx, { added: [], removed: [document] });136}137},138options.initialVersionId ?? 0,139options.workspaceRoot,140);141142this._documents.set(options.id, document);143this._openDocuments.set([...this._openDocuments.get(), document], tx, { added: [document], removed: [] });144145return document;146}147148public override getDocument(id: DocumentId): MutableObservableDocument | undefined {149return this._documents.get(id);150}151152public clear(): void {153this._openDocuments.set([], undefined, { added: [], removed: this._openDocuments.get() });154for (const doc of this._documents.values()) {155doc.dispose();156}157this._documents.clear();158}159160getWorkspaceRoot(documentId: DocumentId): URI | undefined {161return this._documents.get(documentId)?.workspaceRoot;162}163}164165export class MutableObservableDocument extends Disposable implements IObservableDocument {166private readonly _value: ISettableObservable<StringText, StringEditWithReason>;167public get value(): IObservableWithChange<StringText, StringEditWithReason> { return this._value; }168169private readonly _selection: ISettableObservable<readonly OffsetRange[]>;170public get selection(): IObservable<readonly OffsetRange[]> { return this._selection; }171172private readonly _primarySelectionLine: ISettableObservable<number | undefined>;173public get primarySelectionLine(): IObservable<number | undefined> { return this._primarySelectionLine; }174175private readonly _visibleRanges: ISettableObservable<readonly OffsetRange[]>;176public get visibleRanges(): IObservable<readonly OffsetRange[]> { return this._visibleRanges; }177178private readonly _languageId: ISettableObservable<LanguageId>;179public get languageId(): IObservable<LanguageId> { return this._languageId; }180181private readonly _version: ISettableObservable<number>;182public get version(): IObservable<number> { return this._version; }183184private readonly _diagnostics: ISettableObservable<readonly DiagnosticData[]>;185public get diagnostics(): IObservable<readonly DiagnosticData[]> { return this._diagnostics; }186187constructor(188public readonly id: DocumentId,189value: StringText,190selection: readonly OffsetRange[],191languageId: LanguageId,192onDispose: () => void,193versionId: number,194public readonly workspaceRoot: URI | undefined,195) {196super();197198this._value = observableValue(this, value);199this._selection = observableValue(this, selection);200this._primarySelectionLine = observableValue(this, undefined);201this._visibleRanges = observableValue(this, []);202this._languageId = observableValue(this, languageId);203this._version = observableValue(this, versionId);204this._diagnostics = observableValue(this, []);205206this._register(toDisposable(onDispose));207}208209setSelection(selection: readonly OffsetRange[], tx: ITransaction | undefined = undefined, primaryLine?: number): void {210this._selection.set(selection, tx);211this._primarySelectionLine.set(primaryLine, tx);212}213214setVisibleRange(visibleRanges: readonly OffsetRange[], tx: ITransaction | undefined = undefined): void {215this._visibleRanges.set(visibleRanges, tx);216}217218applyEdit(edit: StringEdit | StringEditWithReason, tx: ITransaction | undefined = undefined, newVersion: number | undefined = undefined): void {219const newValue = edit.applyOnText(this.value.get());220const e = edit instanceof StringEditWithReason ? edit : new StringEditWithReason(edit.replacements, EditReason.unknown);221subtransaction(tx, tx => {222this._value.set(newValue, tx, e);223this._version.set(newVersion ?? this._version.get() + 1, tx);224});225}226227updateSelection(selection: readonly OffsetRange[], tx: ITransaction | undefined = undefined, primaryLine?: number): void {228this._selection.set(selection, tx);229this._primarySelectionLine.set(primaryLine, tx);230}231232setValue(value: StringText, tx: ITransaction | undefined = undefined, newVersion: number | undefined = undefined): void {233const reason = EditReason.unknown;234const e = new StringEditWithReason([StringReplacement.replace(new OffsetRange(0, this.value.get().value.length), value.value)], reason);235subtransaction(tx, tx => {236this._value.set(value, tx, e);237this._version.set(newVersion ?? this._version.get() + 1, tx);238});239}240241updateDiagnostics(diagnostics: readonly DiagnosticData[], tx: ITransaction | undefined = undefined): void {242this._diagnostics.set(diagnostics, tx);243}244}245246247