Merge pull request #175849 from microsoft/joh/statistical-gazelle

interactive editor work
This commit is contained in:
Johannes Rieken 2023-03-02 11:01:05 +01:00 committed by GitHub
commit 635055c07e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1559 additions and 24 deletions

View file

@ -131,13 +131,13 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
return this._workerManager.withWorker().then(client => client.computeDirtyDiff(original, modified, ignoreTrimWhitespace));
}
public computeMoreMinimalEdits(resource: URI, edits: languages.TextEdit[] | null | undefined): Promise<languages.TextEdit[] | undefined> {
public computeMoreMinimalEdits(resource: URI, edits: languages.TextEdit[] | null | undefined, pretty: boolean = false): Promise<languages.TextEdit[] | undefined> {
if (isNonEmptyArray(edits)) {
if (!canSyncModel(this._modelService, resource)) {
return Promise.resolve(edits); // File too large
}
const sw = StopWatch.create(true);
const result = this._workerManager.withWorker().then(client => client.computeMoreMinimalEdits(resource, edits));
const result = this._workerManager.withWorker().then(client => client.computeMoreMinimalEdits(resource, edits, pretty));
result.finally(() => this._logService.trace('FORMAT#computeMoreMinimalEdits', resource.toString(true), sw.elapsed()));
return Promise.race([result, timeout(1000).then(() => edits)]);
@ -528,9 +528,9 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien
});
}
public computeMoreMinimalEdits(resource: URI, edits: languages.TextEdit[]): Promise<languages.TextEdit[]> {
public computeMoreMinimalEdits(resource: URI, edits: languages.TextEdit[], pretty: boolean): Promise<languages.TextEdit[]> {
return this._withSyncedResources([resource]).then(proxy => {
return proxy.computeMoreMinimalEdits(resource.toString(), edits);
return proxy.computeMoreMinimalEdits(resource.toString(), edits, pretty);
});
}

View file

@ -99,7 +99,7 @@ export class BlockDecorations extends ViewPart {
bottom = ctx.getVerticalOffsetAfterLineNumber(decoration.range.endLineNumber, true);
} else {
top = ctx.getVerticalOffsetForLineNumber(decoration.range.startLineNumber, true);
bottom = decoration.range.isEmpty()
bottom = decoration.range.isEmpty() && !decoration.options.blockDoesNotCollapse
? ctx.getVerticalOffsetForLineNumber(decoration.range.startLineNumber, false)
: ctx.getVerticalOffsetAfterLineNumber(decoration.range.endLineNumber, true);
}

View file

@ -6,7 +6,7 @@
import * as objects from 'vs/base/common/objects';
import { ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget';
import { ConfigurationChangedEvent, IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { ICommandService } from 'vs/platform/commands/common/commands';
@ -29,6 +29,7 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget {
constructor(
domElement: HTMLElement,
options: IEditorOptions,
codeEditorWidgetOptions: ICodeEditorWidgetOptions,
parentEditor: ICodeEditor,
@IInstantiationService instantiationService: IInstantiationService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@ -40,7 +41,7 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget {
@ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService,
@ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService,
) {
super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService);
super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, codeEditorWidgetOptions, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService, languageConfigurationService, languageFeaturesService);
this._parentEditor = parentEditor;
this._overwriteOptions = options;

View file

@ -99,6 +99,7 @@ export interface IModelDecorationOptions {
* In this case, the range must be empty and set to the last line.
*/
blockIsAfterEnd?: boolean | null;
blockDoesNotCollapse?: boolean | null;
blockPadding?: [top: number, right: number, bottom: number, left: number] | null;
/**

View file

@ -2248,6 +2248,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
readonly description: string;
readonly blockClassName: string | null;
readonly blockIsAfterEnd: boolean | null;
readonly blockDoesNotCollapse?: boolean | null;
readonly blockPadding: [top: number, right: number, bottom: number, left: number] | null;
readonly stickiness: model.TrackedRangeStickiness;
readonly zIndex: number;
@ -2275,6 +2276,7 @@ export class ModelDecorationOptions implements model.IModelDecorationOptions {
private constructor(options: model.IModelDecorationOptions) {
this.description = options.description;
this.blockClassName = options.blockClassName ? cleanClassName(options.blockClassName) : null;
this.blockDoesNotCollapse = options.blockDoesNotCollapse ?? null;
this.blockIsAfterEnd = options.blockIsAfterEnd ?? null;
this.blockPadding = options.blockPadding ?? null;
this.stickiness = options.stickiness || model.TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges;

View file

@ -461,7 +461,7 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
private static readonly _diffLimit = 100000;
public async computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[]): Promise<TextEdit[]> {
public async computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {
const model = this._getModel(modelUrl);
if (!model) {
return edits;
@ -506,7 +506,7 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable {
}
// compute diff between original and edit.text
const changes = stringDiff(original, text, false);
const changes = stringDiff(original, text, pretty);
const editOffset = model.offsetAt(Range.lift(range).getStartPosition());
for (const change of changes) {

View file

@ -28,7 +28,7 @@ export interface IEditorWorkerService {
canComputeDirtyDiff(original: URI, modified: URI): boolean;
computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise<IChange[] | null>;
computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined): Promise<TextEdit[] | undefined>;
computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean): Promise<TextEdit[] | undefined>;
canComputeWordRanges(resource: URI): boolean;
computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null>;

View file

@ -92,7 +92,7 @@ export abstract class SymbolNavigationAction extends EditorAction2 {
}
}
}
return result;
return <typeof opts>result;
}
readonly configuration: SymbolNavigationActionConfig;

View file

@ -313,7 +313,7 @@ export class ReferenceWidget extends peekView.PeekViewWidget {
enabled: false
}
};
this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, this.editor);
this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, {}, this.editor);
dom.hide(this._previewContainer);
this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), PLAINTEXT_LANGUAGE_ID, TextModel.DEFAULT_CREATION_OPTIONS, null, this._undoRedoService, this._languageService, this._languageConfigurationService);

View file

@ -710,7 +710,7 @@ export class SynchronizedInlineCompletionsCache extends Disposable {
for (const c of this.completions) {
const newRange = model.getDecorationRange(c.decorationId);
if (!newRange) {
onUnexpectedError(new Error('Decoration has no range'));
// onUnexpectedError(new Error('Decoration has no range'));
continue;
}
if (!c.synchronizedRange.equalsRange(newRange)) {

View file

@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { InteractiveEditorController } from 'vs/editor/contrib/interactive/browser/interactiveEditorWidget';
import * as interactiveEditorActions from 'vs/editor/contrib/interactive/browser/interactiveEditorActions';
import { IInteractiveEditorService } from 'vs/editor/contrib/interactive/common/interactiveEditor';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { InteractiveEditorServiceImpl } from 'vs/editor/contrib/interactive/common/interactiveEditorServiceImpl';
registerSingleton(IInteractiveEditorService, InteractiveEditorServiceImpl, InstantiationType.Delayed);
registerEditorContribution(InteractiveEditorController.ID, InteractiveEditorController, EditorContributionInstantiation.Lazy);
registerAction2(interactiveEditorActions.StartSessionAction);
registerAction2(interactiveEditorActions.ToggleHistory);
registerAction2(interactiveEditorActions.MakeRequestAction);
registerAction2(interactiveEditorActions.StopRequestAction);
registerAction2(interactiveEditorActions.AcceptWithPreviewInteractiveEditorAction);
registerAction2(interactiveEditorActions.TogglePreviewMode);
registerAction2(interactiveEditorActions.CancelSessionAction);
registerAction2(interactiveEditorActions.ArrowOutUpAction);
registerAction2(interactiveEditorActions.ArrowOutDownAction);
registerAction2(interactiveEditorActions.FocusInteractiveEditor);
registerAction2(interactiveEditorActions.PreviousFromHistory);
registerAction2(interactiveEditorActions.NextFromHistory);
registerAction2(interactiveEditorActions.UndoCommand);

View file

@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .interactive-editor {
z-index: 100;
color: inherit;
padding: 6px 0 6px 6px;
}
/* body */
.monaco-editor .interactive-editor .body {
display: flex;
}
.monaco-editor .interactive-editor .body .expando {
flex-grow: 0;
padding-top: 6px;
padding-right: 2px;
cursor: pointer;
}
.monaco-editor .interactive-editor .body .history {
display: block;
margin: 0;
color: var(--vscode-descriptionForeground);
list-style-type: disc;
line-height: 20px;
}
.monaco-editor .interactive-editor .body .history:not(:empty) {
margin: 0;
padding: 4px 0 0 12px;
}
.monaco-editor .interactive-editor .body .history.hidden {
display: none;
}
.monaco-editor .interactive-editor .body.preview {
padding: 4px 4px 0 4px;
}
.monaco-editor .interactive-editor .body .content .input {
border-radius: 2px;
box-sizing: border-box;
padding: 2px;
background-color: var(--vscode-input-background);
border: 1px solid var(--vscode-settings-textInputBorder);
cursor: text;
}
.monaco-editor .interactive-editor .monaco-editor-background {
background-color: var(--vscode-input-background);
}
.monaco-editor .interactive-editor .body .content .input.synthetic-focus {
outline: 1px solid var(--vscode-focusBorder);
}
.monaco-editor .interactive-editor .body .content .input .editor-placeholder {
position: absolute;
z-index: 1;
padding: 3px 0 0 2px;
color: var(--vscode-input-placeholderForeground);
font-family: var(--monaco-monospace-font);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.monaco-editor .interactive-editor .body .content .input .editor-placeholder.hidden {
display: none;
}
.monaco-editor .interactive-editor .body .content .input .editor-container {
/* height: 24px; */
vertical-align: middle;
}
.monaco-editor .interactive-editor .body .toolbar {
display: flex;
flex-direction: column;
align-self: start;
padding: 4px 8px 0 2px;
}
.monaco-editor .interactive-editor .body .toolbar .actions-container {
display: flex;
flex-direction: row;
gap: 4px;
}
/* progress bit */
.monaco-editor .interactive-editor .progress {
position: relative;
width: calc(100% - 18px);
left: 19px;
}
/* UGLY - fighting against workbench styles */
.monaco-workbench .part.editor > .content .monaco-editor .interactive-editor .progress .monaco-progress-container {
top: 0;
}
/* decoration styles */
.monaco-editor .interactive-editor-lines-deleted-range-inline {
text-decoration: line-through;
background-color: var(--vscode-diffEditor-removedTextBackground);
opacity: 0.6;
}
.monaco-editor .interactive-editor-lines-inserted-range {
background-color: var(--vscode-diffEditor-insertedTextBackground);
}
.monaco-editor .interactive-editor-block {
border-radius: 4px;
border: 1px solid var(--vscode-widget-border);
box-shadow: 0 0 8px 1px var(--vscode-widget-shadow);
}

View file

@ -0,0 +1,328 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from 'vs/base/common/codicons';
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction2 } from 'vs/editor/browser/editorExtensions';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { InteractiveEditorController } from 'vs/editor/contrib/interactive/browser/interactiveEditorWidget';
import { CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, 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_PREVIEW, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, MENU_INTERACTIVE_EDITOR_WIDGET_LHS, CTX_INTERACTIVE_EDITOR_HISTORY_VISIBLE } from 'vs/editor/contrib/interactive/common/interactiveEditor';
import { localize } from 'vs/nls';
import { IAction2Options } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
export class StartSessionAction extends EditorAction2 {
constructor() {
super({
id: 'interactiveEditor.start',
title: { value: localize('run', 'Start Session'), original: 'Start Session' },
category: AbstractInteractiveEditorAction.category,
f1: true,
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, EditorContextKeys.writable, CTX_INTERACTIVE_EDITOR_VISIBLE.negate()),
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI)
}
});
}
override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) {
InteractiveEditorController.get(editor)?.run();
}
}
abstract class AbstractInteractiveEditorAction extends EditorAction2 {
static readonly category = { value: localize('cat', 'Interactive Editor'), original: 'Interactive Editor' };
constructor(desc: IAction2Options) {
super({
...desc,
category: AbstractInteractiveEditorAction.category,
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_HAS_PROVIDER, desc.precondition)
});
}
override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) {
if (editor instanceof EmbeddedCodeEditorWidget) {
editor = editor.getParentEditor();
}
const ctrl = InteractiveEditorController.get(editor);
if (!ctrl) {
return;
}
this.runInteractiveEditorCommand(accessor, ctrl, editor, ..._args);
}
abstract runInteractiveEditorCommand(accessor: ServicesAccessor, ctrl: InteractiveEditorController, editor: ICodeEditor, ...args: any[]): void;
}
export class MakeRequestAction extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.accept',
title: localize('accept', 'Make Request'),
icon: Codicon.arrowUp,
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_EMPTY.negate()),
keybinding: {
when: CTX_INTERACTIVE_EDITOR_FOCUSED,
weight: KeybindingWeight.EditorCore + 7,
primary: KeyCode.Enter
},
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET,
group: 'main',
order: 1,
when: CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST.isEqualTo(false)
}
});
}
runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.accept();
}
}
export class StopRequestAction extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.stop',
title: localize('stop', 'Stop Request'),
icon: Codicon.debugStop,
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_EMPTY.negate(), CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST),
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET,
group: 'main',
order: 1,
when: CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST
},
keybinding: {
weight: KeybindingWeight.EditorContrib,
primary: KeyCode.Escape
}
});
}
runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.cancelCurrentRequest();
}
}
export class AcceptWithPreviewInteractiveEditorAction extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.acceptWithPreview',
title: localize('acceptPreview', 'Ask Question & Preview Reply'),
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_EMPTY.negate()),
keybinding: {
when: CTX_INTERACTIVE_EDITOR_FOCUSED,
weight: KeybindingWeight.EditorCore + 7,
primary: KeyMod.Shift + KeyCode.Enter
}
});
}
runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.accept(true);
}
}
export class TogglePreviewMode extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.togglePreview',
title: localize('togglePreview', 'Inline Preview'),
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE),
toggled: CTX_INTERACTIVE_EDITOR_PREVIEW,
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET,
group: 'C',
order: 1
}
});
}
runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.togglePreview();
}
}
export class CancelSessionAction extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.cancel',
title: localize('cancel', 'Cancel'),
precondition: CTX_INTERACTIVE_EDITOR_VISIBLE,
keybinding: {
weight: KeybindingWeight.EditorContrib - 1,
primary: KeyCode.Escape
}
});
}
runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.cancelSession();
}
}
export class ArrowOutUpAction extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.arrowOutUp',
title: localize('arrowUp', 'Cursor Up'),
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST),
keybinding: {
weight: KeybindingWeight.EditorCore,
primary: KeyCode.UpArrow
}
});
}
runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.arrowOut(true);
}
}
export class ArrowOutDownAction extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.arrowOutDown',
title: localize('arrowDown', 'Cursor Down'),
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST),
keybinding: {
weight: KeybindingWeight.EditorCore,
primary: KeyCode.DownArrow
}
});
}
runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.arrowOut(false);
}
}
export class FocusInteractiveEditor extends EditorAction2 {
constructor() {
super({
id: 'interactiveEditor.focus',
title: localize('focus', 'Focus'),
category: AbstractInteractiveEditorAction.category,
precondition: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CTX_INTERACTIVE_EDITOR_VISIBLE, CTX_INTERACTIVE_EDITOR_FOCUSED.negate()),
keybinding: [{
weight: KeybindingWeight.EditorCore + 10, // win against core_command
when: CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION.isEqualTo('above'),
primary: KeyCode.DownArrow,
}, {
weight: KeybindingWeight.EditorCore + 10, // win against core_command
when: CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION.isEqualTo('below'),
primary: KeyCode.UpArrow,
}]
});
}
override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) {
InteractiveEditorController.get(editor)?.focus();
}
}
export class PreviousFromHistory extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.previousFromHistory',
title: localize('previousFromHistory', 'Previous From History'),
precondition: CTX_INTERACTIVE_EDITOR_FOCUSED,
keybinding: {
weight: KeybindingWeight.EditorCore + 10, // win against core_command
primary: KeyMod.CtrlCmd | KeyCode.UpArrow,
}
});
}
override runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.populateHistory(true);
}
}
export class NextFromHistory extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.nextFromHistory',
title: localize('nextFromHistory', 'Next From History'),
precondition: CTX_INTERACTIVE_EDITOR_FOCUSED,
keybinding: {
weight: KeybindingWeight.EditorCore + 10, // win against core_command
primary: KeyMod.CtrlCmd | KeyCode.DownArrow,
}
});
}
override runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.populateHistory(false);
}
}
export class UndoCommand extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.undo',
title: localize('undo', 'Undo'),
icon: Codicon.commentDiscussion,
precondition: ContextKeyExpr.and(CTX_INTERACTIVE_EDITOR_VISIBLE),
keybinding: {
weight: KeybindingWeight.EditorContrib + 10,
primary: KeyMod.CtrlCmd | KeyCode.KeyZ,
},
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET,
group: 'B',
order: 1
}
});
}
override runInteractiveEditorCommand(_accessor: ServicesAccessor, _ctrl: InteractiveEditorController, editor: ICodeEditor, ..._args: any[]): void {
editor.getModel()?.undo();
}
}
export class ToggleHistory extends AbstractInteractiveEditorAction {
constructor() {
super({
id: 'interactiveEditor.toggleHistory',
title: localize('toggleHistory', 'Toggle History'),
icon: Codicon.chevronRight,
precondition: CTX_INTERACTIVE_EDITOR_VISIBLE,
toggled: {
condition: CTX_INTERACTIVE_EDITOR_HISTORY_VISIBLE,
icon: Codicon.chevronDown,
},
menu: {
id: MENU_INTERACTIVE_EDITOR_WIDGET_LHS,
group: 'main',
order: 1
}
});
}
override runInteractiveEditorCommand(_accessor: ServicesAccessor, ctrl: InteractiveEditorController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.toggleHistory();
}
}

