Skip to content

Commit

Permalink
feat: support NES render in code edits (#4403)
Browse files Browse the repository at this point in the history
* feat: support NES render in code edits

* chore: use enum
  • Loading branch information
Ricbet authored Feb 24, 2025
1 parent c6c8bce commit 4ff2083
Show file tree
Hide file tree
Showing 13 changed files with 489 additions and 176 deletions.
4 changes: 4 additions & 0 deletions packages/ai-native/src/browser/ai-core.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ export class AINativeBrowserContribution
id: AINativeSettingSectionsId.CodeEditsTyping,
localized: 'preference.ai.native.codeEdits.typing',
},
{
id: AINativeSettingSectionsId.CodeEditsRenderType,
localized: 'preference.ai.native.codeEdits.renderType',
},
{
id: AINativeSettingSectionsId.SystemPrompt,
localized: 'preference.ai.native.chat.system.prompt',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Disposable, ECodeEditsSourceTyping } from '@opensumi/ide-core-common';
import { IModelContentChangedEvent, IPosition, IRange, InlineCompletion } from '@opensumi/ide-monaco';
import {
IModelContentChangedEvent,
IPosition,
IRange,
InlineCompletion,
InlineCompletions,
} from '@opensumi/ide-monaco';

import { ITriggerData } from './source/trigger.source';

import type { ILineChangeData } from './source/line-change.source';
import type { ILinterErrorData } from './source/lint-error.source';

export enum CodeEditsRenderType {
Legacy = 'legacy',
Default = 'default',
}

