Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve line change code edits trigger rule #4373

Merged
merged 2 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/ai-native/src/browser/ai-core.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export class AINativeBrowserContribution
register(
IntelligentCompletionsController.ID,
new SyncDescriptor(IntelligentCompletionsController, [this.injector]),
EditorContributionInstantiation.AfterFirstRender,
EditorContributionInstantiation.Eager,
);
register(
InlineCompletionsController.ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { IModelContentChangedEvent, IPosition, IRange, InlineCompletion } from '
import type { ILineChangeData } from './source/line-change.source';
import type { ILinterErrorData } from './source/lint-error.source';

/**
* 有效弃用时间(毫秒)
* 在可见的情况下超过 750ms 弃用才算有效数据,否则视为无效数据
*/
export const VALID_TIME = 750;

export interface IIntelligentCompletionsResult<T = any> {
readonly items: InlineCompletion[];
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Key, KeybindingRegistry, KeybindingScope, PreferenceService } from '@opensumi/ide-core-browser';
import {
ContextKeyChangeEvent,
Key,
KeybindingRegistry,
KeybindingScope,
PreferenceService,
} from '@opensumi/ide-core-browser';
import { MultiLineEditsIsVisible } from '@opensumi/ide-core-browser/lib/contextkey/ai-native';
import {
AINativeSettingSectionsId,
Expand All @@ -19,6 +25,9 @@ import {
autorun,
autorunWithStoreHandleChanges,
derived,
derivedHandleChanges,
derivedOpts,
observableFromEvent,
observableSignal,
observableValue,
transaction,
Expand Down Expand Up @@ -52,7 +61,7 @@ import { LineChangeCodeEditsSource } from './source/line-change.source';
import { LintErrorCodeEditsSource } from './source/lint-error.source';
import { TypingCodeEditsSource } from './source/typing.source';

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

export class IntelligentCompletionsController extends BaseAIMonacoEditorController {
public static readonly ID = 'editor.contrib.ai.intelligent.completions';
Expand Down Expand Up @@ -87,16 +96,15 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
private codeEditsSourceCollection: CodeEditsSourceCollection;
private aiNativeContextKey: AINativeContextKey;
private rewriteWidget: RewriteWidget | null;
private whenMultiLineEditsVisibleDisposable: Disposable;
private codeEditsTriggerSignal: IObservableSignal<void>;
private multiLineEditsIsVisibleObs: IObservable<boolean>;

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

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

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]);
Expand All @@ -105,6 +113,15 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
this.monacoEditor,
]);

const multiLineEditsIsVisibleKey = new Set([MultiLineEditsIsVisible.raw]);
this.multiLineEditsIsVisibleObs = observableFromEvent(
this,
Event.filter(this.aiNativeContextKey.contextKeyService!.onDidChangeContext, (e: ContextKeyChangeEvent) =>
e.payload.affectsSome(multiLineEditsIsVisibleKey),
),
() => !!this.aiNativeContextKey.multiLineEditsIsVisible.get(),
);

this.registerFeature(this.monacoEditor);
return this.featureDisposable;
}
Expand Down Expand Up @@ -259,26 +276,6 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
this.additionsDeletionsDecorationModel.updateDeletionsDecoration(wordChanges, range, eol);
this.renderRewriteWidget(wordChanges, model, range, insertTextString);
}

if (this.whenMultiLineEditsVisibleDisposable.disposed) {
this.whenMultiLineEditsVisibleDisposable = new Disposable();
}
// 监听当前光标位置的变化,如果超出 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.discard.get();
});
}
} else {
this.whenMultiLineEditsVisibleDisposable.dispose();
}
}),
);
}

private async renderRewriteWidget(
Expand Down Expand Up @@ -341,36 +338,52 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
const { range, insertText } = codeEditsResult.items[0];
const newCode = insertText;
const originCode = this.model.getValueInRange(range);
return (type: keyof Pick<CodeEditsRT, 'isReceive' | 'isDrop' | 'isValid'>) => {
contextBean.reporterEnd({
[type]: true,
return (type: keyof Pick<CodeEditsRT, 'isReceive' | 'isDrop' | 'isValid'>, defaultValue: boolean = true) => {
const data = {
[type]: defaultValue,
code: newCode,
originCode,
});
};

contextBean.reporterEnd(data);
};
}
});

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?.('isDrop');
} else {
report?.('isValid');
}
public discard = derivedHandleChanges(
{
owner: this,
createEmptyChangeSummary: () => ({ lastVisibleTime: Date.now() }),
handleChange: (context, changeSummary) => {
if (context.didChange(this.multiLineEditsIsVisibleObs)) {
changeSummary.lastVisibleTime = Date.now();
return this.multiLineEditsIsVisibleObs.get();
}
return false;
},
equalityComparer: () => false,
},
(reader, changeSummary) => {
this.multiLineEditsIsVisibleObs.read(reader);

const lastVisibleTime = changeSummary.lastVisibleTime;
const report = this.reportData.read(reader);
let isValid = false;

if (lastVisibleTime && Date.now() - lastVisibleTime > VALID_TIME) {
isValid = true;
report?.('isDrop');
} else {
isValid = false;
report?.('isValid', false);
}

this.hide();
});
this.hide();
return isValid;
},
);

