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: implement line change code edits api #4106

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IRange, InlineCompletion } from '@opensumi/ide-monaco';

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

export interface IIntelligentCompletionsResult<T = any> {
readonly items: InlineCompletion[];
Expand All @@ -12,12 +13,18 @@ export interface IIntelligentCompletionsResult<T = any> {

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

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

export interface ICodeEdit {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import {
mergeMultiLineDiffChanges,
wordChangesToLineChangesMap,
} from './diff-computer';
import { LintErrorCodeEditsSource } from './lint-error.source';

import { ICodeEditsResult } from '.';
import { CodeEditsResultValue, CodeEditsSourceCollection } from './source/base';
import { LineChangeCodeEditsSource } from './source/line-change.source';
import { LintErrorCodeEditsSource } from './source/lint-error.source';

export class IntelligentCompletionsController extends BaseAIMonacoEditorController {
public static readonly ID = 'editor.contrib.ai.intelligent.completions';
Expand Down Expand Up @@ -171,7 +171,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}
}

public async fetchProvider(editsResult: ICodeEditsResult): Promise<void> {
private async fetchProvider(editsResult: CodeEditsResultValue): Promise<void> {
// 如果上一次补全结果还在,则不重复请求
const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get();
if (isVisible) {
Expand All @@ -183,7 +183,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}
}

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

Expand Down Expand Up @@ -377,7 +377,22 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll
}),
);

const lintErrorCodeEditsSource = this.injector.get(LintErrorCodeEditsSource, [this.monacoEditor, this.token]);
this.featureDisposable.addDispose(lintErrorCodeEditsSource.mount());
const codeEditsSourceCollection = this.injector.get(CodeEditsSourceCollection, [
[LintErrorCodeEditsSource, LineChangeCodeEditsSource],
this.monacoEditor,
]);

codeEditsSourceCollection.mount();

this.featureDisposable.addDispose(
autorun((reader) => {
const source = codeEditsSourceCollection.codeEditsResult.read(reader);
if (source) {
this.fetchProvider(source);
}
}),
);

this.featureDisposable.addDispose(codeEditsSourceCollection);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Autowired, INJECTOR_TOKEN, Injectable, Injector, Optional } from '@opensumi/di';
import {
CancellationTokenSource,
Disposable,
IDisposable,
IntelligentCompletionsRegistryToken,
MaybePromise,
uuid,
} from '@opensumi/ide-core-common';
import { ConstructorOf } from '@opensumi/ide-core-common';
import { ICodeEditor, IPosition } from '@opensumi/ide-monaco';
import { DisposableStore } from '@opensumi/monaco-editor-core/esm/vs/base/common/lifecycle';
import {
autorunDelta,
debouncedObservable,
derived,
disposableObservableValue,
transaction,
} from '@opensumi/monaco-editor-core/esm/vs/base/common/observable';

import { ICodeEdit, ICodeEditsContextBean, ICodeEditsResult } from '../index';
import { IntelligentCompletionsRegistry } from '../intelligent-completions.feature.registry';

@Injectable({ multiple: true })
export abstract class BaseCodeEditsSource extends Disposable {
@Autowired(IntelligentCompletionsRegistryToken)
private readonly intelligentCompletionsRegistry: IntelligentCompletionsRegistry;

protected abstract doTrigger(...args: any[]): MaybePromise<void>;
Ricbet marked this conversation as resolved.
Show resolved Hide resolved

public readonly codeEditsResult = disposableObservableValue<CodeEditsResultValue | undefined>(this, undefined);

public abstract priority: number;
public abstract mount(): IDisposable;

constructor(@Optional() protected readonly monacoEditor: ICodeEditor) {
super();
}

protected cancellationTokenSource = new CancellationTokenSource();

protected get model() {
return this.monacoEditor.getModel();
}
Ricbet marked this conversation as resolved.
Show resolved Hide resolved

protected get token() {
return this.cancellationTokenSource.token;
}

public cancelToken() {
this.cancellationTokenSource.cancel();
this.cancellationTokenSource = new CancellationTokenSource();
}
Ricbet marked this conversation as resolved.
Show resolved Hide resolved

protected resetCodeEditsResult = derived(this, () => {
transaction((tx) => {
this.codeEditsResult.set(undefined, tx);
});
});

protected async launchProvider(editor: ICodeEditor, position: IPosition, bean: ICodeEditsContextBean): Promise<void> {
const provider = this.intelligentCompletionsRegistry.getCodeEditsProvider();
if (provider) {
const result = await provider(editor, position, bean, this.token);

if (result) {
const codeEditsResultValue = new CodeEditsResultValue(result, this);

transaction((tx) => {
this.codeEditsResult.set(codeEditsResultValue, tx);
});
}
}
}
Ricbet marked this conversation as resolved.
Show resolved Hide resolved
}