/**
* 有效弃用时间(毫秒)
* 在可见的情况下超过 750ms 弃用才算有效数据,否则视为无效数据
Expand All @@ -31,7 +42,7 @@ export interface ICodeEditsContextBean {
};
}

export interface ICodeEdit {
export interface ICodeEdit extends InlineCompletion {
/**
* 插入的文本
*/
Expand All @@ -41,16 +52,24 @@ export interface ICodeEdit {
*/
readonly range: IRange;
}
export interface ICodeEditsResult {
readonly items: ICodeEdit[];

export interface ICodeEditsResult<T extends ICodeEdit = ICodeEdit> extends InlineCompletions<T> {
readonly items: readonly T[];
}

export class CodeEditsResultValue extends Disposable {
constructor(private readonly raw: ICodeEditsResult) {
export class CodeEditsResultValue<T extends ICodeEdit = ICodeEdit> extends Disposable {
constructor(private readonly raw: ICodeEditsResult<T>) {
super();
}

public get items(): ICodeEdit[] {
return this.raw.items;
public get items(): T[] {
return this.raw.items.map((item) => ({
...item,
isInlineEdit: true,
}));
}

public get range(): IRange {
return this.raw.items[0].range;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
IntelligentCompletionsRegistryToken,
runWhenIdle,
} from '@opensumi/ide-core-common';
import { Emitter, ICodeEditor, ICursorPositionChangedEvent, IRange, ITextModel, Range } from '@opensumi/ide-monaco';
import { Emitter, ICodeEditor, ICursorPositionChangedEvent, ITextModel } from '@opensumi/ide-monaco';
import {
IObservable,
ISettableObservable,
Expand All @@ -30,7 +30,6 @@ import {
observableValue,
transaction,
} from '@opensumi/ide-monaco/lib/common/observable';
import { empty } from '@opensumi/ide-utils/lib/strings';
import { EditorContextKeys } from '@opensumi/monaco-editor-core/esm/vs/editor/common/editorContextKeys';
import { inlineSuggestCommitId } from '@opensumi/monaco-editor-core/esm/vs/editor/contrib/inlineCompletions/browser/controller/commandIds';
import { InlineCompletionContextKeys } from '@opensumi/monaco-editor-core/esm/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys';
Expand All @@ -42,23 +41,15 @@ import {
import { ContextKeyExpr } from '@opensumi/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';

import { AINativeContextKey } from '../../ai-core.contextkeys';
import { REWRITE_DECORATION_INLINE_ADD, RewriteWidget } from '../../widget/rewrite/rewrite-widget';
import { BaseAIMonacoEditorController } from '../base';

import { AdditionsDeletionsDecorationModel } from './decoration/additions-deletions.decoration';
import { MultiLineDecorationModel } from './decoration/multi-line.decoration';
import {
IMultiLineDiffChangeResult,
computeMultiLineDiffChanges,
mergeMultiLineDiffChanges,
wordChangesToLineChangesMap,
} from './diff-computer';
import { IntelligentCompletionsRegistry } from './intelligent-completions.feature.registry';
import { CodeEditsSourceCollection } from './source/base';
import { LineChangeCodeEditsSource } from './source/line-change.source';
import { LintErrorCodeEditsSource } from './source/lint-error.source';
import { TriggerCodeEditsSource } from './source/trigger.source';
import { TypingCodeEditsSource } from './source/typing.source';
import { CodeEditsPreviewer } from './view/code-edits-previewer';

import { CodeEditsResultValue, VALID_TIME } from './index';

Expand Down Expand Up @@ -90,20 +81,18 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}

private codeEditsResult: ISettableObservable<CodeEditsResultValue | undefined>;
private multiLineDecorationModel: MultiLineDecorationModel;
private additionsDeletionsDecorationModel: AdditionsDeletionsDecorationModel;
private multiLineEditsIsVisibleObs: IObservable<boolean>;

private codeEditsSourceCollection: CodeEditsSourceCollection;
private aiNativeContextKey: AINativeContextKey;
private rewriteWidget: RewriteWidget | null;
private multiLineEditsIsVisibleObs: IObservable<boolean>;
private codeEditsPreviewer: CodeEditsPreviewer;

public mount(): IDisposable {
this.handlerAlwaysVisiblePreference();

this.codeEditsResult = observableValue<CodeEditsResultValue | undefined>(this, undefined);
this.multiLineDecorationModel = new MultiLineDecorationModel(this.monacoEditor);
this.additionsDeletionsDecorationModel = new AdditionsDeletionsDecorationModel(this.monacoEditor);
this.aiNativeContextKey = this.injector.get(AINativeContextKey, [this.monacoEditor.contextKeyService]);
this.codeEditsPreviewer = this.injector.get(CodeEditsPreviewer, [this.monacoEditor, this.aiNativeContextKey]);
this.codeEditsSourceCollection = this.injector.get(CodeEditsSourceCollection, [
[LintErrorCodeEditsSource, LineChangeCodeEditsSource, TypingCodeEditsSource, TriggerCodeEditsSource],
this.monacoEditor,
Expand Down Expand Up @@ -214,125 +203,10 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
);
}

private destroyRewriteWidget() {
if (this.rewriteWidget) {
this.rewriteWidget.dispose();
this.rewriteWidget = null;
}
}

private applyInlineDecorations(completionModel: CodeEditsResultValue) {
const { items } = completionModel;
const { range, insertText } = items[0];

// code edits 必须提供 range
if (!range) {
return;
}

const position = this.monacoEditor.getPosition()!;
const model = this.monacoEditor.getModel();
const insertTextString = insertText.toString();
const originalContent = model?.getValueInRange(range);
const eol = this.model.getEOL();

const changes = computeMultiLineDiffChanges(
originalContent!,
insertTextString,
this.monacoEditor,
range.startLineNumber,
eol,
);

if (!changes) {
return;
}

const { singleLineCharChanges, charChanges, wordChanges, isOnlyAddingToEachWord } = changes;

// 限制 changes 数量,超过这个数量直接显示智能重写
const maxCharChanges = 20;
const maxWordChanges = 20;

if (
range &&
isOnlyAddingToEachWord &&
charChanges.length <= maxCharChanges &&
wordChanges.length <= maxWordChanges
) {
const modificationsResult = this.multiLineDecorationModel.applyInlineDecorations(
this.monacoEditor,
mergeMultiLineDiffChanges(singleLineCharChanges, eol),
range.startLineNumber,
position,
);

this.aiNativeContextKey.multiLineEditsIsVisible.reset();
this.multiLineDecorationModel.clearDecorations();

if (!modificationsResult) {
this.renderRewriteWidget(wordChanges, model, range, insertTextString);
} else if (modificationsResult && modificationsResult.inlineMods) {
this.aiNativeContextKey.multiLineEditsIsVisible.set(true);
this.multiLineDecorationModel.updateLineModificationDecorations(modificationsResult.inlineMods);
}
} else {
this.additionsDeletionsDecorationModel.updateDeletionsDecoration(wordChanges, range, eol);
this.renderRewriteWidget(wordChanges, model, range, insertTextString);
}
}

private async renderRewriteWidget(
wordChanges: IMultiLineDiffChangeResult[],
model: ITextModel | null,
range: IRange,
insertTextString: string,
) {
this.destroyRewriteWidget();

const cursorPosition = this.monacoEditor.getPosition();
if (!cursorPosition) {
return;
}

this.rewriteWidget = this.injector.get(RewriteWidget, [this.monacoEditor]);

const startOffset = this.model.getOffsetAt({ lineNumber: range.startLineNumber, column: range.startColumn });
const endOffset = this.model.getOffsetAt({ lineNumber: range.endLineNumber, column: range.endColumn });
const allText = this.model.getValue();
// 这里是为了能在 rewrite widget 的 editor 当中完整的复用代码高亮与语法检测的能力
const newVirtualContent = allText.substring(0, startOffset) + insertTextString + allText.substring(endOffset);

const lineChangesMap = wordChangesToLineChangesMap(wordChanges, range, model);

await this.rewriteWidget.defered.promise;

this.aiNativeContextKey.multiLineEditsIsVisible.set(true);

const allLineChanges = Object.values(lineChangesMap).map((lineChanges) => ({
changes: lineChanges
.map((change) => change.filter((item) => item.value.trim() !== empty))
.filter((change) => change.length > 0),
}));

this.rewriteWidget.setInsertText(insertTextString);
this.rewriteWidget.show({ position: cursorPosition });
this.rewriteWidget.setEditArea(range);

if (allLineChanges.every(({ changes }) => changes.every((change) => change.every(({ removed }) => removed)))) {
// 处理全是删除的情况
this.rewriteWidget.renderTextLineThrough(allLineChanges);
} else {
this.rewriteWidget.renderVirtualEditor(newVirtualContent, wordChanges);
}
}

public hide() {
this.cancelToken();
this.aiNativeContextKey.multiLineEditsIsVisible.reset();
this.multiLineDecorationModel.clearDecorations();
this.additionsDeletionsDecorationModel.clearDeletionsDecorations();
this.destroyRewriteWidget();
this.codeEditsPreviewer.hide();
}

private readonly reportData = derived(this, (reader) => {
Expand Down Expand Up @@ -382,6 +256,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
report?.('isValid', false);
}

this.codeEditsPreviewer.discard();
this.hide();
return isValid;
},
Expand All @@ -391,27 +266,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
const report = this.reportData.read(reader);
report?.('isReceive');

this.multiLineDecorationModel.accept();

if (this.rewriteWidget) {
this.rewriteWidget.accept();

const virtualEditor = this.rewriteWidget.getVirtualEditor();
// 采纳完之后将 virtualEditor 的 decorations 重新映射在 editor 上
if (virtualEditor) {
const editArea = this.rewriteWidget.getEditArea();
const decorations = virtualEditor.getDecorationsInRange(Range.lift(editArea));
const preAddedDecorations = decorations?.filter(
(decoration) => decoration.options.description === REWRITE_DECORATION_INLINE_ADD,
);
if (preAddedDecorations) {
this.additionsDeletionsDecorationModel.updateAdditionsDecoration(
preAddedDecorations.map((decoration) => decoration.range),
);
}
}
}

this.codeEditsPreviewer.accept();
this.hide();
});

Expand All @@ -423,16 +278,6 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}

private registerFeature(monacoEditor: ICodeEditor): void {
this.featureDisposable.addDispose(
Event.any<any>(
monacoEditor.onDidChangeCursorPosition,
monacoEditor.onDidChangeModelContent,
monacoEditor.onDidBlurEditorWidget,
)(() => {
this.additionsDeletionsDecorationModel.clearAdditionsDecorations();
}),
);

// 监听当前光标位置的变化,如果超出 range 区域则表示弃用
this.featureDisposable.addDispose(
this.monacoEditor.onDidChangeCursorPosition((event: ICursorPositionChangedEvent) => {
Expand Down Expand Up @@ -505,7 +350,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}

try {
this.applyInlineDecorations(completionModel);
this.codeEditsPreviewer.render(completionModel);
} catch (error) {
this.logger.warn('IntelligentCompletionsController applyInlineDecorations error', error);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injector } from '@opensumi/di';
import { Disposable } from '@opensumi/ide-core-common';
import { ICodeEditor } from '@opensumi/ide-monaco';
import {
ObservableCodeEditor,
observableCodeEditor,
} from '@opensumi/monaco-editor-core/esm/vs/editor/browser/observableCodeEditor';

import { CodeEditsResultValue } from '../index';

export abstract class BaseCodeEditsView extends Disposable {
protected editorObs: ObservableCodeEditor;

public modelId: string;

constructor(protected readonly monacoEditor: ICodeEditor, protected readonly injector: Injector) {
super();

this.editorObs = observableCodeEditor(this.monacoEditor);
this.mount();

this.addDispose({ dispose: () => this.hide() });
}

protected mount(): void {}

abstract render(completionModel: CodeEditsResultValue): void;
abstract hide(): void;
abstract accept(): void;
abstract discard(): void;
}
Loading

0 comments on commit 4ff2083

Please sign in to comment.