public accept = derived(this, (reader) => {
public accept = derivedOpts({ owner: this, equalsFn: () => false }, (reader) => {
const report = this.reportData.read(reader);
report?.('isReceive');

Expand Down Expand Up @@ -413,14 +426,19 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}),
);

const multiLineEditsIsVisibleKey = new Set([MultiLineEditsIsVisible.raw]);
this.featureDisposable.addDispose(this.whenMultiLineEditsVisibleDisposable);
// 监听当前光标位置的变化,如果超出 range 区域则表示弃用
this.featureDisposable.addDispose(
this.aiNativeContextKey.contextKeyService!.onDidChangeContext((e) => {
if (e.payload.affectsSome(multiLineEditsIsVisibleKey)) {
const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get();
if (!isVisible) {
this.whenMultiLineEditsVisibleDisposable.dispose();
this.monacoEditor.onDidChangeCursorPosition((event: ICursorPositionChangedEvent) => {
const isVisible = this.multiLineEditsIsVisibleObs.get();
const completionModel = this.codeEditsResult.get();

if (isVisible && completionModel) {
const position = event.position;
const range = completionModel.items[0].range;
if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) {
runWhenIdle(() => {
this.discard.get();
});
}
}
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { Injectable } from '@opensumi/di';
import { AINativeSettingSectionsId, ECodeEditsSourceTyping, IDisposable } from '@opensumi/ide-core-common';
import { ICursorPositionChangedEvent } from '@opensumi/ide-monaco';
import { autorunDelta, observableFromEvent } from '@opensumi/ide-monaco/lib/common/observable';
import { ICursorPositionChangedEvent, IModelContentChangedEvent } from '@opensumi/ide-monaco';
import {
autorunDelta,
derived,
derivedHandleChanges,
observableFromEvent,
onObservableChange,
} from '@opensumi/ide-monaco/lib/common/observable';

import { IntelligentCompletionsController } from '../intelligent-completions.controller';

import { BaseCodeEditsSource } from './base';

Expand All @@ -10,24 +18,89 @@ export interface ILineChangeData {
preLineNumber?: number;
}

const DEPRECATED_LIMIT = 5;
const CONTENT_CHANGE_VALID_TIME = 60 * 1000;

@Injectable({ multiple: true })
export class LineChangeCodeEditsSource extends BaseCodeEditsSource {
public priority = 1;

public mount(): IDisposable {
const positionChangeObs = observableFromEvent<ICursorPositionChangedEvent>(
this,
this.monacoEditor.onDidChangeCursorPosition,
(event: ICursorPositionChangedEvent) => event,
);
/**
* 在当前文件,计算弃用上次 edit 时的次数是否超过了阈值 {@link DEPRECATED_LIMIT} 次,超过则不会触发
* 1. 直接 esc 弃用
* 2. 用户再次移动光标位置致使补全消失也视为弃用
*/
private readonly deprecatedStore = new Map<string, number>();

private readonly positionChangeObs = observableFromEvent<ICursorPositionChangedEvent>(
this,
this.monacoEditor.onDidChangeCursorPosition,
(event: ICursorPositionChangedEvent) => event,
);

private readonly contentChangeObs = observableFromEvent<IModelContentChangedEvent>(
this,
this.monacoEditor.onDidChangeModelContent,
(event: IModelContentChangedEvent) => event,
);

private readonly latestContentChangeTimeObs = derivedHandleChanges(
{
owner: this,
createEmptyChangeSummary: () => ({ latestContentChangeTime: 0 }),
handleChange: (context, changeSummary) => {
if (context.didChange(this.contentChangeObs)) {
changeSummary.latestContentChangeTime = Date.now();
}
return true;
},
},
(reader, changeSummary) => {
this.contentChangeObs.read(reader);
return changeSummary.latestContentChangeTime;
},
);

private readonly isAllowTriggerObs = derived((reader) => {
this.positionChangeObs.read(reader);
const latestContentChangeTime = this.latestContentChangeTimeObs.read(reader);

const isLineChangeEnabled = this.preferenceService.getValid(AINativeSettingSectionsId.CodeEditsLineChange, false);

/**
* 配置开关
*/
if (!isLineChangeEnabled) {
return false;
}

/**
* 弃用次数规则的限制
*/
const deprecatedCount = this.deprecatedStore.get(this.model?.id || '');
if (deprecatedCount && deprecatedCount >= DEPRECATED_LIMIT) {
return false;
}

/**
* 1. 未编辑过代码不触发
* 2. 编辑过代码后,60 内没有再次编辑也不触发
*/
if (
latestContentChangeTime === 0 ||
(latestContentChangeTime && Date.now() - latestContentChangeTime > CONTENT_CHANGE_VALID_TIME)
) {
return false;
}

return true;
});

public mount(): IDisposable {
this.addDispose(
autorunDelta(positionChangeObs, ({ lastValue, newValue }) => {
const isLineChangeEnabled = this.preferenceService.getValid(
AINativeSettingSectionsId.CodeEditsLineChange,
false,
);
if (!isLineChangeEnabled) {
autorunDelta(this.positionChangeObs, ({ lastValue, newValue }, reader) => {
const isAllowTriggerObs = this.isAllowTriggerObs.read(reader);
if (!isAllowTriggerObs) {
return false;
}

Expand All @@ -47,6 +120,35 @@ export class LineChangeCodeEditsSource extends BaseCodeEditsSource {
}),
);

const discard = IntelligentCompletionsController.get(this.monacoEditor)?.discard;
const accept = IntelligentCompletionsController.get(this.monacoEditor)?.accept;
if (discard) {
this.addDispose(
onObservableChange(discard, (isValid: boolean) => {
const modelId = this.model?.id;
if (!modelId || !isValid) {
return;
}

const count = this.deprecatedStore.get(modelId) || 0;
this.deprecatedStore.set(modelId, count + 1);
}),
);
}

if (accept) {
this.addDispose(
onObservableChange(accept, () => {
const modelId = this.model?.id;
if (!modelId) {
return;
}

this.deprecatedStore.delete(modelId);
}),
);
}

return this;
}
}
Loading
Loading