Skip to content

Commit

Permalink
Change the way the InteractiveSessionModel is loaded into the widget (#…
Browse files Browse the repository at this point in the history
…180668)

* Change the way the InteractiveSessionModel is loaded into the widget
And the way viewState is managed. Fixes a bunch of general issues with this flow between views and editors

* Fix reloading editor after clear
  • Loading branch information
roblourens authored Apr 24, 2023
1 parent 3a69e15 commit b21e880
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
// import { CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { localize } from 'vs/nls';
import { Action2, IAction2Options, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
Expand All @@ -20,6 +19,7 @@ import { InteractiveSessionEditorInput } from 'vs/workbench/contrib/interactiveS
import { InteractiveSessionViewPane } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionViewPane';
import { IInteractiveSessionWidgetService } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
import { CONTEXT_IN_INTERACTIVE_INPUT, CONTEXT_IN_INTERACTIVE_SESSION } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionContextKeys';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IInteractiveSessionWidgetHistoryService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionWidgetHistoryService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';

Expand Down Expand Up @@ -155,7 +155,11 @@ export function registerInteractiveSessionActions() {
}
async run(accessor: ServicesAccessor, ...args: any[]) {
const widgetService = accessor.get(IInteractiveSessionWidgetService);
await widgetService.lastFocusedWidget?.clear();
const interactiveSessionService = accessor.get(IInteractiveSessionService);
const sessionId = widgetService.lastFocusedWidget?.viewModel?.sessionId;
if (sessionId) {
interactiveSessionService.clearSession(sessionId);
}
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class InputEditorDecorations extends Disposable {

private async updateInputEditorDecorations() {
const value = this.widget.inputEditor.getValue();
const slashCommands = await this.widget.getSlashCommands();
const slashCommands = await this.widget.getSlashCommands(); // TODO this async call can lead to a flicker of the placeholder text when switching editor tabs

if (!value) {
const extensionPlaceholder = this.widget.viewModel?.inputPlaceholder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export interface IInteractiveSessionWidget {
readonly providerId: string;

acceptInput(query?: string): void;
clear(): void;
focusLastMessage(): void;
focusInput(): void;
getSlashCommands(): Promise<IInteractiveSlashCommand[] | undefined>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@

import * as dom from 'vs/base/browser/dom';
import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle';
import { IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
Expand All @@ -19,7 +18,9 @@ import { IEditorOpenContext } from 'vs/workbench/common/editor';
import { Memento } from 'vs/workbench/common/memento';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { InteractiveSessionEditorInput } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionEditorInput';
import { InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
import { IViewState, InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';
import { IInteractiveSessionModel } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionModel';

export interface IInteractiveSessionEditorOptions extends IEditorOptions {
providerId: string;
Expand All @@ -28,35 +29,41 @@ export interface IInteractiveSessionEditorOptions extends IEditorOptions {
export class InteractiveSessionEditor extends EditorPane {
static readonly ID: string = 'workbench.editor.interactiveSession';

private widget: InteractiveSessionWidget | undefined;
private widgetDisposables = this._register(new DisposableStore());
private widget!: InteractiveSessionWidget;

private parentElement: HTMLElement | undefined;
private dimension: dom.Dimension | undefined;

private readonly _scopedContextKeyService = this._register(new MutableDisposable<IScopedContextKeyService>());
private _scopedContextKeyService!: IScopedContextKeyService;
override get scopedContextKeyService() {
return this._scopedContextKeyService.value;
return this._scopedContextKeyService;
}

private _memento: Memento | undefined;
private _viewState: IViewState | undefined;

constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IThemeService themeService: IThemeService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IStorageService private readonly storageService: IStorageService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService,
) {
super(InteractiveSessionEditor.ID, telemetryService, themeService, storageService);
}

public async clear() {
if (this.widget) {
await this.widget.clear();
if (this.widget?.viewModel) {
this.interactiveSessionService.clearSession(this.widget.viewModel.sessionId);
}
}

protected override createEditor(parent: HTMLElement): void {
this.parentElement = parent;
this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent));
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]));

this.widget = this._register(
scopedInstantiationService.createInstance(InteractiveSessionWidget, { resource: true }, () => editorBackground, () => SIDE_BAR_BACKGROUND, () => SIDE_BAR_BACKGROUND));
this.widget.render(parent);
this.widget.setVisible(true);
}

public override focus(): void {
Expand All @@ -65,55 +72,56 @@ export class InteractiveSessionEditor extends EditorPane {
}
}

override clearInput(): void {
this.saveState();
super.clearInput();
}

override async setInput(input: InteractiveSessionEditorInput, options: IInteractiveSessionEditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
super.setInput(input, options, context, token);

this.widgetDisposables.clear();

const editorModel = await input.resolve();
if (!editorModel) {
throw new Error(`Failed to get model for interactive session editor. id: ${input.sessionId}`);
}

if (!this.parentElement) {
throw new Error('InteractiveSessionEditor lifecycle issue: Parent element not set');
if (!this.widget) {
throw new Error('InteractiveSessionEditor lifecycle issue: no editor widget');
}

this._scopedContextKeyService.value = this._register(this.contextKeyService.createScoped(this.parentElement));
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]));

if (this.widget) {
dom.clearNode(this.parentElement);
}

const memento = new Memento(input.resource.path, this.storageService);
this.widget = this.widgetDisposables.add(
scopedInstantiationService.createInstance(InteractiveSessionWidget, editorModel.model.providerId, editorModel.model, { resource: input.resource }, () => editorBackground, () => SIDE_BAR_BACKGROUND, () => SIDE_BAR_BACKGROUND, memento));
this.widget.render(this.parentElement);
this.widget.setVisible(true);

this.widgetDisposables.add(this.widget.onDidChangeViewModel(() => {
// This part is a bit odd. The widget's session and model will change. When that happens, store the latest session id
// on the EditorInput so that it can be restored when the editor moves or the window reloads.
input.sessionId = this.widget!.viewModel?.sessionId;
}));
this.updateModel(editorModel.model, options);
}

if (this.dimension) {
this.layout(this.dimension, undefined);
}
private updateModel(model: IInteractiveSessionModel, options: IInteractiveSessionEditorOptions): void {
this._memento = new Memento('interactive-session-editor-' + model.sessionId, this.storageService);
this._viewState = this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER) as IViewState;
this.widget.setModel(model, { ...this._viewState });
const listener = model.onDidDispose(() => {
// TODO go back to swapping out the EditorInput when the session is restarted instead of this listener
listener.dispose();
const newModel = this.interactiveSessionService.startSession(options.providerId, false, CancellationToken.None);
if (newModel) {
(this.input as InteractiveSessionEditorInput).sessionId = newModel.sessionId;
this.updateModel(newModel, options);
}
});
}

protected override saveState(): void {
this.widget?.saveState();

if (this._memento && this._viewState) {
const widgetViewState = this.widget.getViewState();
this._viewState!.inputValue = widgetViewState.inputValue;
this._memento!.saveMemento();
}
}

override layout(dimension: dom.Dimension, position?: dom.IDomPosition | undefined): void {
if (this.widget) {
const width = Math.min(dimension.width, 600);
this.widget.layout(dimension.height, width);
}

this.dimension = dimension;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN
private setHistoryNavigationEnablement!: (enabled: boolean) => void;
private inputModel: ITextModel | undefined;
private inputEditorHasText: IContextKey<boolean>;
private providerId: string | undefined;

public readonly inputUri = URI.parse(`${InteractiveSessionInputPart.INPUT_SCHEME}:input-${InteractiveSessionInputPart._counter++}`);

constructor(
private readonly providerId: string,
// private readonly editorOptions: InteractiveSessionEditorOptions, // TODO this should be used
@IInteractiveSessionWidgetHistoryService private readonly historyService: IInteractiveSessionWidgetHistoryService,
@IModelService private readonly modelService: IModelService,
Expand All @@ -79,10 +79,16 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN
super();

this.inputEditorHasText = CONTEXT_INTERACTIVE_INPUT_HAS_TEXT.bindTo(contextKeyService);
this.history = new HistoryNavigator([], 5);
this._register(this.historyService.onDidClearHistory(() => this.history.clear()));
}

const history = this.historyService.getHistory(this.providerId);
setState(providerId: string, inputValue: string): void {
this.providerId = providerId;
const history = this.historyService.getHistory(providerId);
this.history = new HistoryNavigator(history, 50);
this._register(this.historyService.onDidClearHistory(() => this.history.clear()));

this.setValue(inputValue);
}

get element(): HTMLElement {
Expand All @@ -102,15 +108,17 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN
(this.history.previous() ?? this.history.first()) : this.history.next())
?? '';

this.inputEditor.setValue(historyInput);
aria.status(historyInput);
if (historyInput) {
// always leave cursor at the end.
this.inputEditor.setPosition({ lineNumber: 1, column: historyInput.length + 1 });
}
this.setValue(historyInput);
this.setHistoryNavigationEnablement(true);
}

private setValue(value: string): void {
this.inputEditor.setValue(value);
// always leave cursor at the end
this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
}

focus() {
this._inputEditor.focus();
}
Expand Down Expand Up @@ -240,6 +248,6 @@ export class InteractiveSessionInputPart extends Disposable implements IHistoryN

saveState(): void {
const inputHistory = this.history.getHistory();
this.historyService.saveHistory(this.providerId, inputHistory);
this.historyService.saveHistory(this.providerId!, inputHistory);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { editorBackground } from 'vs/platform/theme/common/colorRegistry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane';
import { Memento } from 'vs/workbench/common/memento';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
import { IViewState, InteractiveSessionWidget } from 'vs/workbench/contrib/interactiveSession/browser/interactiveSessionWidget';
import { IInteractiveSessionService } from 'vs/workbench/contrib/interactiveSession/common/interactiveSessionService';

export interface IInteractiveSessionViewOptions {
readonly providerId: string;
Expand All @@ -27,11 +30,15 @@ export const INTERACTIVE_SIDEBAR_PANEL_ID = 'workbench.panel.interactiveSessionS
export class InteractiveSessionViewPane extends ViewPane {
static ID = 'workbench.panel.interactiveSession.view';

private _widget: InteractiveSessionWidget;
private _widget!: InteractiveSessionWidget;
get widget(): InteractiveSessionWidget { return this._widget; }

private modelDisposables = this._register(new DisposableStore());
private memento: Memento;
private viewState: IViewState;

constructor(
interactiveSessionViewOptions: IInteractiveSessionViewOptions,
private readonly interactiveSessionViewOptions: IInteractiveSessionViewOptions,
options: IViewPaneOptions,
@IKeybindingService keybindingService: IKeybindingService,
@IContextMenuService contextMenuService: IContextMenuService,
Expand All @@ -42,30 +49,52 @@ export class InteractiveSessionViewPane extends ViewPane {
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IStorageService private readonly storageService: IStorageService,
@IInteractiveSessionService private readonly interactiveSessionService: IInteractiveSessionService,
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService);
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]));

const memento = new Memento('interactive-session-' + interactiveSessionViewOptions.providerId, storageService);
this._widget = this._register(scopedInstantiationService.createInstance(InteractiveSessionWidget, interactiveSessionViewOptions.providerId, undefined, { viewId: this.id }, () => this.getBackgroundColor(), () => this.getBackgroundColor(), () => editorBackground, memento));
// View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento.
this.memento = new Memento('interactive-session-view-' + this.interactiveSessionViewOptions.providerId, this.storageService);
this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.USER) as IViewState;
}

this._register(this.onDidChangeBodyVisibility(visible => {
this._widget.setVisible(visible);
private updateModel(initial = false): void {
this.modelDisposables.clear();

const model = this.interactiveSessionService.startSession(this.interactiveSessionViewOptions.providerId, initial, CancellationToken.None);
if (!model) {
throw new Error('Could not start interactive session');
}

this._widget.setModel(model, { ...this.viewState });
this.modelDisposables.add(model.onDidDispose(() => {
this.updateModel();
}));
}

protected override renderBody(parent: HTMLElement): void {
super.renderBody(parent);

const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]));

this._widget = this._register(scopedInstantiationService.createInstance(InteractiveSessionWidget, { viewId: this.id }, () => this.getBackgroundColor(), () => this.getBackgroundColor(), () => editorBackground));
this._register(this.onDidChangeBodyVisibility(visible => {
this._widget.setVisible(visible);
}));
this._widget.render(parent);

this.updateModel(true);
}

acceptInput(query?: string): void {
this._widget.acceptInput(query);
}

async clear(): Promise<void> {
await this._widget.clear();
if (this.widget.viewModel) {
this.interactiveSessionService.clearSession(this.widget.viewModel.sessionId);
}
}

focusInput(): void {
Expand All @@ -83,7 +112,14 @@ export class InteractiveSessionViewPane extends ViewPane {
}

override saveState(): void {
// Since input history is per-provider, this is handled by a separate service and not the memento here.
// TODO multiple chat views will overwrite each other
this._widget.saveState();

const widgetViewState = this._widget.getViewState();
this.viewState.inputValue = widgetViewState.inputValue;
this.memento.saveMemento();

super.saveState();
}
}
Expand Down
Loading

0 comments on commit b21e880

Please sign in to comment.