View file

@ -0,0 +1,744 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./interactiveEditor';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions';
import { IRange, Range } from 'vs/editor/common/core/range';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { assertType } from 'vs/base/common/types';
import { IInteractiveEditorResponse, IInteractiveEditorService, CTX_INTERACTIVE_EDITOR_FOCUSED, CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST, 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_PREVIEW, CTX_INTERACTIVE_EDITOR_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET, CTX_INTERACTIVE_EDITOR_HISTORY_VISIBLE, MENU_INTERACTIVE_EDITOR_WIDGET_LHS } from 'vs/editor/contrib/interactive/common/interactiveEditor';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Iterable } from 'vs/base/common/iterator';
import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { Dimension, addDisposableListener, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';
import { Emitter, Event } from 'vs/base/common/event';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { IModelService } from 'vs/editor/common/services/model';
import { URI } from 'vs/base/common/uri';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController';
import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { Position } from 'vs/editor/common/core/position';
import { Selection } from 'vs/editor/common/core/selection';
import { raceCancellationError } from 'vs/base/common/async';
import { isCancellationError } from 'vs/base/common/errors';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { ILogService } from 'vs/platform/log/common/log';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { StopWatch } from 'vs/base/common/stopwatch';
class InteractiveEditorWidget {
private static _noop = () => { };
private readonly _elements = h(
'div.interactive-editor@root',
[
h('div.body', [
h('div.toolbar@lhsToolbar'),
h('div.content', [
h('div.input@input', [
h('div.editor-placeholder@placeholder'),
h('div.editor-container@editor'),
]),
h('ol.history.hidden@history'),
]),
h('div.toolbar@rhsToolbar'),
]),
h('div.progress@progress')
]
);
private readonly _store = new DisposableStore();
private readonly _inputEditor: ICodeEditor;
private readonly _inputModel: ITextModel;
private readonly _ctxInputEmpty: IContextKey<boolean>;
private readonly _ctxHistoryVisible: IContextKey<boolean>;
private readonly _progressBar: ProgressBar;
private readonly _onDidChangeHeight = new Emitter<void>();
readonly onDidChangeHeight: Event<void> = this._onDidChangeHeight.event;
private _isExpanded = false;
private _editorDim: Dimension | undefined;
public acceptInput: (preview: boolean) => void = InteractiveEditorWidget._noop;
private _cancelInput: () => void = InteractiveEditorWidget._noop;
constructor(
parentEditor: ICodeEditor | undefined,
@IModelService private readonly _modelService: IModelService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
this._ctxHistoryVisible = CTX_INTERACTIVE_EDITOR_HISTORY_VISIBLE.bindTo(this._contextKeyService);
// editor logic
const editorOptions: IEditorConstructionOptions = {
ariaLabel: localize('aria-label', "Interactive Editor Input"),
wordWrap: 'on',
overviewRulerLanes: 0,
glyphMargin: false,
lineNumbers: 'off',
folding: false,
selectOnLineNumbers: false,
hideCursorInOverviewRuler: true,
selectionHighlight: false,
scrollbar: {
useShadows: false,
vertical: 'hidden',
horizontal: 'auto',
// alwaysConsumeMouseWheel: false
},
lineDecorationsWidth: 0,
overviewRulerBorder: false,
scrollBeyondLastLine: false,
renderLineHighlight: 'none',
fixedOverflowWidgets: true,
dragAndDrop: false,
revealHorizontalRightPadding: 5,
minimap: { enabled: false },
guides: { indentation: false },
cursorWidth: 2,
wrappingStrategy: 'advanced',
wrappingIndent: 'none',
padding: { top: 3, bottom: 2 },
renderWhitespace: 'none',
dropIntoEditor: { enabled: true },
quickSuggestions: false,
suggest: {
showIcons: false,
showSnippets: false,
}
};
const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {
isSimpleWidget: true,
contributions: EditorExtensionsRegistry.getSomeEditorContributions([
SnippetController2.ID,
GhostTextController.ID,
SuggestController.ID
])
};
this._inputEditor = parentEditor
? this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, editorOptions, codeEditorWidgetOptions, parentEditor)
: this._instantiationService.createInstance(CodeEditorWidget, this._elements.editor, editorOptions, codeEditorWidgetOptions);
this._store.add(this._inputEditor);
const uri = URI.from({ scheme: 'vscode', authority: 'interactive-editor', path: `/interactive-editor/model.txt` });
this._inputModel = this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri);
this._inputEditor.setModel(this._inputModel);
// show/hide placeholder depending on text model being empty
// content height
const currentContentHeight = 0;
this._ctxInputEmpty = CTX_INTERACTIVE_EDITOR_EMPTY.bindTo(this._contextKeyService);
const togglePlaceholder = () => {
const hasText = this._inputModel.getValueLength() > 0;
this._elements.placeholder.classList.toggle('hidden', hasText);
this._ctxInputEmpty.set(!hasText);
const contentHeight = this._inputEditor.getContentHeight();
if (contentHeight !== currentContentHeight && this._editorDim) {
this._editorDim = this._editorDim.with(undefined, contentHeight);
this._inputEditor.layout(this._editorDim);
this._onDidChangeHeight.fire();
}
};
this._store.add(this._inputModel.onDidChangeContent(togglePlaceholder));
togglePlaceholder();
this._store.add(addDisposableListener(this._elements.placeholder, 'click', () => this._inputEditor.focus()));
const lhsToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.lhsToolbar, MENU_INTERACTIVE_EDITOR_WIDGET_LHS, {
telemetrySource: 'interactiveEditorWidget-toolbar-lhs',
toolbarOptions: { primaryGroup: 'main' }
});
this._store.add(lhsToolbar);
const rhsToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.rhsToolbar, MENU_INTERACTIVE_EDITOR_WIDGET, {
telemetrySource: 'interactiveEditorWidget-toolbar-rhs',
toolbarOptions: { primaryGroup: 'main' }
});
this._store.add(rhsToolbar);
this._progressBar = new ProgressBar(this._elements.progress);
this._store.add(this._progressBar);
}
dispose(): void {
this._store.dispose();
this._ctxInputEmpty.reset();
this._ctxHistoryVisible.reset();
}
getDomNode(): HTMLElement {
return this._elements.root;
}
layout(dim: Dimension) {
const innerEditorWidth = Math.min(
Number.MAX_SAFE_INTEGER, // TODO@jrieken define max width?
dim.width - (getTotalWidth(this._elements.lhsToolbar) + getTotalWidth(this._elements.rhsToolbar) + 12 /* L/R-padding */)
);
const newDim = new Dimension(innerEditorWidth, this._inputEditor.getContentHeight());
if (!this._editorDim || !Dimension.equals(this._editorDim, newDim)) {
this._editorDim = newDim;
this._inputEditor.layout(this._editorDim);
this._elements.placeholder.style.width = `${innerEditorWidth - 4 /* input-padding*/}px`;
}
}
getHeight(): number {
return this._inputEditor.getContentHeight() + getTotalHeight(this._elements.history);
}
updateProgress(show: boolean) {
if (show) {
this._progressBar.infinite();
} else {
this._progressBar.stop();
}
}
getInput(placeholder: string, value: string, token: CancellationToken): Promise<{ value: string; preview: boolean } | undefined> {
this._elements.placeholder.innerText = placeholder;
this._elements.placeholder.style.fontSize = `${this._inputEditor.getOption(EditorOption.fontSize)}px`;
this._elements.placeholder.style.lineHeight = `${this._inputEditor.getOption(EditorOption.lineHeight)}px`;
this._inputModel.setValue(value);
const disposeOnDone = new DisposableStore();
disposeOnDone.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire()));
const ctxInnerCursorFirst = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST.bindTo(this._contextKeyService);
const ctxInnerCursorLast = CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST.bindTo(this._contextKeyService);
const ctxInputEditorFocused = CTX_INTERACTIVE_EDITOR_FOCUSED.bindTo(this._contextKeyService);
return new Promise<{ value: string; preview: boolean } | undefined>(resolve => {
this._cancelInput = () => {
this.acceptInput = InteractiveEditorWidget._noop;
this._cancelInput = InteractiveEditorWidget._noop;
resolve(undefined);
return true;
};
this.acceptInput = (preview) => {
const newValue = this._inputEditor.getModel()!.getValue();
if (newValue.trim().length === 0) {
// empty or whitespace only
this._cancelInput();
return;
}
this.acceptInput = InteractiveEditorWidget._noop;
this._cancelInput = InteractiveEditorWidget._noop;
resolve({ value: newValue, preview });
const entry = document.createElement('li');
entry.classList.add('history-entry');
entry.innerText = newValue;
this._elements.history.insertBefore(entry, this._elements.history.firstChild);
this._onDidChangeHeight.fire();
};
disposeOnDone.add(token.onCancellationRequested(() => this._cancelInput()));
// CONTEXT KEYS
// (1) inner cursor position (last/first line selected)
const updateInnerCursorFirstLast = () => {
if (!this._inputEditor.hasModel()) {
return;
}
const { lineNumber } = this._inputEditor.getPosition();
ctxInnerCursorFirst.set(lineNumber === 1);
ctxInnerCursorLast.set(lineNumber === this._inputEditor.getModel().getLineCount());
};
disposeOnDone.add(this._inputEditor.onDidChangeCursorPosition(updateInnerCursorFirstLast));
updateInnerCursorFirstLast();
// (2) input editor focused or not
const updateFocused = () => {
const hasFocus = this._inputEditor.hasWidgetFocus();
ctxInputEditorFocused.set(hasFocus);
this._elements.input.classList.toggle('synthetic-focus', hasFocus);
};
disposeOnDone.add(this._inputEditor.onDidFocusEditorWidget(updateFocused));
disposeOnDone.add(this._inputEditor.onDidBlurEditorWidget(updateFocused));
updateFocused();
this.focus();
}).finally(() => {
disposeOnDone.dispose();
ctxInnerCursorFirst.reset();
ctxInnerCursorLast.reset();
ctxInputEditorFocused.reset();
});
}
populateInputField(value: string) {
this._inputModel.setValue(value.trim());
this._inputEditor.setSelection(this._inputModel.getFullModelRange());
}
toggleHistory(): void {
this._isExpanded = !this._isExpanded;
this._elements.history.classList.toggle('hidden', !this._isExpanded);
this._ctxHistoryVisible.set(this._isExpanded);
this._onDidChangeHeight.fire();
}
reset() {
this._ctxInputEmpty.reset();
// empty history
this._isExpanded = false;
this._elements.history.classList.toggle('hidden', true);
this._ctxHistoryVisible.reset();
reset(this._elements.history);
}
focus() {
this._inputEditor.focus();
}
}
export class InteractiveEditorZoneWidget extends ZoneWidget {
readonly widget: InteractiveEditorWidget;
private readonly _ctxVisible: IContextKey<boolean>;
private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
constructor(
editor: ICodeEditor,
@IInstantiationService private readonly _instaService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'interactive-editor-widget', keepEditorSelection: true });
this._ctxVisible = CTX_INTERACTIVE_EDITOR_VISIBLE.bindTo(contextKeyService);
this._ctxCursorPosition = CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
this._disposables.add(toDisposable(() => {
this._ctxVisible.reset();
this._ctxCursorPosition.reset();
}));
this.widget = this._instaService.createInstance(InteractiveEditorWidget, this.editor);
this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout()));
this._disposables.add(this.widget);
this.create();
// todo@jrieken listen ONLY when showing
const updateCursorIsAboveContextKey = () => {
if (!this.position || !this.editor.hasModel()) {
this._ctxCursorPosition.reset();
} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {
this._ctxCursorPosition.set('above');
} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {
this._ctxCursorPosition.set('below');
} else {
this._ctxCursorPosition.reset();
}
};
this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));
this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey()));
updateCursorIsAboveContextKey();
}
protected override _fillContainer(container: HTMLElement): void {
container.appendChild(this.widget.getDomNode());
}
protected override _getWidth(info: EditorLayoutInfo): number {
// TODO@jrieken
// makes the zone widget wider than wanted but this aligns
// it with wholeLine decorations that are added above
return info.width;
}
private _dimension?: Dimension;
protected override _onWidth(widthInPixel: number): void {
if (this._dimension) {
this._doLayout(this._dimension.height, widthInPixel);
}
}
protected override _doLayout(heightInPixel: number, widthInPixel: number): void {
const info = this.editor.getLayoutInfo();
const spaceLeft = info.lineNumbersWidth + info.glyphMarginWidth + info.decorationsWidth;
const spaceRight = info.minimap.minimapWidth + info.verticalScrollbarWidth;
const width = widthInPixel - (spaceLeft + spaceRight);
this._dimension = new Dimension(width, heightInPixel);
this.widget.getDomNode().style.marginLeft = `${spaceLeft}px`;
this.widget.getDomNode().style.marginRight = `${spaceRight}px`;
this.widget.layout(this._dimension);
}
private _computeHeightInLines(): number {
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
const contentHeightInLines = (this.widget.getHeight() / lineHeight);
return 2 + contentHeightInLines;
}
protected override _relayout() {
super._relayout(this._computeHeightInLines());
}
async getInput(where: IRange, placeholder: string, value: string, token: CancellationToken): Promise<{ value: string; preview: boolean } | undefined> {
assertType(this.editor.hasModel());
super.show(where, this._computeHeightInLines());
this._ctxVisible.set(true);
const task = this.widget.getInput(placeholder, value, token);
const result = await task;
return result;
}
override hide(): void {
this._ctxVisible.reset();
this._ctxCursorPosition.reset();
this.widget.reset();
super.hide();
}
}
export class InteractiveEditorController implements IEditorContribution {
static ID = 'interactiveEditor';
static get(editor: ICodeEditor) {
return editor.getContribution<InteractiveEditorController>(InteractiveEditorController.ID);
}
private static _decoBlock = ModelDecorationOptions.register({
description: 'interactive-editor',
blockClassName: 'interactive-editor-block',
blockDoesNotCollapse: true,
blockPadding: [1, 0, 1, 4]
});
private static _promptHistory: string[] = [];
private _historyOffset: number = -1;
private readonly _store = new DisposableStore();
private readonly _zone: InteractiveEditorZoneWidget;
private readonly _ctxShowPreview: IContextKey<boolean>;
private readonly _ctxHasActiveRequest: IContextKey<boolean>;
private _ctsSession: CancellationTokenSource = new CancellationTokenSource();
private _ctsRequest?: CancellationTokenSource;
constructor(
private readonly _editor: ICodeEditor,
@IInstantiationService instaService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IInteractiveEditorService private readonly _interactiveEditorService: IInteractiveEditorService,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@ILogService private readonly _logService: ILogService,
) {
this._zone = this._store.add(instaService.createInstance(InteractiveEditorZoneWidget, this._editor));
this._ctxShowPreview = CTX_INTERACTIVE_EDITOR_PREVIEW.bindTo(contextKeyService);
this._ctxHasActiveRequest = CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST.bindTo(contextKeyService);
}
dispose(): void {
this._store.dispose();
this._ctsSession.dispose(true);
this._ctsSession.dispose();
}
getId(): string {
return InteractiveEditorController.ID;
}
async run(): Promise<void> {
this._ctsSession.dispose(true);
if (!this._editor.hasModel()) {
return;
}
const provider = Iterable.first(this._interactiveEditorService.getAll());
if (!provider) {
this._logService.trace('[IE] NO provider found');
return;
}
this._ctsSession = new CancellationTokenSource();
const session = await provider.prepareInteractiveEditorSession(this._editor.getModel(), this._editor.getSelection(), this._ctsSession.token);
if (!session) {
this._logService.trace('[IE] NO session', provider.debugName);
return;
}
this._logService.trace('[IE] NEW session', provider.debugName);
const decoBackground = this._editor.createDecorationsCollection();
const decoPreview = this._editor.createDecorationsCollection();
const decoWholeRange = this._editor.createDecorationsCollection();
decoWholeRange.set([{
range: this._editor.getSelection(),
options: { description: 'interactive-editor-marker' }
}]);
let placeholder = session.placeholder ?? '';
let value = '';
const listener = new DisposableStore();
this._editor.onDidChangeModel(this._ctsSession.cancel, this._ctsSession, listener);
// CANCEL if the document has changed outside the current range
this._editor.onDidChangeModelContent(e => {
let cancel = false;
const wholeRange = decoWholeRange.getRange(0);
if (!wholeRange) {
cancel = true;
} else {
for (const change of e.changes) {
if (!Range.areIntersectingOrTouching(wholeRange, change.range)) {
cancel = true;
break;
}
}
}
if (cancel) {
this._ctsSession.cancel();
this._logService.trace('[IE] CANCEL because of model change OUTSIDE range');
}
}, undefined, listener);
do {
const wholeRange = decoWholeRange.getRange(0);
if (!wholeRange) {
// nuked whole file contents?
break;
}
const newDecorations: IModelDeltaDecoration[] = [{
range: wholeRange,
options: InteractiveEditorController._decoBlock
}];
decoBackground.set(newDecorations);
this._historyOffset = -1;
const input = await this._zone.getInput(wholeRange.collapseToEnd(), placeholder, value, this._ctsSession.token);
if (!input || !input.value) {
continue;
}
this._ctsRequest?.dispose(true);
this._ctsRequest = new CancellationTokenSource(this._ctsSession.token);
const sw = StopWatch.create();
const task = provider.provideResponse(
session,
{
session,
prompt: input.value,
selection: this._editor.getSelection(),
wholeRange: wholeRange
},
this._ctsRequest.token
);
let reply: IInteractiveEditorResponse | null | undefined;
try {
this._zone.widget.updateProgress(true);
this._ctxHasActiveRequest.set(true);
reply = await raceCancellationError(Promise.resolve(task), this._ctsRequest.token);
} catch (e) {
if (!isCancellationError(e)) {
this._logService.error('[IE] ERROR during request', provider.debugName);
this._logService.error(e);
}
} finally {
this._ctxHasActiveRequest.set(false);
this._zone.widget.updateProgress(false);
}
this._logService.trace('[IE] request took', sw.elapsed(), provider.debugName);
if (this._ctsRequest.token.isCancellationRequested) {
value = input.value;
continue;
}
if (!reply || isFalsyOrEmpty(reply.edits)) {
this._logService.trace('[IE] NO reply or edits', provider.debugName);
reply = { edits: [] };
placeholder = '';
continue;
}
// make edits more minimal
const moreMinimalEdits = (await this._editorWorkerService.computeMoreMinimalEdits(this._editor.getModel().uri, reply.edits, true)) ?? reply.edits;
// clear old preview
decoPreview.clear();
const undoEdits: IValidEditOperation[] = [];
this._editor.pushUndoStop();
this._editor.executeEdits(
'interactive-editor',
moreMinimalEdits.map(edit => {
// return EditOperation.replaceMove(Range.lift(edit.range), edit.text); ???
return EditOperation.replace(Range.lift(edit.range), edit.text);
}),
_undoEdits => {
let last: Position | null = null;
for (const undoEdit of _undoEdits) {
undoEdits.push(undoEdit);
last = !last || last.isBefore(undoEdit.range.getEndPosition()) ? undoEdit.range.getEndPosition() : last;
}
return last && [Selection.fromPositions(last)];
}
);
this._editor.pushUndoStop();
if (input.preview) {
const decorations: IModelDeltaDecoration[] = [];
for (const edit of undoEdits) {
let content = edit.text;
if (content.length > 12) {
content = content.substring(0, 12) + '…';
}
decorations.push({
range: edit.range,
options: {
description: 'interactive-editor-inline-diff',
className: 'interactive-editor-lines-inserted-range',
before: {
content,
inlineClassName: 'interactive-editor-lines-deleted-range-inline',
attachedData: edit
}
}
});
}
decoPreview.set(decorations);
}
if (!InteractiveEditorController._promptHistory.includes(input.value)) {
InteractiveEditorController._promptHistory.unshift(input.value);
}
placeholder = reply.placeholder ?? session.placeholder ?? '';
} while (!this._ctsSession.token.isCancellationRequested);
// done, cleanup
decoWholeRange.clear();
decoBackground.clear();
decoPreview.clear();
listener.dispose();
session.dispose?.();
this._zone.hide();
this._editor.focus();
this._logService.trace('[IE] session DONE', provider.debugName);
}
accept(preview: boolean = this._preview): void {
this._zone.widget.acceptInput(preview);
}
private _preview: boolean = false; // TODO@jrieken persist this
togglePreview(): void {
this._preview = !this._preview;
this._ctxShowPreview.set(this._preview);
}
cancelCurrentRequest(): void {
this._ctsRequest?.cancel();
}
cancelSession() {
this._ctsSession.cancel();
}
arrowOut(up: boolean): void {
if (this._zone.position && this._editor.hasModel()) {
const { column } = this._editor.getPosition();
const { lineNumber } = this._zone.position;
const newLine = up ? lineNumber : lineNumber + 1;
this._editor.setPosition({ lineNumber: newLine, column });
this._editor.focus();
}
}
focus(): void {
this._zone.widget.focus();
}
populateHistory(up: boolean) {
const len = InteractiveEditorController._promptHistory.length;
if (len === 0) {
return;
}
const pos = (len + this._historyOffset + (up ? 1 : -1)) % len;
const entry = InteractiveEditorController._promptHistory[pos];
this._zone.widget.populateInputField(entry);
this._historyOffset = pos;
}
toggleHistory(): void {
this._zone.widget.toggleHistory();
}
}