export class CodeEditsResultValue extends Disposable {
public readonly uid = uuid(6);

constructor(private readonly raw: ICodeEditsResult, private readonly source: BaseCodeEditsSource) {
super();
}

public get items(): ICodeEdit[] {
return this.raw.items;
}

public get priority(): number {
return this.source.priority;
}
}

@Injectable({ multiple: true })
export class CodeEditsSourceCollection extends Disposable {
@Autowired(INJECTOR_TOKEN)
private readonly injector: Injector;

public readonly codeEditsResult = disposableObservableValue<CodeEditsResultValue | undefined>(this, undefined);

constructor(
private readonly constructorSources: ConstructorOf<BaseCodeEditsSource>[],
private readonly monacoEditor: ICodeEditor,
) {
super();
}

public mount() {
const sources = this.constructorSources.map((source) => this.injector.get(source, [this.monacoEditor]));

sources.forEach((source) => this.addDispose(source.mount()));

const store = this.registerDispose(new DisposableStore());

// 观察所有 source 的 codeEditsResult
const observerCodeEditsResult = derived((reader) => ({
codeEditsResults: new Map(sources.map((source) => [source, source.codeEditsResult.read(reader)])),
}));

this.addDispose(
autorunDelta(
// 这里需要做 debounce 0 处理,将多次连续的事务通知合并为一次
debouncedObservable(observerCodeEditsResult, 0, store),
({ lastValue, newValue }) => {
// 只拿最新的订阅值,如果 uid 相同,表示该值没有变化,就不用往下通知了
const lastSources = sources.filter((source) => {
const newValueResult = newValue?.codeEditsResults.get(source);
const lastValueResult = lastValue?.codeEditsResults?.get(source);
return newValueResult && (!lastValueResult || newValueResult.uid !== lastValueResult.uid);
});

let highestPriority = 0;
let currentResult: CodeEditsResultValue | undefined;

for (const source of lastSources) {
const value = source.codeEditsResult.get();

if (value && value.priority > highestPriority) {
highestPriority = value.priority;
currentResult = value;
}
}
Ricbet marked this conversation as resolved.
Show resolved Hide resolved

transaction((tx) => {
// 只通知最高优先级的结果
this.codeEditsResult.set(currentResult, tx);
});
},
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Injectable } from '@opensumi/di';
import { IDisposable } from '@opensumi/ide-core-common';
import { ICursorPositionChangedEvent, Position } from '@opensumi/ide-monaco';

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

import { BaseCodeEditsSource } from './base';

export interface ILineChangeData {
currentLineNumber: number;
preLineNumber?: number;
}

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

private prePosition = this.monacoEditor.getPosition();

public mount(): IDisposable {
this.addDispose(
this.monacoEditor.onDidChangeCursorPosition((event: ICursorPositionChangedEvent) => {
const currentPosition = event.position;
if (this.prePosition && this.prePosition.lineNumber !== currentPosition.lineNumber) {
this.doTrigger(currentPosition);
this.prePosition = currentPosition;
}
}),
);
return this;
}

private lastEditTime: number | null = null;
protected doTrigger(position: Position) {
if (!position) {
return;
}

// 如果在 60 秒内再次编辑代码,则不触发
const currentTime = Date.now();
if (this.lastEditTime && currentTime - this.lastEditTime < 60 * 1000) {
return;
}

this.lastEditTime = currentTime;
this.launchProvider(this.monacoEditor, position, {
typing: ECodeEditsSource.LineChange,
data: {
preLineNumber: this.prePosition?.lineNumber,
currentLineNumber: position.lineNumber,
},
});
}
Ricbet marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { Autowired, Injectable, Optional } from '@opensumi/di';
import {
CancellationToken,
Disposable,
IDisposable,
IntelligentCompletionsRegistryToken,
} from '@opensumi/ide-core-common';
import { ICodeEditor, ICursorPositionChangedEvent, IPosition, Position } from '@opensumi/ide-monaco';
import { Autowired, Injectable } from '@opensumi/di';
import { IDisposable } from '@opensumi/ide-core-common';
import { ICursorPositionChangedEvent, IPosition, Position } from '@opensumi/ide-monaco';
import { URI } from '@opensumi/ide-monaco/lib/browser/monaco-api';
import { IWorkspaceService } from '@opensumi/ide-workspace';
import { StandaloneServices } from '@opensumi/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
Expand All @@ -16,10 +11,9 @@ import {
MarkerSeverity,
} from '@opensumi/monaco-editor-core/esm/vs/platform/markers/common/markers';

import { IntelligentCompletionsController } from './intelligent-completions.controller';
import { IntelligentCompletionsRegistry } from './intelligent-completions.feature.registry';
import { ECodeEditsSource } from '..';

Ricbet marked this conversation as resolved.
Show resolved Hide resolved
import { ECodeEditsSource } from '.';
import { BaseCodeEditsSource } from './base';

export interface ILinterErrorData {
relativeWorkspacePath: string;
Expand Down Expand Up @@ -59,24 +53,12 @@ namespace MarkerErrorData {
}

@Injectable({ multiple: true })
export class LintErrorCodeEditsSource extends Disposable {
@Autowired(IntelligentCompletionsRegistryToken)
private readonly intelligentCompletionsRegistry: IntelligentCompletionsRegistry;
export class LintErrorCodeEditsSource extends BaseCodeEditsSource {
public priority = 1;

@Autowired(IWorkspaceService)
private readonly workspaceService: IWorkspaceService;

private get model() {
return this.monacoEditor.getModel();
}

constructor(
@Optional() private readonly monacoEditor: ICodeEditor,
@Optional() private readonly token: CancellationToken,
) {
super();
}

public mount(): IDisposable {
let prePosition = this.monacoEditor.getPosition();

Expand All @@ -93,7 +75,7 @@ export class LintErrorCodeEditsSource extends Disposable {
return this;
}

private async doTrigger(position: Position) {
protected async doTrigger(position: Position) {
if (!this.model) {
return;
}
Expand All @@ -105,26 +87,15 @@ export class LintErrorCodeEditsSource extends Disposable {
markers = markers.filter((marker) => Math.abs(marker.startLineNumber - position.lineNumber) <= 1);

if (markers.length) {
const provider = this.intelligentCompletionsRegistry.getCodeEditsProvider();
if (provider) {
const relativeWorkspacePath = await this.workspaceService.asRelativePath(resource.path);
const result = await provider(
this.monacoEditor,
position,
{
typing: ECodeEditsSource.LinterErrors,
data: {
relativeWorkspacePath: relativeWorkspacePath?.path ?? resource.path,
errors: markers.map((marker) => MarkerErrorData.toData(marker)),
},
},
this.token,
);
const relativeWorkspacePath = await this.workspaceService.asRelativePath(resource.path);

if (result) {
IntelligentCompletionsController.get(this.monacoEditor)?.fetchProvider(result);
}
}
this.launchProvider(this.monacoEditor, position, {
typing: ECodeEditsSource.LinterErrors,
data: {
relativeWorkspacePath: relativeWorkspacePath?.path ?? resource.path,
errors: markers.map((marker) => MarkerErrorData.toData(marker)),
},
});
}
}
}
Loading