diff --git a/packages/ai-native/__test__/browser/contrib/intelligent-completions/multi-line.decoration.test.ts b/packages/ai-native/__test__/browser/contrib/intelligent-completions/multi-line.decoration.test.ts index 3bb600891f..263e8824b4 100644 --- a/packages/ai-native/__test__/browser/contrib/intelligent-completions/multi-line.decoration.test.ts +++ b/packages/ai-native/__test__/browser/contrib/intelligent-completions/multi-line.decoration.test.ts @@ -2,13 +2,12 @@ import { GHOST_TEXT, GHOST_TEXT_DESCRIPTION, MultiLineDecorationModel, -} from '@opensumi/ide-ai-native/lib/browser/contrib/intelligent-completions/multi-line.decoration'; +} from '@opensumi/ide-ai-native/lib/browser/contrib/intelligent-completions/decoration/multi-line.decoration'; +import { IMultiLineDiffChangeResult } from '@opensumi/ide-ai-native/lib/browser/contrib/intelligent-completions/diff-computer'; +import { EnhanceDecorationsCollection } from '@opensumi/ide-ai-native/lib/browser/model/enhanceDecorationsCollection'; import { ICodeEditor, IPosition } from '@opensumi/ide-monaco'; import { monacoApi } from '@opensumi/ide-monaco/lib/browser/monaco-api'; -import { IMultiLineDiffChangeResult } from '../../../../src/browser/contrib/intelligent-completions/diff-computer'; -import { EnhanceDecorationsCollection } from '../../../../src/browser/model/enhanceDecorationsCollection'; - describe('MultiLineDecorationModel', () => { let editor: ICodeEditor; let decorationsCollection: EnhanceDecorationsCollection; diff --git a/packages/ai-native/src/browser/ai-core.contribution.ts b/packages/ai-native/src/browser/ai-core.contribution.ts index ab5e66e0ec..5b85a47c7a 100644 --- a/packages/ai-native/src/browser/ai-core.contribution.ts +++ b/packages/ai-native/src/browser/ai-core.contribution.ts @@ -76,6 +76,7 @@ import { ChatProxyService } from './chat/chat-proxy.service'; import { AIChatView } from './chat/chat.view'; import { CodeActionSingleHandler } from './contrib/code-action/code-action.handler'; import { AIInlineCompletionsProvider } from './contrib/inline-completions/completeProvider'; +import { InlineCompletionsController } from './contrib/inline-completions/inline-completions.controller'; import { AICompletionsService } from './contrib/inline-completions/service/ai-completions.service'; import { IntelligentCompletionsController } from './contrib/intelligent-completions/intelligent-completions.controller'; import { ProblemFixController } from './contrib/problem-fix/problem-fix.controller'; @@ -255,6 +256,11 @@ export class AINativeBrowserContribution new SyncDescriptor(IntelligentCompletionsController, [this.injector]), EditorContributionInstantiation.AfterFirstRender, ); + register( + InlineCompletionsController.ID, + new SyncDescriptor(InlineCompletionsController, [this.injector]), + EditorContributionInstantiation.AfterFirstRender, + ); } if (supportsProblemFix) { register( diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.source.ts b/packages/ai-native/src/browser/contrib/inline-completions/inline-completions.controller.ts similarity index 66% rename from packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.source.ts rename to packages/ai-native/src/browser/contrib/inline-completions/inline-completions.controller.ts index 159b5ba48a..d433e1de77 100644 --- a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.source.ts +++ b/packages/ai-native/src/browser/contrib/inline-completions/inline-completions.controller.ts @@ -1,13 +1,13 @@ import debounce from 'lodash/debounce'; -import { Autowired, INJECTOR_TOKEN, Injectable, Injector, Optional } from '@opensumi/di'; import { AI_INLINE_COMPLETION_VISIBLE } from '@opensumi/ide-core-browser/lib/ai-native/command'; import { CommandService, CommandServiceImpl, - Disposable, + IAICompletionOption, IDisposable, IEventBus, + IntelligentCompletionsRegistryToken, Sequencer, runWhenIdle, } from '@opensumi/ide-core-common'; @@ -18,28 +18,40 @@ 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 { BaseAIMonacoEditorController } from '../base'; +import { IIntelligentCompletionsResult } from '../intelligent-completions'; +import { IntelligentCompletionsRegistry } from '../intelligent-completions/intelligent-completions.feature.registry'; -import { IIntelligentCompletionsResult } from './intelligent-completions'; +export class InlineCompletionsController extends BaseAIMonacoEditorController { + public static readonly ID = 'editor.contrib.ai.inline.completions'; -@Injectable({ multiple: true }) -export class InlineCompletionsSource extends Disposable { - @Autowired(INJECTOR_TOKEN) - protected readonly injector: Injector; + public static get(editor: ICodeEditor): InlineCompletionsController | null { + return editor.getContribution(InlineCompletionsController.ID); + } + + private get eventBus(): IEventBus { + return this.injector.get(IEventBus); + } - @Autowired(IEventBus) - private eventBus: IEventBus; + private get commandService(): CommandServiceImpl { + return this.injector.get(CommandService); + } - @Autowired(CommandService) - private commandService: CommandServiceImpl; + private get aiInlineCompletionsProvider(): IAIInlineCompletionsProvider { + return this.injector.get(IAIInlineCompletionsProvider); + } - @Autowired(IAIInlineCompletionsProvider) - private readonly aiInlineCompletionsProvider: IAIInlineCompletionsProvider; + private get intelligentCompletionsRegistry(): IntelligentCompletionsRegistry { + return this.injector.get(IntelligentCompletionsRegistryToken); + } + private aiNativeContextKey: AINativeContextKey; private sequencer = new Sequencer(); private preDidShowItems: InlineCompletions | undefined; - constructor(@Optional() private readonly monacoEditor: ICodeEditor) { - super(); + public mount(): IDisposable { + this.aiNativeContextKey = this.injector.get(AINativeContextKey, [this.monacoEditor.contextKeyService]); // 判断用户是否选择了一块区域或者移动光标 取消掉请补全请求 const selectionChange = () => { @@ -62,7 +74,7 @@ export class InlineCompletionsSource extends Disposable { }); const inlineVisibleKey = new Set([InlineCompletionContextKeys.inlineSuggestionVisible.key]); - this.addDispose( + this.featureDisposable.addDispose( this.monacoEditor.contextKeyService.onDidChangeContext((e) => { // inline completion 真正消失时 if (e.affectsSome(inlineVisibleKey)) { @@ -79,7 +91,7 @@ export class InlineCompletionsSource extends Disposable { }), ); - this.addDispose( + this.featureDisposable.addDispose( this.eventBus.on(EditorSelectionChangeEvent, (e) => { if (e.payload.source === 'mouse') { debouncedSelectionChange(); @@ -90,7 +102,7 @@ export class InlineCompletionsSource extends Disposable { }), ); - this.addDispose( + this.featureDisposable.addDispose( this.monacoEditor.onDidChangeModelContent((e) => { const changes = e.changes; for (const change of changes) { @@ -104,17 +116,15 @@ export class InlineCompletionsSource extends Disposable { }), ); - this.addDispose( + this.featureDisposable.addDispose( this.monacoEditor.onDidBlurEditorText(() => { this.commandService.executeCommand(AI_INLINE_COMPLETION_VISIBLE.id, false); }), ); - } - public fetch(): IDisposable { let prePosition: Position | undefined; - this.addDispose( + this.featureDisposable.addDispose( monacoApi.languages.registerInlineCompletionsProvider('*', { groupId: 'ai-native-intelligent-completions', provideInlineCompletions: async (model, position, context, token) => { @@ -150,6 +160,24 @@ export class InlineCompletionsSource extends Disposable { }), ); - return this; + return this.featureDisposable; + } + + public async fetchProvider(bean: IAICompletionOption): Promise { + const provider = this.intelligentCompletionsRegistry.getInlineCompletionsProvider(); + if (!provider) { + return; + } + + // 如果上一次补全结果还在,则不重复请求 + const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get(); + if (isVisible) { + return; + } + + const position = this.monacoEditor.getPosition()!; + const inlineCompletionModel = await provider(this.monacoEditor, position, bean, this.token); + + return inlineCompletionModel; } } diff --git a/packages/ai-native/src/browser/contrib/inline-completions/model/competionModel.ts b/packages/ai-native/src/browser/contrib/inline-completions/model/competionModel.ts index 104f06f6dc..7e5f86210f 100644 --- a/packages/ai-native/src/browser/contrib/inline-completions/model/competionModel.ts +++ b/packages/ai-native/src/browser/contrib/inline-completions/model/competionModel.ts @@ -1,6 +1,6 @@ import * as monaco from '@opensumi/ide-monaco'; -import { IIntelligentCompletionsResult } from '../../intelligent-completions/intelligent-completions'; +import { IIntelligentCompletionsResult } from '../../intelligent-completions'; /** * 缓存的结果 diff --git a/packages/ai-native/src/browser/contrib/inline-completions/model/inlineCompletionRequestTask.ts b/packages/ai-native/src/browser/contrib/inline-completions/model/inlineCompletionRequestTask.ts index 56fdaf6d2d..674ceb9656 100644 --- a/packages/ai-native/src/browser/contrib/inline-completions/model/inlineCompletionRequestTask.ts +++ b/packages/ai-native/src/browser/contrib/inline-completions/model/inlineCompletionRequestTask.ts @@ -16,9 +16,9 @@ import { WorkbenchEditorService } from '@opensumi/ide-editor'; import { WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service'; import * as monaco from '@opensumi/ide-monaco'; -import { IIntelligentCompletionsResult } from '../../intelligent-completions/intelligent-completions'; -import { IntelligentCompletionsController } from '../../intelligent-completions/intelligent-completions.controller'; +import { IIntelligentCompletionsResult } from '../../intelligent-completions'; import { IntelligentCompletionsRegistry } from '../../intelligent-completions/intelligent-completions.feature.registry'; +import { InlineCompletionsController } from '../inline-completions.controller'; import { InlineCompletionItem } from '../model/competionModel'; import { PromptCache } from '../promptCache'; import { lineBasedPromptProcessor } from '../provider'; @@ -174,14 +174,14 @@ export class InlineCompletionRequestTask extends Disposable { } else { try { this.aiCompletionsService.updateStatusBarItem('running', true); - const provider = this.intelligentCompletionsRegistry.getProvider(); + const provider = this.intelligentCompletionsRegistry.getInlineCompletionsProvider(); if (provider) { const editor = this.workbenchEditorService.currentCodeEditor; if (!editor) { return []; } - const intelligentCompletionsHandler = IntelligentCompletionsController.get(editor.monacoEditor); - completeResult = await intelligentCompletionsHandler?.fetchProvider(requestBean); + const inlineCompletionsHandler = InlineCompletionsController.get(editor.monacoEditor); + completeResult = await inlineCompletionsHandler?.fetchProvider(requestBean); } else { completeResult = await this.aiCompletionsService.complete(requestBean); } diff --git a/packages/ai-native/src/browser/contrib/inline-completions/promptCache.ts b/packages/ai-native/src/browser/contrib/inline-completions/promptCache.ts index 04363842c8..20e0c7027f 100644 --- a/packages/ai-native/src/browser/contrib/inline-completions/promptCache.ts +++ b/packages/ai-native/src/browser/contrib/inline-completions/promptCache.ts @@ -9,7 +9,7 @@ import { } from '@opensumi/ide-core-browser'; import { IHashCalculateService } from '@opensumi/ide-core-common/lib/hash-calculate/hash-calculate'; -import { IIntelligentCompletionsResult } from '../intelligent-completions/intelligent-completions'; +import { IIntelligentCompletionsResult } from '../intelligent-completions'; /** * 缓存服务 diff --git a/packages/ai-native/src/browser/contrib/inline-completions/service/ai-completions.service.ts b/packages/ai-native/src/browser/contrib/inline-completions/service/ai-completions.service.ts index 6e25625d0b..2906a8f3ac 100644 --- a/packages/ai-native/src/browser/contrib/inline-completions/service/ai-completions.service.ts +++ b/packages/ai-native/src/browser/contrib/inline-completions/service/ai-completions.service.ts @@ -15,7 +15,7 @@ import { } from '@opensumi/ide-core-common'; import { CompletionRT, IAIReporter } from '@opensumi/ide-core-common/lib/types/ai-native/reporter'; -import { IIntelligentCompletionsResult } from '../../intelligent-completions/intelligent-completions'; +import { IIntelligentCompletionsResult } from '../../intelligent-completions'; @Injectable() export class AICompletionsService extends Disposable { diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/additions-deletions.decoration.ts b/packages/ai-native/src/browser/contrib/intelligent-completions/decoration/additions-deletions.decoration.ts similarity index 90% rename from packages/ai-native/src/browser/contrib/intelligent-completions/additions-deletions.decoration.ts rename to packages/ai-native/src/browser/contrib/intelligent-completions/decoration/additions-deletions.decoration.ts index a1e2902762..6eb08fc848 100644 --- a/packages/ai-native/src/browser/contrib/intelligent-completions/additions-deletions.decoration.ts +++ b/packages/ai-native/src/browser/contrib/intelligent-completions/decoration/additions-deletions.decoration.ts @@ -1,10 +1,9 @@ import { ICodeEditor, IModelDeltaDecoration, IRange, TrackedRangeStickiness } from '@opensumi/ide-monaco'; -import { EnhanceDecorationsCollection } from '../../model/enhanceDecorationsCollection'; -import { REWRITE_DECORATION_INLINE_ADD } from '../../widget/rewrite/rewrite-widget'; - -import { IMultiLineDiffChangeResult } from './diff-computer'; -import styles from './intelligent-completions.module.less'; +import { EnhanceDecorationsCollection } from '../../../model/enhanceDecorationsCollection'; +import { REWRITE_DECORATION_INLINE_ADD } from '../../../widget/rewrite/rewrite-widget'; +import { IMultiLineDiffChangeResult } from '../diff-computer'; +import styles from '../intelligent-completions.module.less'; export class AdditionsDeletionsDecorationModel { private deletionsDecorations: EnhanceDecorationsCollection; diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/multi-line.decoration.ts b/packages/ai-native/src/browser/contrib/intelligent-completions/decoration/multi-line.decoration.ts similarity index 98% rename from packages/ai-native/src/browser/contrib/intelligent-completions/multi-line.decoration.ts rename to packages/ai-native/src/browser/contrib/intelligent-completions/decoration/multi-line.decoration.ts index e22554d393..82e881fec7 100644 --- a/packages/ai-native/src/browser/contrib/intelligent-completions/multi-line.decoration.ts +++ b/packages/ai-native/src/browser/contrib/intelligent-completions/decoration/multi-line.decoration.ts @@ -4,9 +4,8 @@ import { empty } from '@opensumi/ide-utils/lib/strings'; import { EditOperation } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/editOperation'; import { LineDecoration } from '@opensumi/monaco-editor-core/esm/vs/editor/common/viewLayout/lineDecorations'; -import { EnhanceDecorationsCollection } from '../../model/enhanceDecorationsCollection'; - -import { IMultiLineDiffChangeResult } from './diff-computer'; +import { EnhanceDecorationsCollection } from '../../../model/enhanceDecorationsCollection'; +import { IMultiLineDiffChangeResult } from '../diff-computer'; export interface IModificationsInline { newValue: string; diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/index.ts b/packages/ai-native/src/browser/contrib/intelligent-completions/index.ts new file mode 100644 index 0000000000..cdd5de1e21 --- /dev/null +++ b/packages/ai-native/src/browser/contrib/intelligent-completions/index.ts @@ -0,0 +1,34 @@ +import { IRange, InlineCompletion } from '@opensumi/ide-monaco'; + +import type { ILinterErrorData } from './lint-error.source'; + +export interface IIntelligentCompletionsResult { + readonly items: InlineCompletion[]; + /** + * 定义的额外信息 + */ + extra?: T; +} + +export enum ECodeEditsSource { + LinterErrors = 'lint_errors', +} + +export interface ICodeEditsContextBean { + typing: ECodeEditsSource.LinterErrors; + data: ILinterErrorData; +} + +export interface ICodeEdit { + /** + * 插入的文本 + */ + readonly insertText: string; + /** + * 替换的文本范围 + */ + readonly range: IRange; +} +export interface ICodeEditsResult { + readonly items: ICodeEdit[]; +} diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.controller.ts b/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.controller.ts index 70cf57991f..05c9691075 100644 --- a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.controller.ts +++ b/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.controller.ts @@ -1,14 +1,6 @@ import { Key, KeybindingRegistry, KeybindingScope, PreferenceService } from '@opensumi/ide-core-browser'; import { MultiLineEditsIsVisible } from '@opensumi/ide-core-browser/lib/contextkey/ai-native'; -import { - AINativeSettingSectionsId, - Disposable, - Event, - IAICompletionOption, - IDisposable, - IntelligentCompletionsRegistryToken, - runWhenIdle, -} from '@opensumi/ide-core-common'; +import { AINativeSettingSectionsId, Disposable, Event, IDisposable, runWhenIdle } from '@opensumi/ide-core-common'; import { ICodeEditor, ICursorPositionChangedEvent, IRange, ITextModel, Range } from '@opensumi/ide-monaco'; import { empty } from '@opensumi/ide-utils/lib/strings'; import { autorun, transaction } from '@opensumi/monaco-editor-core/esm/vs/base/common/observable'; @@ -28,17 +20,17 @@ import { AINativeContextKey } from '../../contextkey/ai-native.contextkey.servic import { REWRITE_DECORATION_INLINE_ADD, RewriteWidget } from '../../widget/rewrite/rewrite-widget'; import { BaseAIMonacoEditorController } from '../base'; -import { AdditionsDeletionsDecorationModel } from './additions-deletions.decoration'; +import { AdditionsDeletionsDecorationModel } from './decoration/additions-deletions.decoration'; +import { MultiLineDecorationModel } from './decoration/multi-line.decoration'; import { IMultiLineDiffChangeResult, computeMultiLineDiffChanges, mergeMultiLineDiffChanges, wordChangesToLineChangesMap, } from './diff-computer'; -import { IIntelligentCompletionsResult } from './intelligent-completions'; -import { IntelligentCompletionsRegistry } from './intelligent-completions.feature.registry'; -import { InlineCompletionsSource } from './intelligent-completions.source'; -import { MultiLineDecorationModel } from './multi-line.decoration'; +import { LintErrorCodeEditsSource } from './lint-error.source'; + +import { ICodeEditsResult } from '.'; export class IntelligentCompletionsController extends BaseAIMonacoEditorController { public static readonly ID = 'editor.contrib.ai.intelligent.completions'; @@ -47,10 +39,6 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll return editor.getContribution(IntelligentCompletionsController.ID); } - private get intelligentCompletionsRegistry(): IntelligentCompletionsRegistry { - return this.injector.get(IntelligentCompletionsRegistryToken); - } - private get model(): ITextModel { return this.monacoEditor.getModel()!; } @@ -183,45 +171,30 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll } } - public async fetchProvider(bean: IAICompletionOption): Promise { - const provider = this.intelligentCompletionsRegistry.getProvider(); - if (!provider) { - return; - } - + public async fetchProvider(editsResult: ICodeEditsResult): Promise { // 如果上一次补全结果还在,则不重复请求 const isVisible = this.aiNativeContextKey.multiLineEditsIsVisible.get(); if (isVisible) { return; } - const position = this.monacoEditor.getPosition()!; - const intelligentCompletionModel = await provider(this.monacoEditor, position, bean, this.token); - - if ( - intelligentCompletionModel && - intelligentCompletionModel.enableMultiLine && - intelligentCompletionModel.items.length > 0 - ) { - return this.applyInlineDecorations(intelligentCompletionModel); + if (editsResult && editsResult.items.length > 0) { + this.applyInlineDecorations(editsResult); } - - return intelligentCompletionModel; } - private applyInlineDecorations(completionModel: IIntelligentCompletionsResult) { + private applyInlineDecorations(completionModel: ICodeEditsResult) { const { items } = completionModel; - - const position = this.monacoEditor.getPosition()!; - const model = this.monacoEditor.getModel(); const { range, insertText } = items[0]; - const insertTextString = insertText.toString(); - // 如果只是开启了 enableMultiLine 而没有传递 range ,则不显示 multi line + // code edits 必须提供 range if (!range) { - return completionModel; + 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(); @@ -404,7 +377,7 @@ export class IntelligentCompletionsController extends BaseAIMonacoEditorControll }), ); - const inlineCompletionsSource = this.injector.get(InlineCompletionsSource, [this.monacoEditor]); - this.featureDisposable.addDispose(inlineCompletionsSource.fetch()); + const lintErrorCodeEditsSource = this.injector.get(LintErrorCodeEditsSource, [this.monacoEditor, this.token]); + this.featureDisposable.addDispose(lintErrorCodeEditsSource.mount()); } } diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.feature.registry.ts b/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.feature.registry.ts index 1d3f123732..a2b60e469a 100644 --- a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.feature.registry.ts +++ b/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.feature.registry.ts @@ -1,17 +1,30 @@ import { Injectable } from '@opensumi/di'; import { Disposable } from '@opensumi/ide-core-common'; -import { IIntelligentCompletionProvider, IIntelligentCompletionsRegistry } from '../../types'; +import { ICodeEditsProvider, IIntelligentCompletionProvider, IIntelligentCompletionsRegistry } from '../../types'; @Injectable() export class IntelligentCompletionsRegistry extends Disposable implements IIntelligentCompletionsRegistry { - private provider: IIntelligentCompletionProvider | undefined; + private inlineCompletionsProvider: IIntelligentCompletionProvider | undefined; + private codeEditsProvider: ICodeEditsProvider | undefined; registerIntelligentCompletionProvider(provider: IIntelligentCompletionProvider): void { - this.provider = provider; + this.inlineCompletionsProvider = provider; } - getProvider(): IIntelligentCompletionProvider | undefined { - return this.provider; + registerInlineCompletionsProvider(provider: IIntelligentCompletionProvider): void { + this.inlineCompletionsProvider = provider; + } + + registerCodeEditsProvider(provider: ICodeEditsProvider): void { + this.codeEditsProvider = provider; + } + + getInlineCompletionsProvider(): IIntelligentCompletionProvider | undefined { + return this.inlineCompletionsProvider; + } + + getCodeEditsProvider(): ICodeEditsProvider | undefined { + return this.codeEditsProvider; } } diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.ts b/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.ts deleted file mode 100644 index 594c3df4a6..0000000000 --- a/packages/ai-native/src/browser/contrib/intelligent-completions/intelligent-completions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { InlineCompletion } from '@opensumi/ide-monaco'; - -export interface IIntelligentCompletionsResult { - readonly items: InlineCompletion[]; - /** - * 是否开启多行补全 - * 开启后,items 中的 range 必填 - * 否则显示默认的 inline completion - */ - readonly enableMultiLine?: boolean | undefined; - /** - * 定义的额外信息 - */ - extra?: T; -} diff --git a/packages/ai-native/src/browser/contrib/intelligent-completions/lint-error.source.ts b/packages/ai-native/src/browser/contrib/intelligent-completions/lint-error.source.ts new file mode 100644 index 0000000000..3cdbf88121 --- /dev/null +++ b/packages/ai-native/src/browser/contrib/intelligent-completions/lint-error.source.ts @@ -0,0 +1,130 @@ +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 { 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'; +import { + IMarker, + IMarkerService, + IRelatedInformation, + 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 '.'; + +export interface ILinterErrorData { + relativeWorkspacePath: string; + errors: Array; +} + +export interface IMarkerErrorData { + message: string; + range: { + startPosition: IPosition; + endPosition: IPosition; + }; + source: URI; + severity: string; + relatedInformation?: IRelatedInformation[]; +} + +namespace MarkerErrorData { + export function toData(marker: IMarker): IMarkerErrorData { + return { + message: marker.message, + range: { + startPosition: { + lineNumber: marker.startLineNumber, + column: marker.startColumn, + }, + endPosition: { + lineNumber: marker.endLineNumber, + column: marker.endColumn, + }, + }, + source: marker.resource, + relatedInformation: marker.relatedInformation, + severity: MarkerSeverity.toString(marker.severity), + }; + } +} + +@Injectable({ multiple: true }) +export class LintErrorCodeEditsSource extends Disposable { + @Autowired(IntelligentCompletionsRegistryToken) + private readonly intelligentCompletionsRegistry: IntelligentCompletionsRegistry; + + @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(); + + this.addDispose( + // 仅在光标的行号发生变化时,才触发 + this.monacoEditor.onDidChangeCursorPosition((event: ICursorPositionChangedEvent) => { + const currentPosition = event.position; + if (prePosition && prePosition.lineNumber !== currentPosition.lineNumber) { + this.doTrigger(currentPosition); + } + prePosition = currentPosition; + }), + ); + return this; + } + + private async doTrigger(position: Position) { + if (!this.model) { + return; + } + + const markerService = StandaloneServices.get(IMarkerService); + const resource = this.model.uri; + + let markers = markerService.read({ resource, severities: MarkerSeverity.Error }); + 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, + ); + + if (result) { + IntelligentCompletionsController.get(this.monacoEditor)?.fetchProvider(result); + } + } + } + } +} diff --git a/packages/ai-native/src/browser/types.ts b/packages/ai-native/src/browser/types.ts index f4387f7977..8bad328488 100644 --- a/packages/ai-native/src/browser/types.ts +++ b/packages/ai-native/src/browser/types.ts @@ -19,7 +19,11 @@ import { IMarker } from '@opensumi/monaco-editor-core/esm/vs/platform/markers/co import { IChatWelcomeMessageContent, ISampleQuestions, ITerminalCommandSuggestionDesc } from '../common'; -import { IIntelligentCompletionsResult } from './contrib/intelligent-completions/intelligent-completions'; +import { + ICodeEditsContextBean, + ICodeEditsResult, + IIntelligentCompletionsResult, +} from './contrib/intelligent-completions'; import { BaseTerminalDetectionLineMatcher } from './contrib/terminal/matcher'; import { InlineChatController } from './widget/inline-chat/inline-chat-controller'; @@ -81,11 +85,11 @@ export interface IInlineChatFeatureRegistry { */ registerTerminalInlineChat(operational: AIActionItem, handler: ITerminalInlineChatHandler): IDisposable; /** - * 注销 terminal 内联功能 + * 注册 terminal 内联功能 */ unregisterTerminalInlineChat(operational: AIActionItem): void; /** - * proposed api,可能随时都会有变化 + * 注册 interactive input 功能 */ registerInteractiveInput( strategyOptions: IInteractiveInputRunStrategy, @@ -212,8 +216,24 @@ export type IIntelligentCompletionProvider = ( contextBean: IAICompletionOption, token: CancellationToken, ) => MaybePromise; + +export type ICodeEditsProvider = ( + editor: ICodeEditor, + position: IPosition, + contextBean: ICodeEditsContextBean, + token: CancellationToken, +) => MaybePromise; + export interface IIntelligentCompletionsRegistry { + /** + * @deprecated use registerInlineCompletionProvider API + */ registerIntelligentCompletionProvider(provider: IIntelligentCompletionProvider): void; + registerInlineCompletionsProvider(provider: IIntelligentCompletionProvider): void; + /** + * 注册 code edits 功能 + */ + registerCodeEditsProvider(provider: ICodeEditsProvider): void; } export interface IProblemFixContext { @@ -267,6 +287,7 @@ export interface AINativeCoreContribution { registerTerminalProvider?(registry: ITerminalProviderRegistry): void; /** * 注册智能代码补全相关功能 + * proposed api */ registerIntelligentCompletionFeature?(registry: IIntelligentCompletionsRegistry): void; } diff --git a/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts b/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts index bff19a10ab..f2ee368fb2 100644 --- a/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts +++ b/packages/startup/entry/sample-modules/ai-native/ai-native.contribution.ts @@ -40,7 +40,7 @@ import { ReplyResponse, getDebugLogger, } from '@opensumi/ide-core-common'; -import { ICodeEditor, NewSymbolName, NewSymbolNameTag } from '@opensumi/ide-monaco'; +import { ICodeEditor, NewSymbolName, NewSymbolNameTag, Range } from '@opensumi/ide-monaco'; import { MarkdownString } from '@opensumi/monaco-editor-core/esm/vs/base/common/htmlContent'; import { SlashCommand } from './SlashCommand'; @@ -425,82 +425,32 @@ export class AINativeContribution implements AINativeCoreContribution { } registerIntelligentCompletionFeature(registry: IIntelligentCompletionsRegistry): void { - registry.registerIntelligentCompletionProvider(async (editor, position, bean, token) => { - const model = editor.getModel()!; - const value = model.getValueInRange({ - startLineNumber: position.lineNumber, - startColumn: 1, - endLineNumber: position.lineNumber + 3, - endColumn: model?.getLineMaxColumn(position.lineNumber + 3), - }); - - const cancelController = new AbortController(); - const { signal } = cancelController; - - token.onCancellationRequested(() => { - cancelController.abort(); - }); - - /** - * mock randown - */ - const getRandomString = (length) => { - const characters = 'opensumi'; - let result = ''; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); - } - return result; - }; - - const insertRandomStrings = (originalString) => { - const minChanges = 2; - const maxChanges = 5; - const changesCount = Math.floor(Math.random() * (maxChanges - minChanges + 1)) + minChanges; - let modifiedString = originalString; - for (let i = 0; i < changesCount; i++) { - const randomIndex = Math.floor(Math.random() * originalString.length); - const operation = Math.random() < 0.5 ? 'delete' : 'insert'; - if (operation === 'delete') { - modifiedString = modifiedString.slice(0, randomIndex) + modifiedString.slice(randomIndex + 1); - } else { - const randomChar = getRandomString(1); - modifiedString = modifiedString.slice(0, randomIndex) + randomChar + modifiedString.slice(randomIndex); - } - } - return modifiedString; - }; - - try { - await new Promise((resolve, reject) => { - const timeout = setTimeout(resolve, 1000); - - signal.addEventListener('abort', () => { - clearTimeout(timeout); - reject(new DOMException('Aborted', 'AbortError')); - }); - }); - - return { - items: [ - { - insertText: insertRandomStrings(value), - range: { - startLineNumber: position.lineNumber, - startColumn: 1, - endLineNumber: position.lineNumber + 3, - endColumn: model?.getLineMaxColumn(position.lineNumber + 3), + registry.registerInlineCompletionsProvider(async (editor, position, bean, token) => ({ + items: [{ insertText: 'Hello OpenSumi' }], + })); + + registry.registerCodeEditsProvider(async (editor, position, bean, token) => { + const model = editor.getModel(); + const maxLine = Math.max(position.lineNumber + 3, model?.getLineCount() ?? 0); + const lineMaxColumn = model?.getLineMaxColumn(maxLine); + + return { + items: [ + { + insertText: 'Hello OpenSumi', + range: Range.fromPositions( + { + lineNumber: position.lineNumber, + column: 1, }, - }, - ], - enableMultiLine: true, - }; - } catch (error) { - if (error.name === 'AbortError') { - return { items: [] }; - } - throw error; - } + { + lineNumber: position.lineNumber + 3, + column: lineMaxColumn ?? 1, + }, + ), + }, + ], + }; }); } }