Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the way the InteractiveSessionModel is loaded into the widget #180668

Merged
merged 2 commits into from
Apr 24, 2023
Merged
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
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