Merge pull request #179960 from microsoft/aiday/addLinkToJumpToInteractiveWindow

Add link to jump to interactive window
This commit is contained in:
Johannes Rieken 2023-04-18 16:43:55 +02:00 committed by GitHub
commit 196a5dac94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 81 additions and 15 deletions

View file

@ -83,7 +83,7 @@
}
/* UGLY - fighting against workbench styles */
.monaco-workbench .part.editor > .content .monaco-editor .interactive-editor .progress .monaco-progress-container {
.monaco-workbench .part.editor>.content .monaco-editor .interactive-editor .progress .monaco-progress-container {
top: 0;
}
@ -104,8 +104,33 @@
display: none;
}
.monaco-editor .interactive-editor .status .label {
.monaco-editor .interactive-editor .status .link {
color: var(--vscode-textLink-foreground);
cursor: pointer;
font-size: 12px;
padding-left: 10px;
white-space: nowrap;
margin-top: auto;
display: flex;
}
.monaco-editor .interactive-editor .status .label {
overflow: hidden;
font-size: 12px;
}
.monaco-editor .interactive-editor .status .label.message>span>p {
margin: 0px;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
display: -webkit-box;
}
.monaco-editor .interactive-editor .status .link .codicon {
line-height: unset;
font-size: 12px;
padding-right: 4px;
}
.monaco-editor .interactive-editor .status .label A {

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
import { raceCancellationError } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
@ -503,7 +504,7 @@ export class InteractiveEditorController implements IEditorContribution {
if (!isCancellationError(e)) {
this._logService.error('[IE] ERROR during request', provider.debugName);
this._logService.error(e);
this._zone.widget.updateMessage(toErrorMessage(e), ['error']);
this._zone.widget.updateMessage(toErrorMessage(e), { classes: ['error'] });
// statusWidget
continue;
}
@ -521,7 +522,7 @@ export class InteractiveEditorController implements IEditorContribution {
if (!reply) {
this._logService.trace('[IE] NO reply or edits', provider.debugName);
this._zone.widget.updateMessage(localize('empty', "No results, please refine your input and try again."), ['warn']);
this._zone.widget.updateMessage(localize('empty', "No results, please refine your input and try again."), { classes: ['warn'] });
continue;
}
@ -530,7 +531,9 @@ export class InteractiveEditorController implements IEditorContribution {
if (reply.type === 'message') {
this._logService.info('[IE] received a MESSAGE, continuing outside editor', provider.debugName);
this._instaService.invokeFunction(showMessageResponse, request.prompt, reply.message.value);
const messageReply = reply.message;
const renderedMarkdown = renderMarkdown(messageReply, { inline: true });
this._zone.widget.updateMessage(renderedMarkdown.element, { linkListener: () => this._instaService.invokeFunction(showMessageResponse, request.prompt, messageReply.value), isMessageReply: true });
continue;
}
@ -730,7 +733,7 @@ export class InteractiveEditorController implements IEditorContribution {
const kind = helpful ? InteractiveEditorResponseFeedbackKind.Helpful : InteractiveEditorResponseFeedbackKind.Unhelpful;
this._lastEditState.provider.handleInteractiveEditorResponseFeedback?.(this._lastEditState.session, this._lastEditState.response.raw, kind);
this._ctxLastFeedbackKind.set(helpful ? 'helpful' : 'unhelpful');
this._zone.widget.updateMessage('Thank you for your feedback!', undefined, 1250);
this._zone.widget.updateMessage('Thank you for your feedback!', { resetAfter: 1250 });
}
}

View file

@ -5,7 +5,7 @@
import 'vs/css!./interactiveEditor';
import { CancellationToken } from 'vs/base/common/cancellation';
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IRange, Range } from 'vs/editor/common/core/range';
@ -16,8 +16,8 @@ import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { assertType } from 'vs/base/common/types';
import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST, CTX_INTERACTIVE_EDITOR_EMPTY, CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_STATUS } from 'vs/workbench/contrib/interactiveEditor/common/interactiveEditor';
import { ITextModel } from 'vs/editor/common/model';
import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';
import { Event, MicrotaskEmitter } from 'vs/base/common/event';
import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset, append } from 'vs/base/browser/dom';
import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
@ -37,6 +37,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation';
import { ILanguageSelection } from 'vs/editor/common/languages/language';
import { ResourceLabel } from 'vs/workbench/browser/labels';
import { FileKind } from 'vs/platform/files/common/files';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
const _inputEditorOptions: IEditorConstructionOptions = {
padding: { top: 3, bottom: 2 },
@ -93,6 +94,30 @@ const _previewEditorEditorOptions: IDiffEditorConstructionOptions = {
readOnly: true,
};
class StatusLink extends Disposable {
private readonly _domNode: HTMLAnchorElement;
private readonly _onClicked = this._register(new Emitter<void>());
readonly onClicked = this._onClicked.event;
constructor(container: HTMLElement) {
super();
const linkNode = document.createElement('a');
const codicon = renderLabelWithIcons('$(comment-discussion)' + localize('viewInChat', 'View in Chat'));
reset(linkNode, ...codicon);
this._domNode = append(container, linkNode);
this._register(addDisposableListener(this._domNode, 'click', () => this._onClicked.fire()));
}
show(): void {
this._domNode.style.display = 'flex';
}
hide(): void {
this._domNode.style.display = 'none';
}
}
class InteractiveEditorWidget {
private static _modelPool: number = 1;
@ -117,7 +142,8 @@ class InteractiveEditorWidget {
h('div.previewCreate.hidden@previewCreate'),
h('div.status@status', [
h('div.actions.hidden@statusToolbar'),
h('div.label@statusLabel'),
h('span.label@statusLabel'),
h('span.link@statusLink'),
]),
]
);
@ -147,6 +173,9 @@ class InteractiveEditorWidget {
public acceptInput: () => void = InteractiveEditorWidget._noop;
private _cancelInput: () => void = InteractiveEditorWidget._noop;
private readonly _statusLink: StatusLink;
private readonly _statusLinkListener = this._store.add(new MutableDisposable());
constructor(
parentEditor: ICodeEditor,
@IModelService private readonly _modelService: IModelService,
@ -221,6 +250,7 @@ class InteractiveEditorWidget {
this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true }));
this._previewCreateEditor = this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor));
this._statusLink = new StatusLink(this._elements.statusLink);
}
dispose(): void {
@ -361,24 +391,32 @@ class InteractiveEditorWidget {
this._onDidChangeHeight.fire();
}
updateMessage(message: string, classes?: string[], resetAfter?: number) {
const isTempMessage = typeof resetAfter === 'number';
updateMessage(message: string | HTMLElement, ops: { linkListener?: () => void; isMessageReply?: boolean; classes?: string[]; resetAfter?: number } = {}) {
this._statusLinkListener.clear();
if (ops.isMessageReply) {
this._statusLink.show();
this._statusLinkListener.value = this._statusLink.onClicked(() => ops.linkListener?.());
} else {
this._statusLink.hide();
}
const isTempMessage = typeof ops.resetAfter === 'number';
if (isTempMessage && !this._elements.statusLabel.dataset['state']) {
const messageNow = this._elements.statusLabel.innerText;
const classes = Array.from(this._elements.statusLabel.classList.values());
setTimeout(() => {
if (messageNow) {
this.updateMessage(messageNow, classes);
this.updateMessage(messageNow, { ...ops, classes });
} else {
reset(this._elements.statusLabel);
}
}, resetAfter);
}, ops.resetAfter);
}
this._elements.status.classList.toggle('hidden', false);
reset(this._elements.statusLabel, message);
this._elements.statusLabel.className = `label ${(classes ?? []).join(' ')}`;
this._elements.statusLabel.className = `label ${(ops.classes ?? []).join(' ')}`;
this._elements.statusLabel.classList.toggle('message', ops.isMessageReply);
if (isTempMessage) {
this._elements.statusLabel.dataset['state'] = 'temp';
} else {