Skip to content

Commit

Permalink
Use a light weight diff for live inline chat mode (#199027)
Browse files Browse the repository at this point in the history
* wip

* add yet another live mode which is live and live preview...

* use `renderLines` and view zones for orginal contents, render accept, discard, and compare in line

* remove LiveStrategy2 again

* render insertion diff as progressive changes happen

* implement discard

* implement explicit accept via button and implict accept via typing inside changes

* merge diff changes that are somewhat near

* pass zone, not just widget to strategies

* add `IEditorDecorationsCollection#append`

* move live3 actions into widget, don't render normal accept, discard, allow strategry to trigger accept, discard flows

* cleanup

* make sure re-run works
  • Loading branch information
jrieken authored Nov 24, 2023
1 parent ac336e6 commit 5d73488
Show file tree
Hide file tree
Showing 12 changed files with 640 additions and 146 deletions.
4 changes: 2 additions & 2 deletions src/vs/base/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,11 @@ export class EmptySubmenuAction extends Action {
}
}

export function toAction(props: { id: string; label: string; enabled?: boolean; checked?: boolean; run: Function }): IAction {
export function toAction(props: { id: string; label: string; enabled?: boolean; checked?: boolean; class?: string; run: Function }): IAction {
return {
id: props.id,
label: props.label,
class: undefined,
class: props.class,
enabled: props.enabled ?? true,
checked: props.checked ?? false,
run: async (...args: unknown[]) => props.run(...args),
Expand Down
14 changes: 14 additions & 0 deletions src/vs/editor/browser/widget/codeEditorWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2312,6 +2312,20 @@ class EditorDecorationsCollection implements editorCommon.IEditorDecorationsColl
}
return this._decorationIds;
}

public append(newDecorations: readonly IModelDeltaDecoration[]): string[] {
let newDecorationIds: string[] = [];
try {
this._isChangingDecorations = true;
this._editor.changeDecorations((accessor) => {
newDecorationIds = accessor.deltaDecorations([], newDecorations);
this._decorationIds = this._decorationIds.concat(newDecorationIds);
});
} finally {
this._isChangingDecorations = false;
}
return newDecorationIds;
}
}