View file

@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IRange } from 'vs/editor/common/core/range';
import { ISelection } from 'vs/editor/common/core/selection';
import { ProviderResult, TextEdit } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { localize } from 'vs/nls';
import { MenuId } from 'vs/platform/actions/common/actions';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export interface IInteractiveEditorSession {
id: number;
placeholder?: string;
dispose?(): void;
}
export interface IInteractiveEditorRequest {
session: IInteractiveEditorSession;
prompt: string;
// model: ITextModel;
selection: ISelection;
wholeRange: IRange;
}
export interface IInteractiveEditorResponse {
// item: IInputModeSession;
edits: TextEdit[]; // WorkspaceEdit?
placeholder?: string;
}
export interface IInteractiveEditorSessionProvider {
debugName: string;
prepareInteractiveEditorSession(model: ITextModel, range: ISelection, token: CancellationToken): ProviderResult<IInteractiveEditorSession>;
provideResponse(item: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): ProviderResult<IInteractiveEditorResponse>;
}
export const IInteractiveEditorService = createDecorator<IInteractiveEditorService>('IInteractiveEditorService');
export interface IInteractiveEditorService {
_serviceBrand: undefined;
add(provider: IInteractiveEditorSessionProvider): IDisposable;
getAll(): Iterable<IInteractiveEditorSessionProvider>;
}
export const MENU_INTERACTIVE_EDITOR_WIDGET_LHS = MenuId.for('interactiveEditorWidgetLhs');
export const MENU_INTERACTIVE_EDITOR_WIDGET = MenuId.for('interactiveEditorWidgetRhs');
export const CTX_INTERACTIVE_EDITOR_HAS_PROVIDER = new RawContextKey<boolean>('interactiveEditorHasProvider', false, localize('interactiveEditorHasProvider', "Whether a provider for interactive editors exists"));
export const CTX_INTERACTIVE_EDITOR_VISIBLE = new RawContextKey<boolean>('interactiveEditorVisible', false, localize('interactiveEditorVisible', "Whether the interactive editor input is visible"));
export const CTX_INTERACTIVE_EDITOR_FOCUSED = new RawContextKey<boolean>('interactiveEditorFocused', false, localize('interactiveEditorFocused', "Whether the interactive editor input is focused"));
export const CTX_INTERACTIVE_EDITOR_EMPTY = new RawContextKey<boolean>('interactiveEditorEmpty', false, localize('interactiveEditorEmpty', "Whether the interactive editor input is empty"));
export const CTX_INTERACTIVE_EDITOR_PREVIEW = new RawContextKey<boolean>('interactiveEditorPreview', false, localize('interactiveEditorPreview', "Whether the interactive editor input shows inline previews"));
export const CTX_INTERACTIVE_EDITOR_HISTORY_VISIBLE = new RawContextKey<boolean>('interactiveEditorHistoryVisible', false, localize('interactiveEditorHistoryVisible', "Whether the interactive editor history is visible"));
export const CTX_INTERACTIVE_EDITOR_INNER_CURSOR_FIRST = new RawContextKey<boolean>('interactiveEditorInnerCursorFirst', false, localize('interactiveEditorInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line"));
export const CTX_INTERACTIVE_EDITOR_INNER_CURSOR_LAST = new RawContextKey<boolean>('interactiveEditorInnerCursorLast', false, localize('interactiveEditorInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line"));
export const CTX_INTERACTIVE_EDITOR_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('interactiveEditorOuterCursorPosition', '', localize('interactiveEditorOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input"));
export const CTX_INTERACTIVE_EDITOR_HAS_ACTIVE_REQUEST = new RawContextKey<boolean>('interactiveEditorHasActiveRequest', false, localize('interactiveEditorHasActiveRequest', "Whether interactive editor has an active request"));

View file

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { LinkedList } from 'vs/base/common/linkedList';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInteractiveEditorService, IInteractiveEditorSessionProvider, CTX_INTERACTIVE_EDITOR_HAS_PROVIDER } from './interactiveEditor';
export class InteractiveEditorServiceImpl implements IInteractiveEditorService {
declare _serviceBrand: undefined;
private readonly _entries = new LinkedList<IInteractiveEditorSessionProvider>();
private readonly _ctxHasProvider: IContextKey<boolean>;
constructor(@IContextKeyService contextKeyService: IContextKeyService) {
this._ctxHasProvider = CTX_INTERACTIVE_EDITOR_HAS_PROVIDER.bindTo(contextKeyService);
}
add(provider: IInteractiveEditorSessionProvider): IDisposable {
const rm = this._entries.push(provider);
this._ctxHasProvider.set(true);
return toDisposable(() => {
rm();
this._ctxHasProvider.set(this._entries.size > 0);
});
}
getAll() {
return [...this._entries];
}
}

View file

@ -261,7 +261,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider {
}
}
private _getWidth(info: EditorLayoutInfo): number {
protected _getWidth(info: EditorLayoutInfo): number {
return info.width - info.minimap.minimapWidth - info.verticalScrollbarWidth;
}

View file

@ -57,6 +57,7 @@ import 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter';
import 'vs/editor/contrib/wordOperations/browser/wordOperations';
import 'vs/editor/contrib/wordPartOperations/browser/wordPartOperations';
import 'vs/editor/contrib/readOnlyMessage/browser/contribution';
import 'vs/editor/contrib/interactive/browser/interactiveEditor.contribution';
// Load up these strings even in VSCode, even if they are not used
// in order to get them translated

View file

@ -88,7 +88,7 @@ suite('EditorSimpleWorker', () => {
test('MoreMinimal', () => {
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: 'This is line One', range: new Range(1, 1, 1, 17) }]).then(edits => {
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: 'This is line One', range: new Range(1, 1, 1, 17) }], false).then(edits => {
assert.strictEqual(edits.length, 1);
const [first] = edits;
assert.strictEqual(first.text, 'O');
@ -104,7 +104,7 @@ suite('EditorSimpleWorker', () => {
'}'
], '\n');
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"a":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => {
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"a":1\r\n}', range: new Range(1, 1, 3, 2) }], false).then(edits => {
assert.strictEqual(edits.length, 0);
});
});
@ -117,7 +117,7 @@ suite('EditorSimpleWorker', () => {
'}'
], '\n');
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"b":1\r\n}', range: new Range(1, 1, 3, 2) }]).then(edits => {
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '{\r\n\t"b":1\r\n}', range: new Range(1, 1, 3, 2) }], false).then(edits => {
assert.strictEqual(edits.length, 1);
const [first] = edits;
assert.strictEqual(first.text, 'b');
@ -125,7 +125,7 @@ suite('EditorSimpleWorker', () => {
});
});
test('MoreMinimal, issue #15385 newline changes and other', function () {
test('MoreMinimal, issue #15385 newline changes and other 2/2', function () {
const model = worker.addModel([
'package main', // 1
@ -133,7 +133,7 @@ suite('EditorSimpleWorker', () => {
'}' // 3
]);
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '\n', range: new Range(3, 2, 4, 1000) }]).then(edits => {
return worker.computeMoreMinimalEdits(model.uri.toString(), [{ text: '\n', range: new Range(3, 2, 4, 1000) }], false).then(edits => {
assert.strictEqual(edits.length, 1);
const [first] = edits;
assert.strictEqual(first.text, '\n');

1
src/vs/monaco.d.ts vendored
View file

@ -1552,6 +1552,7 @@ declare namespace monaco.editor {
* In this case, the range must be empty and set to the last line.
*/
blockIsAfterEnd?: boolean | null;
blockDoesNotCollapse?: boolean | null;
blockPadding?: [top: number, right: number, bottom: number, left: number] | null;
/**
* Message to be rendered when hovering over the glyph margin decoration.

View file

@ -440,6 +440,8 @@ export class MenuItemAction implements IAction {
this.enabled = !item.precondition || contextKeyService.contextMatchesRules(item.precondition);
this.checked = undefined;
let icon: ThemeIcon | undefined;
if (item.toggled) {
const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as {
condition: ContextKeyExpression; icon?: Icon; tooltip?: string | ILocalizedString; title?: string | ILocalizedString;
@ -449,17 +451,24 @@ export class MenuItemAction implements IAction {
this.tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value;
}
if (this.checked && ThemeIcon.isThemeIcon(toggled.icon)) {
icon = toggled.icon;
}
if (toggled.title) {
this.label = typeof toggled.title === 'string' ? toggled.title : toggled.title.value;
}
}
if (!icon) {
icon = ThemeIcon.isThemeIcon(item.icon) ? item.icon : undefined;
}
this.item = item;
this.alt = alt ? new MenuItemAction(alt, undefined, options, hideActions, contextKeyService, _commandService) : undefined;
this._options = options;
if (ThemeIcon.isThemeIcon(item.icon)) {
this.class = ThemeIcon.asClassName(item.icon);
}
this.class = icon && ThemeIcon.asClassName(icon);
}
run(...args: any[]): Promise<void> {

View file

@ -70,6 +70,7 @@ import './mainThreadNotebookKernels';
import './mainThreadNotebookDocumentsAndEditors';
import './mainThreadNotebookRenderers';
import './mainThreadInteractive';
import './mainThreadInteractiveEditor';
import './mainThreadInteractiveSession';
import './mainThreadTask';
import './mainThreadLabelService';

View file

@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableMap } from 'vs/base/common/lifecycle';
import { IInteractiveEditorService } from 'vs/editor/contrib/interactive/common/interactiveEditor';
import { ExtHostContext, ExtHostInteractiveEditorShape, MainContext, MainThreadInteractiveEditorShape } from 'vs/workbench/api/common/extHost.protocol';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadInteractiveEditor)
export class MainThreadInteractiveEditor implements MainThreadInteractiveEditorShape {
private readonly _registrations = new DisposableMap<number>();
private readonly _proxy: ExtHostInteractiveEditorShape;
constructor(
extHostContext: IExtHostContext,
@IInteractiveEditorService private readonly _interactiveEditorService: IInteractiveEditorService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostInteractiveEditor);
}
dispose(): void {
this._registrations.dispose();
}
async $registerInteractiveEditorProvider(handle: number, debugName: string): Promise<void> {
const unreg = this._interactiveEditorService.add({
debugName,
prepareInteractiveEditorSession: async (model, range, token) => {
const session = await this._proxy.$prepareInteractiveSession(handle, model.uri, range, token);
if (!session) {
return undefined;
}
return {
...session,
dispose: () => {
this._proxy.$releaseSession(handle, session.id);
}
};
},
provideResponse: (item, request, token) => {
return this._proxy.$provideResponse(handle, item, request, token);
}
});
this._registrations.set(handle, unreg);
}
async $unregisterInteractiveEditorProvider(handle: number): Promise<void> {
this._registrations.deleteAndDispose(handle);
}
}

View file

@ -98,6 +98,7 @@ import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessi
import { ExtHostProfileContentHandlers } from 'vs/workbench/api/common/extHostProfileContentHandler';
import { ExtHostQuickDiff } from 'vs/workbench/api/common/extHostQuickDiff';
import { ExtHostInteractiveSession } from 'vs/workbench/api/common/extHostInteractiveSession';
import { ExtHostInteractiveEditor } from 'vs/workbench/api/common/extHostInteractiveEditor';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@ -192,6 +193,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
const extHostProfileContentHandlers = rpcProtocol.set(ExtHostContext.ExtHostProfileContentHandlers, new ExtHostProfileContentHandlers(rpcProtocol));
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInteractiveEditor, new ExtHostInteractiveEditor(rpcProtocol, extHostDocuments, extHostLogService));
const extHostInteractiveSession = rpcProtocol.set(ExtHostContext.ExtHostInteractiveSession, new ExtHostInteractiveSession(rpcProtocol, extHostLogService));
// Check that no named customers are missing
@ -1216,6 +1218,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
// this needs to be updated whenever the API proposal changes
_version: 1,
registerInteractiveEditorSessionProvider(provider: vscode.InteractiveEditorSessionProvider) {
checkProposedApiEnabled(extension, 'interactive');
return extHostInteractiveEditor.registerProvider(extension, provider);
},
registerInteractiveSessionProvider(id: string, provider: vscode.InteractiveSessionProvider) {
checkProposedApiEnabled(extension, 'interactive');
return extHostInteractiveSession.registerInteractiveSessionProvider(extension, id, provider);

View file

@ -26,6 +26,7 @@ import * as languages from 'vs/editor/common/languages';
import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/languages/languageConfiguration';
import { EndOfLineSequence } from 'vs/editor/common/model';
import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel';
import { IInteractiveEditorResponse, IInteractiveEditorSession, IInteractiveEditorRequest } from 'vs/editor/contrib/interactive/common/interactiveEditor';
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
@ -1068,6 +1069,17 @@ export interface MainThreadNotebookRenderersShape extends IDisposable {
export interface MainThreadInteractiveShape extends IDisposable {
}
export interface MainThreadInteractiveEditorShape extends IDisposable {
$registerInteractiveEditorProvider(handle: number, debugName: string): Promise<void>;
$unregisterInteractiveEditorProvider(handle: number): Promise<void>;
}
export interface ExtHostInteractiveEditorShape {
$prepareInteractiveSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise<IInteractiveEditorSession | undefined>;
$provideResponse(handle: number, session: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): Promise<IInteractiveEditorResponse | undefined>;
$releaseSession(handle: number, sessionId: number): void;
}
export interface MainThreadUrlsShape extends IDisposable {
$registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise<void>;
$unregisterUriHandler(handle: number): Promise<void>;
@ -2411,6 +2423,7 @@ export const MainContext = {
MainThreadNotebookRenderers: createProxyIdentifier<MainThreadNotebookRenderersShape>('MainThreadNotebookRenderers'),
MainThreadInteractive: createProxyIdentifier<MainThreadInteractiveShape>('MainThreadInteractive'),
MainThreadInteractiveSession: createProxyIdentifier<MainThreadInteractiveSessionShape>('MainThreadInteractiveSession'),
MainThreadInteractiveEditor: createProxyIdentifier<MainThreadInteractiveEditorShape>('MainThreadInteractiveEditor'),
MainThreadTheming: createProxyIdentifier<MainThreadThemingShape>('MainThreadTheming'),
MainThreadTunnelService: createProxyIdentifier<MainThreadTunnelServiceShape>('MainThreadTunnelService'),
MainThreadTimeline: createProxyIdentifier<MainThreadTimelineShape>('MainThreadTimeline'),
@ -2466,6 +2479,7 @@ export const ExtHostContext = {
ExtHostNotebookKernels: createProxyIdentifier<ExtHostNotebookKernelsShape>('ExtHostNotebookKernels'),
ExtHostNotebookRenderers: createProxyIdentifier<ExtHostNotebookRenderersShape>('ExtHostNotebookRenderers'),
ExtHostInteractive: createProxyIdentifier<ExtHostInteractiveShape>('ExtHostInteractive'),
ExtHostInteractiveEditor: createProxyIdentifier<ExtHostInteractiveEditorShape>('ExtHostInteractiveEditor'),
ExtHostInteractiveSession: createProxyIdentifier<ExtHostInteractiveSessionShape>('ExtHostInteractiveSession'),
ExtHostTheming: createProxyIdentifier<ExtHostThemingShape>('ExtHostTheming'),
ExtHostTunnelService: createProxyIdentifier<ExtHostTunnelServiceShape>('ExtHostTunnelService'),

View file

@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { toDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ISelection } from 'vs/editor/common/core/selection';
import { IInteractiveEditorResponse, IInteractiveEditorSession, IInteractiveEditorRequest } from 'vs/editor/contrib/interactive/common/interactiveEditor';
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtHostInteractiveEditorShape, IMainContext, MainContext, MainThreadInteractiveEditorShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import type * as vscode from 'vscode';
class ProviderWrapper {
private static _pool = 0;
readonly handle: number = ProviderWrapper._pool++;
constructor(
readonly extension: Readonly<IRelaxedExtensionDescription>,
readonly provider: vscode.InteractiveEditorSessionProvider,
) { }
}
export class ExtHostInteractiveEditor implements ExtHostInteractiveEditorShape {
private static _nextId = 0;
private readonly _inputProvider = new Map<number, ProviderWrapper>();
private readonly _inputSessions = new Map<number, vscode.InteractiveEditorSession>();
private readonly _proxy: MainThreadInteractiveEditorShape;
constructor(
mainContext: IMainContext,
private readonly _documents: ExtHostDocuments,
private readonly _logService: ILogService
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadInteractiveEditor);
}
registerProvider(extension: Readonly<IRelaxedExtensionDescription>, provider: vscode.InteractiveEditorSessionProvider): vscode.Disposable {
const wrapper = new ProviderWrapper(extension, provider);
this._inputProvider.set(wrapper.handle, wrapper);
this._proxy.$registerInteractiveEditorProvider(wrapper.handle, extension.identifier.value);
return toDisposable(() => {
this._proxy.$unregisterInteractiveEditorProvider(wrapper.handle);
this._inputProvider.delete(wrapper.handle);
});
}
async $prepareInteractiveSession(handle: number, uri: UriComponents, range: ISelection, token: CancellationToken): Promise<IInteractiveEditorSession | undefined> {
const entry = this._inputProvider.get(handle);
if (!entry) {
this._logService.warn('CANNOT prepare session because the PROVIDER IS GONE');
return undefined;
}
const document = this._documents.getDocument(URI.revive(uri));
const session = await entry.provider.prepareInteractiveEditorSession({ document, selection: typeConvert.Selection.to(range) }, token);
if (!session) {
return undefined;
}
const id = ExtHostInteractiveEditor._nextId++;
this._inputSessions.set(id, session);
return { id, placeholder: session.placeholder };
}
async $provideResponse(handle: number, item: IInteractiveEditorSession, request: IInteractiveEditorRequest, token: CancellationToken): Promise<IInteractiveEditorResponse | undefined> {
const entry = this._inputProvider.get(handle);
if (!entry) {
return undefined;
}
const session = this._inputSessions.get(item.id);
if (!session) {
return;
}
const res = await entry.provider.provideInteractiveEditorResponse({
session,
prompt: request.prompt,
selection: typeConvert.Selection.to(request.selection),
wholeRange: typeConvert.Range.to(request.wholeRange)
}, token);
if (!res) {
return;
}
return {
edits: res.edits.map(typeConvert.TextEdit.from),
placeholder: res.placeholder
};
}
$releaseSession(handle: number, sessionId: number) {
const session = this._inputSessions.get(sessionId);
const entry = this._inputProvider.get(handle);
if (session && entry) {
entry.provider.releaseInteractiveEditorSession?.(session);
}
this._inputSessions.delete(sessionId);
}
}

View file

@ -183,6 +183,7 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget {
EmbeddedCodeEditorWidget,
editorContainer,
editorOptions,
{},
this.editor
);

View file

@ -1182,6 +1182,7 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer {
EmbeddedCodeEditorWidget,
this.container,
commonEditorOptions,
{},
this.editor,
) : this.instantiationService.createInstance(
CodeEditorWidget,

View file

@ -184,6 +184,7 @@ export class TypeHierarchyTreePeekWidget extends peekView.PeekViewWidget {
EmbeddedCodeEditorWidget,
editorContainer,
editorOptions,
{},
this.editor
);

View file

@ -31,11 +31,11 @@ declare module 'vscode' {
action?: string;
}
export interface InteractivEditorSessionProvider {
export interface InteractiveEditorSessionProvider {
// Create a session. The lifetime of this session is the duration of the editing session with the input mode widget.
prepareInteractiveEditorSession(context: TextDocumentContext, token: CancellationToken): ProviderResult<InteractiveEditorSession>;
provideInteractivEditorResponse(request: InteractiveEditorRequest, token: CancellationToken): ProviderResult<InteractiveEditorResponse>;
provideInteractiveEditorResponse(request: InteractiveEditorRequest, token: CancellationToken): ProviderResult<InteractiveEditorResponse>;
// eslint-disable-next-line local/vscode-dts-provider-naming
releaseInteractiveEditorSession?(session: InteractiveEditorSession): any;
@ -85,5 +85,7 @@ declare module 'vscode' {
export function registerInteractiveSessionProvider(id: string, provider: InteractiveSessionProvider): Disposable;
export function addInteractiveRequest(context: InteractiveSessionRequestArgs): void;
export function registerInteractiveEditorSessionProvider(provider: InteractiveEditorSessionProvider): Disposable;
}
}