Skip to content

Commit 4ff2083

Browse files
authored
feat: support NES render in code edits (#4403)
* feat: support NES render in code edits * chore: use enum
1 parent c6c8bce commit 4ff2083

File tree

13 files changed

+489
-176
lines changed

13 files changed

+489
-176
lines changed

packages/ai-native/src/browser/ai-core.contribution.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,10 @@ export class AINativeBrowserContribution
424424
id: AINativeSettingSectionsId.CodeEditsTyping,
425425
localized: 'preference.ai.native.codeEdits.typing',
426426
},
427+
{
428+
id: AINativeSettingSectionsId.CodeEditsRenderType,
429+
localized: 'preference.ai.native.codeEdits.renderType',
430+
},
427431
{
428432
id: AINativeSettingSectionsId.SystemPrompt,
429433
localized: 'preference.ai.native.chat.system.prompt',

packages/ai-native/src/browser/contrib/intelligent-completions/index.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import { Disposable, ECodeEditsSourceTyping } from '@opensumi/ide-core-common';
2-
import { IModelContentChangedEvent, IPosition, IRange, InlineCompletion } from '@opensumi/ide-monaco';
2+
import {
3+
IModelContentChangedEvent,
4+
IPosition,
5+
IRange,
6+
InlineCompletion,
7+
InlineCompletions,
8+
} from '@opensumi/ide-monaco';
39

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

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

15+
export enum CodeEditsRenderType {
16+
Legacy = 'legacy',
17+
Default = 'default',
18+
}
19+
920
/**
1021
* 有效弃用时间(毫秒)
1122
* 在可见的情况下超过 750ms 弃用才算有效数据,否则视为无效数据
@@ -31,7 +42,7 @@ export interface ICodeEditsContextBean {
3142
};
3243
}
3344

34-
export interface ICodeEdit {
45+
export interface ICodeEdit extends InlineCompletion {
3546
/**
3647
* 插入的文本
3748
*/
@@ -41,16 +52,24 @@ export interface ICodeEdit {
4152
*/
4253
readonly range: IRange;
4354
}
44-
export interface ICodeEditsResult {
45-
readonly items: ICodeEdit[];
55+
56+
export interface ICodeEditsResult<T extends ICodeEdit = ICodeEdit> extends InlineCompletions<T> {
57+
readonly items: readonly T[];
4658
}
4759

48-
export class CodeEditsResultValue extends Disposable {
49-
constructor(private readonly raw: ICodeEditsResult) {
60+
export class CodeEditsResultValue<T extends ICodeEdit = ICodeEdit> extends Disposable {
61+
constructor(private readonly raw: ICodeEditsResult<T>) {
5062
super();
5163
}
5264

53-
public get items(): ICodeEdit[] {
54-
return this.raw.items;
65+
public get items(): T[] {
66+
return this.raw.items.map((item) => ({
67+
...item,
68+
isInlineEdit: true,
69+
}));
70+
}
71+
72+
public get range(): IRange {
73+
return this.raw.items[0].range;
5574
}
5675
}

packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.controller.ts

Lines changed: 10 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
IntelligentCompletionsRegistryToken,
1717
runWhenIdle,
1818
} from '@opensumi/ide-core-common';
19-
import { Emitter, ICodeEditor, ICursorPositionChangedEvent, IRange, ITextModel, Range } from '@opensumi/ide-monaco';
19+
import { Emitter, ICodeEditor, ICursorPositionChangedEvent, ITextModel } from '@opensumi/ide-monaco';
2020
import {
2121
IObservable,
2222
ISettableObservable,
@@ -30,7 +30,6 @@ import {
3030
observableValue,
3131
transaction,
3232
} from '@opensumi/ide-monaco/lib/common/observable';
33-
import { empty } from '@opensumi/ide-utils/lib/strings';
3433
import { EditorContextKeys } from '@opensumi/monaco-editor-core/esm/vs/editor/common/editorContextKeys';
3534
import { inlineSuggestCommitId } from '@opensumi/monaco-editor-core/esm/vs/editor/contrib/inlineCompletions/browser/controller/commandIds';
3635
import { InlineCompletionContextKeys } from '@opensumi/monaco-editor-core/esm/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys';
@@ -42,23 +41,15 @@ import {
4241
import { ContextKeyExpr } from '@opensumi/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
4342

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

48-
import { AdditionsDeletionsDecorationModel } from './decoration/additions-deletions.decoration';
49-
import { MultiLineDecorationModel } from './decoration/multi-line.decoration';
50-
import {
51-
IMultiLineDiffChangeResult,
52-
computeMultiLineDiffChanges,
53-
mergeMultiLineDiffChanges,
54-
wordChangesToLineChangesMap,
55-
} from './diff-computer';
5646
import { IntelligentCompletionsRegistry } from './intelligent-completions.feature.registry';
5747
import { CodeEditsSourceCollection } from './source/base';
5848
import { LineChangeCodeEditsSource } from './source/line-change.source';
5949
import { LintErrorCodeEditsSource } from './source/lint-error.source';
6050
import { TriggerCodeEditsSource } from './source/trigger.source';
6151
import { TypingCodeEditsSource } from './source/typing.source';
52+
import { CodeEditsPreviewer } from './view/code-edits-previewer';
6253

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

@@ -90,20 +81,18 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
9081
}
9182

9283
private codeEditsResult: ISettableObservable<CodeEditsResultValue | undefined>;
93-
private multiLineDecorationModel: MultiLineDecorationModel;
94-
private additionsDeletionsDecorationModel: AdditionsDeletionsDecorationModel;
84+
private multiLineEditsIsVisibleObs: IObservable<boolean>;
85+
9586
private codeEditsSourceCollection: CodeEditsSourceCollection;
9687
private aiNativeContextKey: AINativeContextKey;
97-
private rewriteWidget: RewriteWidget | null;
98-
private multiLineEditsIsVisibleObs: IObservable<boolean>;
88+
private codeEditsPreviewer: CodeEditsPreviewer;
9989

10090
public mount(): IDisposable {
10191
this.handlerAlwaysVisiblePreference();
10292

10393
this.codeEditsResult = observableValue<CodeEditsResultValue | undefined>(this, undefined);
104-
this.multiLineDecorationModel = new MultiLineDecorationModel(this.monacoEditor);
105-
this.additionsDeletionsDecorationModel = new AdditionsDeletionsDecorationModel(this.monacoEditor);
10694
this.aiNativeContextKey = this.injector.get(AINativeContextKey, [this.monacoEditor.contextKeyService]);
95+
this.codeEditsPreviewer = this.injector.get(CodeEditsPreviewer, [this.monacoEditor, this.aiNativeContextKey]);
10796
this.codeEditsSourceCollection = this.injector.get(CodeEditsSourceCollection, [
10897
[LintErrorCodeEditsSource, LineChangeCodeEditsSource, TypingCodeEditsSource, TriggerCodeEditsSource],
10998
this.monacoEditor,
@@ -214,125 +203,10 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
214203
);
215204
}
216205

217-
private destroyRewriteWidget() {
218-
if (this.rewriteWidget) {
219-
this.rewriteWidget.dispose();
220-
this.rewriteWidget = null;
221-
}
222-
}
223-
224-
private applyInlineDecorations(completionModel: CodeEditsResultValue) {
225-
const { items } = completionModel;
226-
const { range, insertText } = items[0];
227-
228-
// code edits 必须提供 range
229-
if (!range) {
230-
return;
231-
}
232-
233-
const position = this.monacoEditor.getPosition()!;
234-
const model = this.monacoEditor.getModel();
235-
const insertTextString = insertText.toString();
236-
const originalContent = model?.getValueInRange(range);
237-
const eol = this.model.getEOL();
238-
239-
const changes = computeMultiLineDiffChanges(
240-
originalContent!,
241-
insertTextString,
242-
this.monacoEditor,
243-
range.startLineNumber,
244-
eol,
245-
);
246-
247-
if (!changes) {
248-
return;
249-
}
250-
251-
const { singleLineCharChanges, charChanges, wordChanges, isOnlyAddingToEachWord } = changes;
252-
253-
// 限制 changes 数量,超过这个数量直接显示智能重写
254-
const maxCharChanges = 20;
255-
const maxWordChanges = 20;
256-
257-
if (
258-
range &&
259-
isOnlyAddingToEachWord &&
260-
charChanges.length <= maxCharChanges &&
261-
wordChanges.length <= maxWordChanges
262-
) {
263-
const modificationsResult = this.multiLineDecorationModel.applyInlineDecorations(
264-
this.monacoEditor,
265-
mergeMultiLineDiffChanges(singleLineCharChanges, eol),
266-
range.startLineNumber,
267-
position,
268-
);
269-
270-
this.aiNativeContextKey.multiLineEditsIsVisible.reset();
271-
this.multiLineDecorationModel.clearDecorations();
272-
273-
if (!modificationsResult) {
274-
this.renderRewriteWidget(wordChanges, model, range, insertTextString);
275-
} else if (modificationsResult && modificationsResult.inlineMods) {
276-
this.aiNativeContextKey.multiLineEditsIsVisible.set(true);
277-
this.multiLineDecorationModel.updateLineModificationDecorations(modificationsResult.inlineMods);
278-
}
279-
} else {
280-
this.additionsDeletionsDecorationModel.updateDeletionsDecoration(wordChanges, range, eol);
281-
this.renderRewriteWidget(wordChanges, model, range, insertTextString);
282-
}
283-
}
284-
285-
private async renderRewriteWidget(
286-
wordChanges: IMultiLineDiffChangeResult[],
287-
model: ITextModel | null,
288-
range: IRange,
289-
insertTextString: string,
290-
) {
291-
this.destroyRewriteWidget();
292-
293-
const cursorPosition = this.monacoEditor.getPosition();
294-
if (!cursorPosition) {
295-
return;
296-
}
297-
298-
this.rewriteWidget = this.injector.get(RewriteWidget, [this.monacoEditor]);
299-
300-
const startOffset = this.model.getOffsetAt({ lineNumber: range.startLineNumber, column: range.startColumn });
301-
const endOffset = this.model.getOffsetAt({ lineNumber: range.endLineNumber, column: range.endColumn });
302-
const allText = this.model.getValue();
303-
// 这里是为了能在 rewrite widget 的 editor 当中完整的复用代码高亮与语法检测的能力
304-
const newVirtualContent = allText.substring(0, startOffset) + insertTextString + allText.substring(endOffset);
305-
306-
const lineChangesMap = wordChangesToLineChangesMap(wordChanges, range, model);
307-
308-
await this.rewriteWidget.defered.promise;
309-
310-
this.aiNativeContextKey.multiLineEditsIsVisible.set(true);
311-
312-
const allLineChanges = Object.values(lineChangesMap).map((lineChanges) => ({
313-
changes: lineChanges
314-
.map((change) => change.filter((item) => item.value.trim() !== empty))
315-
.filter((change) => change.length > 0),
316-
}));
317-
318-
this.rewriteWidget.setInsertText(insertTextString);
319-
this.rewriteWidget.show({ position: cursorPosition });
320-
this.rewriteWidget.setEditArea(range);
321-
322-
if (allLineChanges.every(({ changes }) => changes.every((change) => change.every(({ removed }) => removed)))) {
323-
// 处理全是删除的情况
324-
this.rewriteWidget.renderTextLineThrough(allLineChanges);
325-
} else {
326-
this.rewriteWidget.renderVirtualEditor(newVirtualContent, wordChanges);
327-
}
328-
}
329-
330206
public hide() {
331207
this.cancelToken();
332208
this.aiNativeContextKey.multiLineEditsIsVisible.reset();
333-
this.multiLineDecorationModel.clearDecorations();
334-
this.additionsDeletionsDecorationModel.clearDeletionsDecorations();
335-
this.destroyRewriteWidget();
209+
this.codeEditsPreviewer.hide();
336210
}
337211

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

259+
this.codeEditsPreviewer.discard();
385260
this.hide();
386261
return isValid;
387262
},
@@ -391,27 +266,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
391266
const report = this.reportData.read(reader);
392267
report?.('isReceive');
393268

394-
this.multiLineDecorationModel.accept();
395-
396-
if (this.rewriteWidget) {
397-
this.rewriteWidget.accept();
398-
399-
const virtualEditor = this.rewriteWidget.getVirtualEditor();
400-
// 采纳完之后将 virtualEditor 的 decorations 重新映射在 editor 上
401-
if (virtualEditor) {
402-
const editArea = this.rewriteWidget.getEditArea();
403-
const decorations = virtualEditor.getDecorationsInRange(Range.lift(editArea));
404-
const preAddedDecorations = decorations?.filter(
405-
(decoration) => decoration.options.description === REWRITE_DECORATION_INLINE_ADD,
406-
);
407-
if (preAddedDecorations) {
408-
this.additionsDeletionsDecorationModel.updateAdditionsDecoration(
409-
preAddedDecorations.map((decoration) => decoration.range),
410-
);
411-
}
412-
}
413-
}
414-
269+
this.codeEditsPreviewer.accept();
415270
this.hide();
416271
});
417272

@@ -423,16 +278,6 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
423278
}
424279

425280
private registerFeature(monacoEditor: ICodeEditor): void {
426-
this.featureDisposable.addDispose(
427-
Event.any<any>(
428-
monacoEditor.onDidChangeCursorPosition,
429-
monacoEditor.onDidChangeModelContent,
430-
monacoEditor.onDidBlurEditorWidget,
431-
)(() => {
432-
this.additionsDeletionsDecorationModel.clearAdditionsDecorations();
433-
}),
434-
);
435-
436281
// 监听当前光标位置的变化,如果超出 range 区域则表示弃用
437282
this.featureDisposable.addDispose(
438283
this.monacoEditor.onDidChangeCursorPosition((event: ICursorPositionChangedEvent) => {
@@ -505,7 +350,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
505350
}
506351

507352
try {
508-
this.applyInlineDecorations(completionModel);
353+
this.codeEditsPreviewer.render(completionModel);
509354
} catch (error) {
510355
this.logger.warn('IntelligentCompletionsController applyInlineDecorations error', error);
511356
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Injector } from '@opensumi/di';
2+
import { Disposable } from '@opensumi/ide-core-common';
3+
import { ICodeEditor } from '@opensumi/ide-monaco';
4+
import {
5+
ObservableCodeEditor,
6+
observableCodeEditor,
7+
} from '@opensumi/monaco-editor-core/esm/vs/editor/browser/observableCodeEditor';
8+
9+
import { CodeEditsResultValue } from '../index';
10+
11+
export abstract class BaseCodeEditsView extends Disposable {
12+
protected editorObs: ObservableCodeEditor;
13+
14+
public modelId: string;
15+
16+
constructor(protected readonly monacoEditor: ICodeEditor, protected readonly injector: Injector) {
17+
super();
18+
19+
this.editorObs = observableCodeEditor(this.monacoEditor);
20+
this.mount();
21+
22+
this.addDispose({ dispose: () => this.hide() });
23+
}
24+
25+
protected mount(): void {}
26+
27+
abstract render(completionModel: CodeEditsResultValue): void;
28+
abstract hide(): void;
29+
abstract accept(): void;
30+
abstract discard(): void;
31+
}

0 commit comments

Comments
 (0)