const squigglyStart = encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 6 3' enable-background='new 0 0 6 3' height='3' width='6'><g fill='`);
Expand Down
4 changes: 4 additions & 0 deletions src/vs/editor/common/editorCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,10 @@ export interface IEditorDecorationsCollection {
* Replace all previous decorations with `newDecorations`.
*/
set(newDecorations: readonly IModelDeltaDecoration[]): string[];
/**
* Append `newDecorations` to this collection.
*/
append(newDecorations: readonly IModelDeltaDecoration[]): string[];
/**
* Remove all previous decorations.
*/
Expand Down
4 changes: 4 additions & 0 deletions src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2781,6 +2781,10 @@ declare namespace monaco.editor {
* Replace all previous decorations with `newDecorations`.
*/
set(newDecorations: readonly IModelDeltaDecoration[]): string[];
/**
* Append `newDecorations` to this collection.
*/
append(newDecorations: readonly IModelDeltaDecoration[]): string[];
/**
* Remove all previous decorations.
*/
Expand Down
169 changes: 102 additions & 67 deletions src/vs/platform/actions/browser/buttonbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
*--------------------------------------------------------------------------------------------*/

import { ButtonBar, IButton } from 'vs/base/browser/ui/button/button';
import { ActionRunner, IAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
import { ActionRunner, IAction, IActionRunner, SubmenuAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { localize } from 'vs/nls';
import { MenuId, IMenuService, SubmenuItemAction, MenuItemAction } from 'vs/platform/actions/common/actions';
import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
Expand All @@ -21,44 +21,120 @@ export type IButtonConfigProvider = (action: IAction) => {
isSecondary?: boolean;
} | undefined;

export interface IMenuWorkbenchButtonBarOptions {
export interface IWorkbenchButtonBarOptions {
telemetrySource?: string;
buttonConfigProvider?: IButtonConfigProvider;
}

export class MenuWorkbenchButtonBar extends ButtonBar {
export class WorkbenchButtonBar extends ButtonBar {

private readonly _store = new DisposableStore();
protected readonly _store = new DisposableStore();

private readonly _actionRunner: IActionRunner;
private readonly _onDidChange = new Emitter<this>();
readonly onDidChange: Event<this> = this._onDidChange.event;

private readonly _onDidChangeMenuItems = new Emitter<this>();
readonly onDidChangeMenuItems: Event<this> = this._onDidChangeMenuItems.event;

constructor(
container: HTMLElement,
menuId: MenuId,
options: IMenuWorkbenchButtonBarOptions | undefined,
@IMenuService menuService: IMenuService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextMenuService contextMenuService: IContextMenuService,
@IKeybindingService keybindingService: IKeybindingService,
private readonly _options: IWorkbenchButtonBarOptions | undefined,
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(container);

const menu = menuService.createMenu(menuId, contextKeyService);
this._store.add(menu);

const actionRunner = this._store.add(new ActionRunner());
if (options?.telemetrySource) {
actionRunner.onDidRun(e => {
this._actionRunner = this._store.add(new ActionRunner());
if (_options?.telemetrySource) {
this._actionRunner.onDidRun(e => {
telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>(
'workbenchActionExecuted',
{ id: e.action.id, from: options.telemetrySource! }
{ id: e.action.id, from: _options.telemetrySource! }
);
}, undefined, this._store);
}
}

override dispose() {
this._onDidChange.dispose();
this._store.dispose();
super.dispose();
}

update(actions: IAction[]): void {

const conifgProvider: IButtonConfigProvider = this._options?.buttonConfigProvider ?? (() => ({ showLabel: true }));

this.clear();

for (let i = 0; i < actions.length; i++) {

const secondary = i > 0;
const actionOrSubmenu = actions[i];
let action: IAction;
let btn: IButton;

if (actionOrSubmenu instanceof SubmenuAction && actionOrSubmenu.actions.length > 0) {
const [first, ...rest] = actionOrSubmenu.actions;
action = <MenuItemAction>first;
btn = this.addButtonWithDropdown({
secondary: conifgProvider(action)?.isSecondary ?? secondary,
actionRunner: this._actionRunner,
actions: rest,
contextMenuProvider: this._contextMenuService,
});
} else {
action = actionOrSubmenu;
btn = this.addButton({
secondary: conifgProvider(action)?.isSecondary ?? secondary,
});
}

btn.enabled = action.enabled;
btn.element.classList.add('default-colors');
if (conifgProvider(action)?.showLabel ?? true) {
btn.label = action.label;
} else {
btn.element.classList.add('monaco-text-button');
}
if (conifgProvider(action)?.showIcon) {
if (action instanceof MenuItemAction && ThemeIcon.isThemeIcon(action.item.icon)) {
btn.icon = action.item.icon;
} else if (action.class) {
btn.element.classList.add(...action.class.split(' '));
}
}
const kb = this._keybindingService.lookupKeybinding(action.id);
if (kb) {
btn.element.title = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel());
} else {
btn.element.title = action.label;

}
btn.onDidClick(async () => {
this._actionRunner.run(action);
});
}
this._onDidChange.fire(this);
}
}

export class MenuWorkbenchButtonBar extends WorkbenchButtonBar {

const conifgProvider: IButtonConfigProvider = options?.buttonConfigProvider ?? (() => ({ showLabel: true }));
constructor(
container: HTMLElement,
menuId: MenuId,
options: IWorkbenchButtonBarOptions | undefined,
@IMenuService menuService: IMenuService,
@IContextKeyService contextKeyService: IContextKeyService,
@IContextMenuService contextMenuService: IContextMenuService,
@IKeybindingService keybindingService: IKeybindingService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(container, options, contextMenuService, keybindingService, telemetryService);

const menu = menuService.createMenu(menuId, contextKeyService);
this._store.add(menu);

const update = () => {

Expand All @@ -68,59 +144,18 @@ export class MenuWorkbenchButtonBar extends ButtonBar {
.getActions({ renderShortTitle: true })
.flatMap(entry => entry[1]);

for (let i = 0; i < actions.length; i++) {

const secondary = i > 0;
const actionOrSubmenu = actions[i];
let action: MenuItemAction | SubmenuItemAction;
let btn: IButton;

if (actionOrSubmenu instanceof SubmenuItemAction && actionOrSubmenu.actions.length > 0) {
const [first, ...rest] = actionOrSubmenu.actions;
action = <MenuItemAction>first;
btn = this.addButtonWithDropdown({
secondary: conifgProvider(action)?.isSecondary ?? secondary,
actionRunner,
actions: rest,
contextMenuProvider: contextMenuService,
});
} else {
action = actionOrSubmenu;
btn = this.addButton({
secondary: conifgProvider(action)?.isSecondary ?? secondary,
});
}

btn.enabled = action.enabled;
btn.element.classList.add('default-colors');
if (conifgProvider(action)?.showLabel ?? true) {
btn.label = action.label;
} else {
btn.element.classList.add('monaco-text-button');
}
if (conifgProvider(action)?.showIcon && ThemeIcon.isThemeIcon(action.item.icon)) {
btn.icon = action.item.icon;
}
const kb = keybindingService.lookupKeybinding(action.id);
if (kb) {
btn.element.title = localize('labelWithKeybinding', "{0} ({1})", action.label, kb.getLabel());
} else {
btn.element.title = action.label;
super.update(actions);

}
btn.onDidClick(async () => {
actionRunner.run(action);
});
}
this._onDidChangeMenuItems.fire(this);
};
this._store.add(menu.onDidChange(update));
update();
}

override dispose() {
this._onDidChangeMenuItems.dispose();
this._store.dispose();
super.dispose();
}

override update(_actions: IAction[]): void {
throw new Error('Use Menu or WorkbenchButtonBar');
}
}
27 changes: 27 additions & 0 deletions src/vs/workbench/contrib/inlineChat/browser/inlineChat.css
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,35 @@
display: none;
}

.monaco-editor .inline-chat-toolbar {
display: flex;
}

.monaco-editor .inline-chat-toolbar > .monaco-button{
margin-right: 6px;
}

.monaco-editor .inline-chat-toolbar .action-label.checked {
color: var(--vscode-inputOption-activeForeground);
background-color: var(--vscode-inputOption-activeBackground);
outline: 1px solid var(--vscode-inputOption-activeBorder);
}

/* decoration styles */

.monaco-editor .inline-chat-inserted-range {
background-color: var(--vscode-diffEditor-insertedTextBackground);
}

.monaco-editor .inline-chat-inserted-range-linehighlight {
background-color: var(--vscode-diffEditor-insertedLineBackground);
}

.monaco-editor .inline-chat-original-zone2 {
background-color: var(--vscode-diffEditor-removedLineBackground);
opacity: 0.8;
}

.monaco-editor .inline-chat-lines-deleted-range-inline {
text-decoration: line-through;
background-color: var(--vscode-diffEditor-removedTextBackground);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, {
icon: Codicon.discard,
group: '0_main',
order: 2,
when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages)),
when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Live3), CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages)),
rememberDefaultAction: true
});

Expand Down Expand Up @@ -513,7 +513,7 @@ export class ApplyPreviewEdits extends AbstractInlineChatAction {
when: CTX_INLINE_CHAT_USER_DID_EDIT
}],
menu: {
when: CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages),
when: ContextKeyExpr.and(CTX_INLINE_CHAT_RESPONSE_TYPES.notEqualsTo(InlineChateResponseTypes.OnlyMessages), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Live3)),
id: MENU_INLINE_CHAT_WIDGET_STATUS,
group: '0_main',
order: 0
Expand Down
Loading

0 comments on commit 5d73488

Please sign in to comment.