Skip to content

Commit

Permalink
feat: add code edits api reporter (#4118)
Browse files Browse the repository at this point in the history
* feat: add code edits api reporter

* fix: typo
  • Loading branch information
Ricbet authored Oct 25, 2024
1 parent 0dec87c commit d7d8a1d
Show file tree
Hide file tree
Showing 29 changed files with 362 additions and 159 deletions.
12 changes: 6 additions & 6 deletions packages/ai-native/src/browser/chat/chat.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getIcon, useInjectable, useUpdateOnEvent } from '@opensumi/ide-core-bro
import { Popover, PopoverPosition } from '@opensumi/ide-core-browser/lib/components';
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
import {
AISerivceType,
AIServiceType,
ActionSourceEnum,
ActionTypeEnum,
CancellationToken,
Expand Down Expand Up @@ -198,7 +198,7 @@ export const AIChatView = observer(() => {
disposer.addDispose(
chatApiService.onChatReplyMessageLaunch((data) => {
if (data.kind === 'content') {
const relationId = aiReporter.start(AISerivceType.CustomReplay, {
const relationId = aiReporter.start(AIServiceType.CustomReplay, {
message: data.content,
});
msgHistoryManager.addAssistantMessage({
Expand All @@ -207,7 +207,7 @@ export const AIChatView = observer(() => {
});
renderSimpleMarkdownReply({ chunk: data.content, relationId });
} else {
const relationId = aiReporter.start(AISerivceType.CustomReplay, {
const relationId = aiReporter.start(AIServiceType.CustomReplay, {
message: 'component#' + data.component,
});
msgHistoryManager.addAssistantMessage({
Expand All @@ -228,7 +228,7 @@ export const AIChatView = observer(() => {
list.forEach((item) => {
const { role } = item;

const relationId = aiReporter.start(AISerivceType.Chat, {
const relationId = aiReporter.start(AIServiceType.Chat, {
message: '',
});

Expand Down Expand Up @@ -290,7 +290,7 @@ export const AIChatView = observer(() => {
disposer.addDispose(
chatAgentService.onDidSendMessage((chunk) => {
const newChunk = chunk as IChatComponent | IChatContent;
const relationId = aiReporter.start(AISerivceType.Agent, {
const relationId = aiReporter.start(AIServiceType.Agent, {
message: '',
});

Expand Down Expand Up @@ -495,7 +495,7 @@ export const AIChatView = observer(() => {
aiChatService.setLatestRequestId(request.requestId);

const startTime = Date.now();
const reportType = ChatProxyService.AGENT_ID === agentId ? AISerivceType.Chat : AISerivceType.Agent;
const reportType = ChatProxyService.AGENT_ID === agentId ? AIServiceType.Chat : AIServiceType.Agent;
const relationId = aiReporter.start(command || reportType, {
message,
agentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { empty } from '@opensumi/ide-utils/lib/strings';
import { InlineCompletionContextKeys } from '@opensumi/monaco-editor-core/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys';

import { IAIInlineCompletionsProvider } from '../../../common';
import { AINativeContextKey } from '../../contextkey/ai-native.contextkey.service';
import { AINativeContextKey } from '../../ai-core.contextkeys';
import { BaseAIMonacoEditorController } from '../base';
import { IIntelligentCompletionsResult } from '../intelligent-completions';
import { IntelligentCompletionsRegistry } from '../intelligent-completions/intelligent-completions.feature.registry';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
URI,
uuid,
} from '@opensumi/ide-core-common';
import { AISerivceType, IAIReporter } from '@opensumi/ide-core-common/lib/types/ai-native/reporter';
import { AIServiceType, IAIReporter } from '@opensumi/ide-core-common/lib/types/ai-native/reporter';
import { WorkbenchEditorService } from '@opensumi/ide-editor';
import { WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service';
import * as monaco from '@opensumi/ide-monaco';
Expand Down Expand Up @@ -165,7 +165,7 @@ export class InlineCompletionRequestTask extends Disposable {
let completeResult: IIntelligentCompletionsResult | undefined;
const cacheData = this.promptCache.getCache(requestBean);
const relationId =
cacheData?.relationId || this.aiReporter.start(AISerivceType.Completion, { message: AISerivceType.Completion });
cacheData?.relationId || this.aiReporter.start(AIServiceType.Completion, { message: AIServiceType.Completion });

this.aiCompletionsService.setLastRelationId(relationId);
// 如果存在缓存
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Disposable } from '@opensumi/ide-core-common';
import { IRange, InlineCompletion } from '@opensumi/ide-monaco';
import { Disposable, ECodeEditsSourceTyping } from '@opensumi/ide-core-common';
import { IPosition, IRange, InlineCompletion } from '@opensumi/ide-monaco';

import type { ILineChangeData } from './source/line-change.source';
import type { ILinterErrorData } from './source/lint-error.source';
Expand All @@ -12,14 +12,9 @@ export interface IIntelligentCompletionsResult<T = any> {
extra?: T;
}

export enum ECodeEditsSource {
LinterErrors = 'lint_errors',
LineChange = 'line_change',
}

export type ICodeEditsContextBean =
| { typing: ECodeEditsSource.LinterErrors; data: ILinterErrorData }
| { typing: ECodeEditsSource.LineChange; data: ILineChangeData };
| { typing: ECodeEditsSourceTyping.LinterErrors; position: IPosition; data: ILinterErrorData }
| { typing: ECodeEditsSourceTyping.LineChange; position: IPosition; data: ILineChangeData };

export interface ICodeEdit {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@opensumi/ide-core-browser';
import {
AI_MULTI_LINE_COMPLETION_ACCEPT,
AI_MULTI_LINE_COMPLETION_HIDE,
AI_MULTI_LINE_COMPLETION_DISCARD,
} from '@opensumi/ide-core-browser/lib/ai-native/command';
import { MultiLineEditsIsVisible } from '@opensumi/ide-core-browser/lib/contextkey/ai-native';
import { CommandContribution, CommandRegistry, Domain } from '@opensumi/ide-core-common';
Expand All @@ -23,11 +23,11 @@ export class IntelligentCompletionsContribution implements KeybindingContributio
private readonly workbenchEditorService: WorkbenchEditorServiceImpl;

registerCommands(commands: CommandRegistry): void {
commands.registerCommand(AI_MULTI_LINE_COMPLETION_HIDE, {
commands.registerCommand(AI_MULTI_LINE_COMPLETION_DISCARD, {
execute: () => {
const editor = this.workbenchEditorService.currentCodeEditor;
if (editor) {
IntelligentCompletionsController.get(editor.monacoEditor)?.hide();
IntelligentCompletionsController.get(editor.monacoEditor)?.discard.get();
}
},
});
Expand All @@ -36,15 +36,15 @@ export class IntelligentCompletionsContribution implements KeybindingContributio
execute: () => {
const editor = this.workbenchEditorService.currentCodeEditor;
if (editor) {
IntelligentCompletionsController.get(editor.monacoEditor)?.accept();
IntelligentCompletionsController.get(editor.monacoEditor)?.accept.get();
}
},
});
}

registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: AI_MULTI_LINE_COMPLETION_HIDE.id,
command: AI_MULTI_LINE_COMPLETION_DISCARD.id,
keybinding: Key.ESCAPE.code,
when: MultiLineEditsIsVisible.raw,
priority: 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@ import { Key, KeybindingRegistry, KeybindingScope, PreferenceService } from '@op
import { MultiLineEditsIsVisible } from '@opensumi/ide-core-browser/lib/contextkey/ai-native';
import {
AINativeSettingSectionsId,
CodeEditsRT,
Disposable,
Event,
IDisposable,
ILogger,
IntelligentCompletionsRegistryToken,
runWhenIdle,
} from '@opensumi/ide-core-common';
import { ICodeEditor, ICursorPositionChangedEvent, IRange, ITextModel, Range } from '@opensumi/ide-monaco';
import {
ISettableObservable,
ObservableValue,
autorun,
autorunWithStoreHandleChanges,
derived,
observableValue,
transaction,
} from '@opensumi/ide-monaco/lib/common/observable';
import { empty } from '@opensumi/ide-utils/lib/strings';
import { autorun, transaction } from '@opensumi/monaco-editor-core/esm/vs/base/common/observable';
import { ObservableValue } from '@opensumi/monaco-editor-core/esm/vs/base/common/observableInternal/base';
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/commandIds';
import { InlineCompletionContextKeys } from '@opensumi/monaco-editor-core/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys';
Expand All @@ -23,7 +32,7 @@ import {
import { SuggestController } from '@opensumi/monaco-editor-core/esm/vs/editor/contrib/suggest/browser/suggestController';
import { ContextKeyExpr } from '@opensumi/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';

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

Expand Down Expand Up @@ -65,19 +74,31 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
return this.injector.get(IntelligentCompletionsRegistryToken);
}

private get logger(): ILogger {
return this.injector.get(ILogger);
}

private codeEditsResult: ISettableObservable<CodeEditsResultValue | undefined>;
private multiLineDecorationModel: MultiLineDecorationModel;
private additionsDeletionsDecorationModel: AdditionsDeletionsDecorationModel;
private codeEditsSourceCollection: CodeEditsSourceCollection;
private aiNativeContextKey: AINativeContextKey;
private rewriteWidget: RewriteWidget | null;
private whenMultiLineEditsVisibleDisposable: Disposable;

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

this.codeEditsResult = observableValue<CodeEditsResultValue | undefined>(this, undefined);

this.whenMultiLineEditsVisibleDisposable = new Disposable();
this.multiLineDecorationModel = new MultiLineDecorationModel(this.monacoEditor);
this.additionsDeletionsDecorationModel = new AdditionsDeletionsDecorationModel(this.monacoEditor);
this.aiNativeContextKey = this.injector.get(AINativeContextKey, [this.monacoEditor.contextKeyService]);
this.codeEditsSourceCollection = this.injector.get(CodeEditsSourceCollection, [
[LintErrorCodeEditsSource, LineChangeCodeEditsSource],
this.monacoEditor,
]);

this.registerFeature(this.monacoEditor);
return this.featureDisposable;
Expand Down Expand Up @@ -219,15 +240,15 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
const maxWordChanges = 20;

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

Expand All @@ -248,15 +269,15 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
if (this.whenMultiLineEditsVisibleDisposable.disposed) {
this.whenMultiLineEditsVisibleDisposable = new Disposable();
}
// 监听当前光标位置的变化,如果超出 range 区域则取消 multiLine edits
// 监听当前光标位置的变化,如果超出 range 区域则表示弃用
this.whenMultiLineEditsVisibleDisposable.addDispose(
this.monacoEditor.onDidChangeCursorPosition((event: ICursorPositionChangedEvent) => {
const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get();
if (isVisible) {
const position = event.position;
if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) {
runWhenIdle(() => {
this.hide();
this.discard.get();
});
}
} else {
Expand Down Expand Up @@ -319,7 +340,46 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
this.destroyRewriteWidget();
}

public accept() {
private readonly reportData = derived(this, (reader) => {
const contextBean = this.codeEditsSourceCollection.codeEditsContextBean.read(reader);
const codeEditsResult = this.codeEditsResult.read(reader);
if (contextBean && codeEditsResult) {
const { range, insertText } = codeEditsResult.items[0];
const newCode = insertText;
const originCode = this.model.getValueInRange(range);
return (type: keyof Pick<CodeEditsRT, 'accept' | 'discard' | 'isCancel'>) => {
contextBean.reporterEnd({
[type]: true,
code: newCode,
originCode,
});
};
}
});

private lastVisibleTime = derived(this, (reader) => {
const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get();
return isVisible ? Date.now() : undefined;
});

public discard = derived(this, (reader) => {
const lastVisibleTime = this.lastVisibleTime.read(reader);
const report = this.reportData.read(reader);

// 在可见的情况下超过 750ms 弃用才算有效数据,否则视为取消
if (lastVisibleTime && Date.now() - lastVisibleTime > 750) {
report?.('discard');
} else {
report?.('isCancel');
}

this.hide();
});

public accept = derived(this, (reader) => {
const report = this.reportData.read(reader);
report?.('accept');

this.multiLineDecorationModel.accept();

if (this.rewriteWidget) {
Expand All @@ -342,7 +402,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}

this.hide();
}
});

private registerFeature(monacoEditor: ICodeEditor): void {
this.featureDisposable.addDispose(
Expand Down Expand Up @@ -379,38 +439,56 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}),
);

const codeEditsSourceCollection = this.injector.get(CodeEditsSourceCollection, [
[LintErrorCodeEditsSource, LineChangeCodeEditsSource],
this.monacoEditor,
]);
this.featureDisposable.addDispose(
autorunWithStoreHandleChanges(
{
createEmptyChangeSummary: () => ({}),
handleChange: (context, changeSummary) => {
if (context.didChange(this.codeEditsSourceCollection.codeEditsContextBean)) {
// 如果上一次补全结果还在,则不重复请求
const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get();
return !isVisible;
}
return false;
},
},
async (reader, _, store) => {
const context = this.codeEditsSourceCollection.codeEditsContextBean.read(reader);

codeEditsSourceCollection.mount();
const provider = this.intelligentCompletionsRegistry.getCodeEditsProvider();
if (context && provider) {
// 新的请求进来且上一次的请求还在继续时,则取消掉上一次的请求
store.add(Disposable.create(() => context.cancelToken()));

this.featureDisposable.addDispose(
autorun(async (reader) => {
const context = codeEditsSourceCollection.codeEditsContextBean.read(reader);
context.reporterStart();

// 如果上一次补全结果还在,则不重复请求
const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get();
if (isVisible) {
const result = await provider(this.monacoEditor, context.position, context.bean, context.token);

if (result && result.items.length > 0) {
transaction((tx) => {
this.codeEditsResult.set(new CodeEditsResultValue(result), tx);
});
}
}
},
),
);

this.featureDisposable.addDispose(
autorun((reader) => {
const completionModel = this.codeEditsResult.read(reader);
if (!completionModel) {
return;
}

const provider = this.intelligentCompletionsRegistry.getCodeEditsProvider();
if (context && provider) {
const result = await provider(
this.monacoEditor,
this.monacoEditor.getPosition()!,
context.bean,
codeEditsSourceCollection.token,
);
if (result && result.items.length > 0) {
this.applyInlineDecorations(new CodeEditsResultValue(result));
}
try {
this.applyInlineDecorations(completionModel);
} catch (error) {
this.logger.warn('IntelligentCompletionsController applyInlineDecorations error', error);
}
}),
);

this.featureDisposable.addDispose(codeEditsSourceCollection);
this.featureDisposable.addDispose(this.codeEditsSourceCollection);
}
}
Loading

0 comments on commit d7d8a1d

Please sign in to comment.