diff --git a/packages/ai-native/src/browser/ai-editor.contribution.ts b/packages/ai-native/src/browser/ai-editor.contribution.ts index 583246f7e5..110fbb6c2e 100644 --- a/packages/ai-native/src/browser/ai-editor.contribution.ts +++ b/packages/ai-native/src/browser/ai-editor.contribution.ts @@ -43,7 +43,7 @@ import { AINativeCoreContribution, IAIMiddleware } from './types'; import { InlineChatFeatureRegistry } from './widget/inline-chat/inline-chat.feature.registry'; import { AIInlineChatService, EInlineChatStatus } from './widget/inline-chat/inline-chat.service'; import { AIInlineContentWidget } from './widget/inline-chat/inline-content-widget'; -import { AIDiffWidget } from './widget/inline-diff/inline-diff-widget'; +import { InlineDiffWidget } from './widget/inline-diff/inline-diff-widget'; @Injectable() export class AIEditorContribution extends Disposable implements IEditorFeatureContribution { @@ -105,7 +105,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo this.logger = this.loggerManagerClient.getLogger(SupportLogNamespace.Browser); } - private aiDiffWidget: AIDiffWidget; + private aiDiffWidget: InlineDiffWidget; private aiInlineContentWidget: AIInlineContentWidget; private aiInlineChatDisposed: Disposable = new Disposable(); private aiInlineChatOperationDisposed: Disposable = new Disposable(); @@ -453,7 +453,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo this.aiInlineChatService.onThumbs((isLike: boolean) => { this.aiReporter.end(relationId, { isLike }); }), - this.aiDiffWidget.onMaxLincCount((count) => { + this.aiDiffWidget.onMaxLineCount((count) => { requestAnimationFrame(() => { if (crossSelection.endLineNumber === model!.getLineCount()) { const lineHeight = monacoEditor.getOption(monacoApi.editor.EditorOption.lineHeight); @@ -487,8 +487,8 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo } private visibleDiffWidget(monacoEditor: monaco.ICodeEditor, crossSelection: monaco.Selection, answer: string): void { - monacoEditor.setHiddenAreas([crossSelection], AIDiffWidget._hideId); - this.aiDiffWidget = this.injector.get(AIDiffWidget, [monacoEditor, crossSelection, answer]); + monacoEditor.setHiddenAreas([crossSelection], InlineDiffWidget._hideId); + this.aiDiffWidget = this.injector.get(InlineDiffWidget, [monacoEditor, crossSelection, answer]); this.aiDiffWidget.create(); this.aiDiffWidget.showByLine( crossSelection.startLineNumber - 1, diff --git a/packages/ai-native/src/browser/merge-conflict/index.ts b/packages/ai-native/src/browser/merge-conflict/index.ts index f05102ccfd..577ada1107 100644 --- a/packages/ai-native/src/browser/merge-conflict/index.ts +++ b/packages/ai-native/src/browser/merge-conflict/index.ts @@ -1,26 +1,21 @@ import debounce from 'lodash/debounce'; import { Autowired, INJECTOR_TOKEN, Injector } from '@opensumi/di'; -import { message } from '@opensumi/ide-components'; -import { AINativeConfigService, ClientAppContribution } from '@opensumi/ide-core-browser'; +import { AINativeConfigService, ClientAppContribution, MERGE_CONFLICT_COMMANDS } from '@opensumi/ide-core-browser'; import { MergeConflictReportService } from '@opensumi/ide-core-browser/lib/ai-native/conflict-report.service'; import { - AIBackSerivcePath, CancelResponse, CancellationTokenSource, ChatResponse, - Command, CommandContribution, CommandRegistry, Constants, - ConstructorOf, Disposable, Domain, Emitter, ErrorResponse, Event, ExtensionActivatedEvent, - IAIBackService, IConflictContentMetadata, IEventBus, IInternalResolveConflictRegistry, @@ -34,48 +29,32 @@ import { Uri, localize, } from '@opensumi/ide-core-common'; +import { GitCommands } from '@opensumi/ide-core-common/lib/commands/git'; import { IEditor, WorkbenchEditorService } from '@opensumi/ide-editor/lib/browser'; +import { + CommitType, + DocumentMergeConflict, + MergeConflictParser, +} from '@opensumi/ide-editor/lib/browser/merge-conflict'; import * as monaco from '@opensumi/ide-monaco'; import { ITextModel } from '@opensumi/ide-monaco'; -import { ReactInlineContentWidget } from '@opensumi/ide-monaco/lib/browser/ai-native/BaseInlineContentWidget'; import { LineRange } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/model/line-range'; import { + ACCEPT_CURRENT_ACTIONS, AI_RESOLVE_REGENERATE_ACTIONS, IConflictActionsEvent, IGNORE_ACTIONS, REVOKE_ACTIONS, } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/types'; -import { ResultCodeEditor } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/view/editors/resultCodeEditor'; import styles from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/view/merge-editor.module.less'; +import { IWidgetFactory, WidgetFactory } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/widget/facotry'; import { StopWidget } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/widget/stop-widget'; +import { IMergeEditorShape } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/widget/types'; import { monacoApi } from '@opensumi/ide-monaco/lib/browser/monaco-api'; import { ICodeEditor, IModelDeltaDecoration } from '@opensumi/ide-monaco/lib/browser/monaco-api/editor'; import { languageFeaturesService } from '@opensumi/ide-monaco/lib/browser/monaco-api/languages'; -import { Position } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/position'; -import { IValidEditOperation } from '@opensumi/monaco-editor-core/esm/vs/editor/common/model'; -import { CacheConflict, DocumentMergeConflict } from './cache-conflicts'; import { OverrideResolveResultWidget as ResolveResultWidget } from './override-resolve-result-widget'; -import { CommitType } from './types'; -export namespace MERGE_CONFLICT { - const CATEGORY = 'MergeConflict'; - export const AI_ACCEPT: Command = { - id: 'merge-conflict.ai.accept', - category: CATEGORY, - }; - export const ALL_RESET: Command = { - id: 'merge-conflict.ai.all-reset', - category: CATEGORY, - }; - export const AI_ALL_ACCEPT: Command = { - id: 'merge-conflict.ai.all-accept', - category: CATEGORY, - }; - export const AI_ALL_ACCEPT_STOP: Command = { - id: 'merge-conflict.ai.all-accept-stop', - category: CATEGORY, - }; -} const MERGE_CONFLICT_CODELENS_STYLE = 'merge-conflict-codelens-style'; @@ -127,16 +106,17 @@ function loadStyleString(load: boolean) { } } -interface IWidgetFactory { - hideWidget(id?: string): void; - addWidget(range: LineRange): void; - hasWidget(range: LineRange): boolean; -} - -interface ICacheResolvedConflicts extends IValidEditOperation { +interface ICacheResolvedConflicts { + /** + * 冲突的所有文本范围(包含 incoming 和 current) + */ newRange: IRange; id: string; conflictText: string; + /** + * 解决冲突后的文本 + */ + text: string; metadata: IConflictContentMetadata; isAccept?: boolean; isClosed?: boolean; @@ -147,58 +127,15 @@ interface IRequestCancel { type: 'cancel'; } -class WidgetFactory implements IWidgetFactory { - private widgetMap: Map; - - constructor( - private contentWidget: ConstructorOf, - private editor: ResultCodeEditor, - private injector: Injector, - ) { - this.widgetMap = new Map(); - } - - hasWidget(range: LineRange): boolean { - return this.widgetMap.get(range.id) !== undefined; - } - - public hideWidget(id?: string): void { - if (id) { - const widget = this.widgetMap.get(id); - if (widget) { - widget.hide(); - this.widgetMap.delete(id); - } - return; - } - - this.widgetMap.forEach((widget) => { - widget.hide(); - }); - this.widgetMap.clear(); - } - - public addWidget(range: LineRange): void { - const id = range.id; - if (this.widgetMap.has(id)) { - return; - } - - const position = new Position(range.endLineNumberExclusive, 1); - - const widget = this.injector.get(this.contentWidget, [this.editor, range]); - widget.show({ position }); - - this.widgetMap.set(id, widget); - } -} - interface IReportData extends Partial { relationId?: string; } @Domain(CommandContribution, ClientAppContribution) -export class MergeConflictContribution extends Disposable implements CommandContribution, ClientAppContribution { +export class MergeConflictContribution + extends Disposable + implements CommandContribution, ClientAppContribution, IMergeEditorShape +{ @Autowired(INJECTOR_TOKEN) private readonly injector: Injector; @@ -212,10 +149,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont private readonly aiNativeConfigService: AINativeConfigService; @Autowired() - private readonly cacheConflicts: CacheConflict; - - @Autowired(AIBackSerivcePath) - private aiBackService: IAIBackService; + private readonly conflictParser: MergeConflictParser; @Autowired(MergeConflictReportService) private readonly mergeConflictReportService: MergeConflictReportService; @@ -236,7 +170,8 @@ export class MergeConflictContribution extends Disposable implements CommandCont public readonly onRequestsCancel: Event = this._onRequestsCancel.event; // for widget - private editor: ICodeEditor; + editor: ICodeEditor; + // for codelens loading private loadingRange: Set = new Set(); // for report @@ -249,7 +184,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont super.dispose(); this.cancelRequestToken(); this.mergeConflictReportService.dispose(); - this.cacheConflicts.dispose(); + this.conflictParser.dispose(); } onDidStart(): MaybePromise { @@ -436,22 +371,26 @@ export class MergeConflictContribution extends Disposable implements CommandCont } private reportConflictData() { + if (!this.editor) { + return; + } + const uri = this.getUri(); const reportData = this.currentReportMap.get(uri); if (reportData) { - this.mergeConflictReportService.report(this.getUri(), this.reportData); + this.mergeConflictReportService.report(uri, this.reportData); } } private updateReportData() { - const allConflictCache = this.cacheConflicts.getAllConflictsByUri(this.getUri()); + const allConflictCache = this.conflictParser.getAllConflictsByUri(this.getUri()); let conflictPointNum = 0; - let useAiConflictPointNum = 0; + let useAIConflictPointNum = 0; let receiveNum = 0; conflictPointNum = allConflictCache?.length || 0; allConflictCache?.forEach((cacheConflict) => { if (cacheConflict.isResolved) { - useAiConflictPointNum += 1; + useAIConflictPointNum += 1; } }); // 内部修改 删除态无法统计 @@ -460,7 +399,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont // 统计当前文件 if (uri === this.getModel().uri.toString()) { // 计算当前文件采纳数量 - const aiResult = cacheResolvedConflicts.textChange.newText; + const aiResult = cacheResolvedConflicts.text; const currentResult = this.getModel()?.getValueInRange(cacheResolvedConflicts.newRange); if (aiResult.trim() === currentResult.trim()) { cacheResolvedConflicts.isAccept = true; @@ -473,7 +412,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont } this.reportData = { conflictPointNum, - useAiConflictPointNum, + useAiConflictPointNum: useAIConflictPointNum, receiveNum, }; } @@ -492,7 +431,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont for (const [id, value] of this.getCacheResolvedConflicts().entries()) { if (!value.isClosed) { const lineRange = this.toLineRange(value.newRange, id); - this.resolveResultWidgetManager.addWidget(lineRange); + this.resolveResultWidgetManager.addWidget(lineRange, value.newRange, value.text); } } } @@ -502,7 +441,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont } private getUri(): string { - return this.getModel().uri.toString(); + return this.getModel()?.uri.toString(); } /* cache widget */ @@ -550,7 +489,6 @@ export class MergeConflictContribution extends Disposable implements CommandCont private toLineRange(range: IRange, id?: string) { const lineRange = new LineRange(range.startLineNumber, range.endLineNumber); if (id) { - // @ts-ignore lineRange.setId(id); } return lineRange; @@ -580,14 +518,14 @@ export class MergeConflictContribution extends Disposable implements CommandCont registerCommands(commands: CommandRegistry): void { this.disposables.push( - commands.registerCommand(MERGE_CONFLICT.AI_ACCEPT, { + commands.registerCommand(MERGE_CONFLICT_COMMANDS.AI_ACCEPT, { execute: async (type: CommitType, conflict: DocumentMergeConflict) => { this.conflictAIAccept(conflict); }, }), - commands.registerCommand(MERGE_CONFLICT.ALL_RESET, { + commands.registerCommand(MERGE_CONFLICT_COMMANDS.ALL_RESET, { execute: async (uri: Uri) => { - const content = this.cacheConflicts.getConflictText(uri.toString()); + const content = this.conflictParser.getConflictText(uri.toString()); this.cancelRequestToken(); if (content) { if (this.editorService.currentEditor?.currentUri?.toString() === uri.toString()) { @@ -595,7 +533,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont if (editor) { const model = editor.getModel(); model?.setValue(content); - this.cacheConflicts.deleteConflictText(uri.toString()); + this.conflictParser.deleteConflictText(uri.toString()); this.cleanAllCache(); } } @@ -603,13 +541,13 @@ export class MergeConflictContribution extends Disposable implements CommandCont }, }), - commands.registerCommand(MERGE_CONFLICT.AI_ALL_ACCEPT, { + commands.registerCommand(MERGE_CONFLICT_COMMANDS.AI_ALL_ACCEPT, { execute: async () => { const document = this.getModel(); if (!document) { return Promise.resolve(); } - const conflicts = this.cacheConflicts.scanDocument(document as monaco.editor.ITextModel); + const conflicts = this.conflictParser.scanDocument(document as monaco.editor.ITextModel); if (!conflicts?.length) { return Promise.resolve(); } @@ -620,16 +558,16 @@ export class MergeConflictContribution extends Disposable implements CommandCont await this.acceptAllConflict(); }, }), - commands.registerCommand(MERGE_CONFLICT.AI_ALL_ACCEPT_STOP, { + commands.registerCommand(MERGE_CONFLICT_COMMANDS.AI_ALL_ACCEPT_STOP, { execute: async () => { this.cancelRequestToken(); }, }), - commands.afterExecuteCommand('git.stage', (args) => { + commands.afterExecuteCommand(GitCommands.Stage, (args) => { this.reportConflictData(); return args; }), - commands.afterExecuteCommand('git.stageAllMerge', (args) => { + commands.afterExecuteCommand(GitCommands.StageAllMerge, (args) => { this.reportConflictData(); return args; }), @@ -648,12 +586,8 @@ export class MergeConflictContribution extends Disposable implements CommandCont const currentEditor = editor; if (currentEditor && monacoEditor && !this.editor) { this.editor = monacoEditor; - this.resolveResultWidgetManager = new WidgetFactory( - ResolveResultWidget, - this as unknown as ResultCodeEditor, - this.injector, - ); - this.stopWidgetManager = new WidgetFactory(StopWidget, this as unknown as ResultCodeEditor, this.injector); + this.resolveResultWidgetManager = new WidgetFactory(ResolveResultWidget, this, this.injector); + this.stopWidgetManager = new WidgetFactory(StopWidget, this, this.injector); this.init(); } } @@ -662,35 +596,35 @@ export class MergeConflictContribution extends Disposable implements CommandCont document: monaco.editor.ITextModel, _token: monaco.CancellationToken, ): Promise { - const conflicts = this.cacheConflicts.scanDocument(document); + const conflicts = this.conflictParser.scanDocument(document); if (!conflicts.length) { return null; } const items: monaco.languages.CodeLens[] = []; conflicts.forEach((conflict) => { const aiAcceptCommand = { - id: MERGE_CONFLICT.AI_ACCEPT.id, - title: `$(ai-magic) ${localize('mergeEditor.conflict.resolve.all')}`, + id: MERGE_CONFLICT_COMMANDS.AI_ACCEPT.id, + title: `$(ai-magic) ${localize('mergeEditor.conflict.ai.resolve.all')}`, arguments: ['know-conflict', conflict], - tooltip: localize('mergeEditor.conflict.resolve.all'), + tooltip: localize('mergeEditor.conflict.ai.resolve.all'), }; // loading 效果 this.loadingRange.forEach((range) => { if (conflict.range.equalsRange(range)) { - aiAcceptCommand.title = `$(loading~spin) ${localize('mergeEditor.conflict.resolve.all')}`; + aiAcceptCommand.title = `$(loading~spin) ${localize('mergeEditor.conflict.ai.resolve.all')}`; } }); items.push({ range: conflict.range, command: aiAcceptCommand, - id: MERGE_CONFLICT.AI_ACCEPT.id, + id: MERGE_CONFLICT_COMMANDS.AI_ACCEPT.id, }); }); return Promise.resolve(items); } - private async conflictAIAccept(conflict?: DocumentMergeConflict, lineRan?: LineRange, isRegenerate?: boolean) { + private async conflictAIAccept(conflict?: DocumentMergeConflict, _lineRange?: LineRange, isRegenerate?: boolean) { if (!this.editorService.currentEditor?.monacoEditor) { return; } @@ -710,37 +644,36 @@ export class MergeConflictContribution extends Disposable implements CommandCont incomingName: conflict.incoming.name, }; } else { - lineRange = lineRan!; + lineRange = _lineRange!; } - const range = lineRange.toRange(1, conflict?.range.endColumn ?? Constants.MAX_SAFE_SMALL_INTEGER); - const skeletonDecorationDispose = this.renderSkeletonDecoration(range, [ + const newRange = lineRange.toRange(1, conflict?.range.endColumn ?? Constants.MAX_SAFE_SMALL_INTEGER); + const skeletonDecorationDispose = this.renderSkeletonDecoration(newRange, [ styles.skeleton_decoration, styles.skeleton_decoration_background_black, ]); this.stopWidgetManager.addWidget(lineRange); - let codeAssemble = this.getModel()?.getValueInRange(lineRange.toRange(1, range.endColumn)) ?? ''; + let conflictText = this.getModel()?.getValueInRange(lineRange.toRange(1, newRange.endColumn)) ?? ''; if (isRegenerate) { const cache = this.getCacheResolvedConflicts().get(lineRange.id); - codeAssemble = cache?.conflictText ?? ''; + conflictText = cache?.conflictText ?? ''; conflictMetadata = cache?.metadata; } let resolveConflictResult: ChatResponse | undefined; try { - this.loadingRange.add(range); + this.loadingRange.add(newRange); this.reportData = { clickNum: this.reportData.clickNum! + 1, }; - resolveConflictResult = await this.requestAiResolveConflict(conflictMetadata!, lineRange, isRegenerate); + resolveConflictResult = await this.requestAIResolveConflict(conflictMetadata!, lineRange, isRegenerate); } catch (error) { throw new Error(`AI resolve conflict error: ${error.toString()}`); } finally { skeletonDecorationDispose(); this.stopWidgetManager.hideWidget(lineRange.id); - this.loadingRange.delete(range); - this.updateCodeLensProvider(); + this.loadingRange.delete(newRange); } if (ReplyResponse.is(resolveConflictResult)) { @@ -748,65 +681,48 @@ export class MergeConflictContribution extends Disposable implements CommandCont aiOutputNum: this.reportData.aiOutputNum! + 1, }; - const { text, lineNumber, lines } = this.resolveEndLineEOL(resolveConflictResult!.message!); - const endLineNumber = lineRange.startLineNumber + lineNumber - 1; - const endColumn = lines[lines.length - 1].length + 1; - const newRange = new monaco.Range(lineRange.startLineNumber, 1, endLineNumber, endColumn); - const edit = { - range, - text, - }; - let validEditOperation: IValidEditOperation[] = []; - const selections = this.getModel()?.pushEditOperations(null, [edit], (operation) => { - validEditOperation = operation; - const selections: monaco.Selection[] = []; - operation.forEach((op) => { - selections.push( - new monaco.Selection( - op.range.startLineNumber, - op.range.startColumn, - op.range.endLineNumber, - op.range.endColumn, - ), - ); - }); - return selections; - }); - selections!.forEach((selection) => { - const decorationDispose = this.renderSkeletonDecoration(selection, [styles.skeleton_decoration_complete]); - this.decorationId2Dispose.set(lineRange.id, decorationDispose); - this.decorationId2Range.set(lineRange.id, selection); - }); + const { text } = this.resolveEndLineEOL(resolveConflictResult!.message!); + + const decorationDispose = this.renderSkeletonDecoration(newRange, [styles.skeleton_decoration_complete]); + this.decorationId2Dispose.set(lineRange.id, decorationDispose); + this.decorationId2Range.set(lineRange.id, newRange); + + const widgetLineRange = this.toLineRange( + { + ...newRange, + endLineNumber: newRange.endLineNumber + 1, + }, + lineRange.id, + ); - const newLineRange = this.toLineRange(newRange, lineRange.id); - this.resolveResultWidgetManager.addWidget(newLineRange); - this.setCacheResolvedConflict(newLineRange.id, { - ...validEditOperation[0], + this.resolveResultWidgetManager.addWidget(widgetLineRange, newRange, text); + this.setCacheResolvedConflict(lineRange.id, { newRange, - id: newLineRange.id, + id: lineRange.id, metadata: conflictMetadata!, // 保留原始冲突文本 - conflictText: (isRegenerate && codeAssemble) || validEditOperation[0].text, + conflictText, + text, isAccept: true, }); if (!isRegenerate) { // 记录处理数量 非重新生成 conflict 存在 const uri = this.getModel().uri.toString(); - const cacheConflictRanges = this.cacheConflicts.getAllConflictsByUri(uri); + const cacheConflictRanges = this.conflictParser.getAllConflictsByUri(uri); if (cacheConflictRanges) { const cacheConflict = cacheConflictRanges.find((cacheConflict) => { if (cacheConflict.isResolved) { return false; } - if (cacheConflict.range.equalsRange(range)) { + if (cacheConflict.range.equalsRange(newRange)) { return true; } - if (cacheConflict.text === codeAssemble) { + if (cacheConflict.text === conflictText) { return true; } }); if (cacheConflict && !cacheConflict.isResolved) { - this.cacheConflicts.setConflictResolved(uri, cacheConflict.id); + this.conflictParser.setConflictResolved(uri, cacheConflict.id); } } } @@ -822,7 +738,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont } if (ErrorResponse.is(resolveConflictResult)) { - this.loadingRange.delete(range); + this.loadingRange.delete(newRange); return Promise.resolve(resolveConflictResult); } } @@ -848,7 +764,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont if (!document) { return Promise.resolve(); } - const conflicts = this.cacheConflicts.scanDocument(document); + const conflicts = this.conflictParser.scanDocument(document); if (!conflicts.length) { return Promise.resolve(); } @@ -890,7 +806,7 @@ export class MergeConflictContribution extends Disposable implements CommandCont }); } - private async requestAiResolveConflict( + private async requestAIResolveConflict( metadata: IConflictContentMetadata, range: LineRange, isRegenerate = false, @@ -947,30 +863,49 @@ export class MergeConflictContribution extends Disposable implements CommandCont public launchConflictActionsEvent(eventData: Omit): void { const { range, action } = eventData; - if (action === REVOKE_ACTIONS) { - const cacheConflict = this.getCacheResolvedConflicts().get(range.id); - if (cacheConflict) { - const edit = { - range: cacheConflict.newRange, - text: cacheConflict.conflictText || cacheConflict.text, - }; - this.getModel()?.pushEditOperations(null, [edit], () => null); + + switch (action) { + case ACCEPT_CURRENT_ACTIONS: + { + const cacheConflict = this.getCacheResolvedConflicts().get(range.id); + if (cacheConflict) { + const edit = { + range: cacheConflict.newRange, + text: cacheConflict.text, + }; + this.getModel()?.pushEditOperations(null, [edit], () => null); + } + this.cleanWidget(range.id); + this.deleteCacheResolvedConflicts(range.id); + this.cleanDecoration(range.id); + } + break; + case REVOKE_ACTIONS: { + this.cleanWidget(range.id); + this.deleteCacheResolvedConflicts(range.id); + this.cleanDecoration(range.id); + break; + } + case AI_RESOLVE_REGENERATE_ACTIONS: { + const cacheConflict = this.getCacheResolvedConflicts().get(range.id); + if (cacheConflict) { + const lineRange = this.toLineRange(cacheConflict.newRange, range.id); + this.conflictAIAccept(undefined, lineRange, true); + } + this.cleanWidget(range.id); + this.cleanDecoration(range.id); + break; + } + case IGNORE_ACTIONS: { + this.cleanWidget(range.id); + this.cleanDecoration(range.id); + const resolvedConflict = this.getCacheResolvedConflicts().get(range.id)!; + this.setCacheResolvedConflict(range.id, { + ...resolvedConflict, + isClosed: true, + }); + break; } - this.cleanWidget(range.id); - this.deleteCacheResolvedConflicts(range.id); - this.cleanDecoration(range.id); - } else if (action === AI_RESOLVE_REGENERATE_ACTIONS) { - this.cleanWidget(range.id); - this.conflictAIAccept(undefined, range, true); - this.cleanDecoration(range.id); - } else if (action === IGNORE_ACTIONS) { - this.cleanWidget(range.id); - // this.deleteCacheResolvedConflicts(range.id); - const resolvedConflict = this.getCacheResolvedConflicts().get(range.id)!; - this.setCacheResolvedConflict(range.id, { - ...resolvedConflict, - isClosed: true, - }); } } @@ -999,6 +934,8 @@ export class MergeConflictContribution extends Disposable implements CommandCont if (this.decorationId2Range.has(id)) { this.decorationId2Range.delete(id); } + + this.updateCodeLensProvider(); } // 强制刷新 codelens diff --git a/packages/ai-native/src/browser/merge-conflict/override-resolve-result-widget.tsx b/packages/ai-native/src/browser/merge-conflict/override-resolve-result-widget.tsx index 8abd458eb6..395882ae74 100644 --- a/packages/ai-native/src/browser/merge-conflict/override-resolve-result-widget.tsx +++ b/packages/ai-native/src/browser/merge-conflict/override-resolve-result-widget.tsx @@ -1,35 +1,58 @@ import React, { ReactNode } from 'react'; -import { Injectable } from '@opensumi/di'; +import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di'; import { ContentWidgetContainerPanel } from '@opensumi/ide-core-browser/lib/components/ai-native/content-widget/containerPanel'; -import { IAiInlineResultIconItemsProps } from '@opensumi/ide-core-browser/lib/components/ai-native/inline-chat/result'; -import { localize, uuid } from '@opensumi/ide-core-common'; +import { IAIInlineResultIconItemsProps } from '@opensumi/ide-core-browser/lib/components/ai-native/inline-chat/result'; +import { localize } from '@opensumi/ide-core-common'; +import * as monaco from '@opensumi/ide-monaco'; import { LineRange } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/model/line-range'; import { - AiResolveConflictContentWidget, + ACCEPT_CURRENT_ACTIONS, + AIResolveConflictContentWidget, + ECompleteReason, IGNORE_ACTIONS, REVOKE_ACTIONS, } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/types'; import { ResultCodeEditor } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/view/editors/resultCodeEditor'; import { ResolveResultWidget, - WapperAiInlineResult, + WapperAIInlineResult, } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/widget/resolve-result-widget'; +import { InlineDiffWidget } from '../widget/inline-diff/inline-diff-widget'; + @Injectable({ multiple: true }) export class OverrideResolveResultWidget extends ResolveResultWidget { - protected uid: string = uuid(4); + @Autowired(INJECTOR_TOKEN) + protected injector: Injector; - constructor(protected readonly codeEditor: ResultCodeEditor, protected readonly lineRange: LineRange) { - super(codeEditor, lineRange); + constructor( + protected uid: string, + protected readonly codeEditor: ResultCodeEditor, + protected readonly lineRange: LineRange, + protected readonly range: monaco.IRange, + protected readonly text: string, + ) { + super(uid, codeEditor, lineRange); } protected isRenderThumbs(): boolean { return true; } - protected iconItems(): IAiInlineResultIconItemsProps[] { + protected iconItems(): IAIInlineResultIconItemsProps[] { return [ + { + icon: 'check', + text: localize('aiNative.inline.chat.operate.check.title'), + onClick: () => { + this.codeEditor.launchConflictActionsEvent({ + range: this.lineRange, + action: ACCEPT_CURRENT_ACTIONS, + reason: ECompleteReason.UserManual, + }); + }, + }, { icon: 'discard', text: localize('aiNative.operate.discard.title'), @@ -37,13 +60,32 @@ export class OverrideResolveResultWidget extends ResolveResultWidget { this.codeEditor.launchConflictActionsEvent({ range: this.lineRange, action: REVOKE_ACTIONS, + reason: ECompleteReason.UserManual, }); }, }, ]; } - public renderView(): ReactNode { + private inlineDiffWidget: InlineDiffWidget; + + private visibleDiffWidget(monacoEditor: monaco.ICodeEditor, range: monaco.IRange, answer: string): void { + if (this.inlineDiffWidget) { + this.inlineDiffWidget.dispose(); + } + + monacoEditor.setHiddenAreas([range], InlineDiffWidget._hideId); + this.inlineDiffWidget = this.injector.get(InlineDiffWidget, [monacoEditor, range, answer]); + this.inlineDiffWidget.create(); + this.inlineDiffWidget.showByLine(range.startLineNumber + 2, range.endLineNumber - range.startLineNumber + 2); + } + + override hide(): void { + super.hide(); + this.inlineDiffWidget?.dispose(); + } + + public override renderView(): ReactNode { const iconResultItems = this.iconItems(); const isRenderThumbs = this.isRenderThumbs(); @@ -51,12 +93,16 @@ export class OverrideResolveResultWidget extends ResolveResultWidget { this.codeEditor.launchConflictActionsEvent({ range: this.lineRange, action: IGNORE_ACTIONS, + reason: ECompleteReason.UserManual, }); }; + this.visibleDiffWidget(this.codeEditor.editor, this.range, this.text); + return ( - void; + onMaxLineCount: (n) => void; editor: ICodeEditor; } const DiffContentProvider = React.memo((props: IDiffContentProviderProps) => { - const { dto, onMaxLincCount, editor } = props; + const { dto, onMaxLineCount, editor } = props; const monacoService: MonacoService = useInjectable(MonacoService); const editorRef = useRef(null); @@ -67,9 +67,9 @@ const DiffContentProvider = React.memo((props: IDiffContentProviderProps) => { return; } - const { selection, modifiedValue } = dto; + const { range, modifiedValue } = dto; - const codeValueInRange = model.getValueInRange(selection); + const codeValueInRange = model.getValueInRange(range); const diffEditor = monacoService.createDiffEditor(editorRef.current!, { ...diffEditorOptions, lineDecorationsWidth: editor.getLayoutInfo().decorationsWidth, @@ -86,9 +86,9 @@ const DiffContentProvider = React.memo((props: IDiffContentProviderProps) => { original: originalModel, modified: modifiedModel, }); - diffEditor.revealLine(selection.startLineNumber, monaco.editor.ScrollType.Immediate); + diffEditor.revealLine(range.startLineNumber, monaco.editor.ScrollType.Immediate); - if (onMaxLincCount) { + if (onMaxLineCount) { const originalEditor = diffEditor.getOriginalEditor(); const modifiedEditor = diffEditor.getModifiedEditor(); @@ -99,7 +99,7 @@ const DiffContentProvider = React.memo((props: IDiffContentProviderProps) => { const modifiedLineCount = modifiedContentHeight / modifiedEditor.getOption(monacoApi.editor.EditorOption.lineHeight); - onMaxLincCount(Math.max(originLineCount, modifiedLineCount) + 1); + onMaxLineCount(Math.max(originLineCount, modifiedLineCount) + 1); } return () => { @@ -113,15 +113,16 @@ const DiffContentProvider = React.memo((props: IDiffContentProviderProps) => { }); @Injectable({ multiple: true }) -export class AIDiffWidget extends ZoneWidget { +export class InlineDiffWidget extends ZoneWidget { public static readonly _hideId = 'overlayDiff'; @Autowired(AppConfig) private configContext: AppConfig; - private readonly _onMaxLincCount = new Emitter(); - public readonly onMaxLincCount: Event = this._onMaxLincCount.event; - private selection: monaco.Selection; + private readonly _onMaxLineCount = new Emitter(); + public readonly onMaxLineCount: Event = this._onMaxLineCount.event; + + private range: monaco.IRange; private modifiedValue: string; private root: ReactDOMClient.Root | null; @@ -136,12 +137,12 @@ export class AIDiffWidget extends ZoneWidget {
{ + onMaxLineCount={(n) => { if (n) { this._relayout(n); - this._onMaxLincCount.fire(n); + this._onMaxLineCount.fire(n); } }} /> @@ -150,16 +151,17 @@ export class AIDiffWidget extends ZoneWidget { ); } - constructor(editor: ICodeEditor, selection: monaco.Selection, modifiedValue: string) { + constructor(editor: ICodeEditor, selection: monaco.IRange, modifiedValue: string) { super(editor, { showArrow: false, showFrame: false, arrowColor: undefined, frameColor: undefined, keepEditorSelection: true, + showInHiddenAreas: true, }); - this.selection = selection; + this.range = selection; this.modifiedValue = modifiedValue; } @@ -188,7 +190,7 @@ export class AIDiffWidget extends ZoneWidget { } public override hide(): void { - this.editor.setHiddenAreas([], AIDiffWidget._hideId); + this.editor.setHiddenAreas([], InlineDiffWidget._hideId); super.hide(); if (this.root) { this.root.unmount(); @@ -196,13 +198,6 @@ export class AIDiffWidget extends ZoneWidget { } public showByLine(line: number, lineNumber = 20): void { - /** - * 由于 monaco 在最新的版本中支持了 showInHiddenAreas 选项(见:https://github.com/microsoft/vscode/pull/181029),具备了在空白行显示 zonewidget 的能力 - * 所以这里暂时通过 hack 的方式使其能让 zonewidget 在空白处显示出来,后续需要升级 monaco 来实现 - */ - // @ts-ignore - this.editor._modelData.viewModel.coordinatesConverter.modelPositionIsVisible = () => true; - super.show( { startLineNumber: line, diff --git a/packages/ai-native/src/common/prompts/merge-conflict-prompt.ts b/packages/ai-native/src/common/prompts/merge-conflict-prompt.ts index 5f5acb7c51..1b17978f31 100644 --- a/packages/ai-native/src/common/prompts/merge-conflict-prompt.ts +++ b/packages/ai-native/src/common/prompts/merge-conflict-prompt.ts @@ -13,17 +13,17 @@ export class MergeConflictPromptManager extends BasePromptManager { return `The current solution to the code conflict is \n\`\`\`\n${text}\n\`\`\`\n, but I'm not fully satisfied with it. Could you please provide an alternative solution?`; } - public toThreeWayCodeAssemble(metadata: IConflictContentMetadata) { + public assembleCode(metadata: IConflictContentMetadata) { return `<<<<<<< HEAD\n${metadata.current}\n||||||| base\n${metadata.base}\n>>>>>>>\n${metadata.incoming}`; } - public convertDefaultThreeWayPrompt(metadata: IConflictContentMetadata) { - const codeAssemble = this.toThreeWayCodeAssemble(metadata); + public convertDefaultPrompt(metadata: IConflictContentMetadata) { + const codeAssemble = this.assembleCode(metadata); return this.toPrompt(codeAssemble); } - public convertDefaultThreeWayRegeneratePrompt(metadata: IConflictContentMetadata) { - const codeAssemble = this.toThreeWayCodeAssemble(metadata); + public convertDefaultRegeneratePrompt(metadata: IConflictContentMetadata) { + const codeAssemble = this.assembleCode(metadata); return this.toRegeneratePrompt(codeAssemble); } } diff --git a/packages/core-browser/src/common/common.command.ts b/packages/core-browser/src/common/common.command.ts index fca8e01247..cf653f2292 100644 --- a/packages/core-browser/src/common/common.command.ts +++ b/packages/core-browser/src/common/common.command.ts @@ -1069,3 +1069,23 @@ export namespace SCM_COMMANDS { id: 'git.openMergeEditor', }; } + +export namespace MERGE_CONFLICT_COMMANDS { + const CATEGORY = 'MergeConflict'; + export const AI_ACCEPT: Command = { + id: 'merge-conflict.ai.accept', + category: CATEGORY, + }; + export const ALL_RESET: Command = { + id: 'merge-conflict.ai.all-reset', + category: CATEGORY, + }; + export const AI_ALL_ACCEPT: Command = { + id: 'merge-conflict.ai.all-accept', + category: CATEGORY, + }; + export const AI_ALL_ACCEPT_STOP: Command = { + id: 'merge-conflict.ai.all-accept-stop', + category: CATEGORY, + }; +} diff --git a/packages/core-browser/src/components/actions/index.tsx b/packages/core-browser/src/components/actions/index.tsx index aff222ecdf..baac8c8790 100644 --- a/packages/core-browser/src/components/actions/index.tsx +++ b/packages/core-browser/src/components/actions/index.tsx @@ -3,7 +3,8 @@ import React, { useMemo, useState } from 'react'; import { Button, CheckBox, Icon } from '@opensumi/ide-components'; import { ClickParam, Menu } from '@opensumi/ide-components/lib/menu'; -import { isBoolean, strings } from '@opensumi/ide-core-common'; +import { CommandRegistry, IDisposable, isBoolean, strings } from '@opensumi/ide-core-common'; +import { GitCommands } from '@opensumi/ide-core-common/lib/commands/git'; import { AbstractMenuService, @@ -232,6 +233,8 @@ const InlineActionWidget: React.FC< > = React.memo(({ iconService, type = 'icon', data, context = [], className, afterClick, ...restProps }) => { const styles_iconAction = useDesignStyles(styles.iconAction, 'iconAction'); const styles_btnAction = useDesignStyles(styles.btnAction, 'btnAction'); + const commandRegistry = useInjectable(CommandRegistry); + const [loading, setLoading] = useState(false); const handleClick = React.useCallback( async (event?: React.MouseEvent, ...extraArgs: any[]) => { @@ -257,6 +260,38 @@ const InlineActionWidget: React.FC< [data, context], ); + React.useEffect(() => { + let dispose: IDisposable | undefined; + switch (data.id) { + case GitCommands.Stage: + { + const firstArg = context[0]; + if (!firstArg) { + break; + } + if (!firstArg.sourceUri) { + break; + } + dispose = commandRegistry.registerHandler(`${data.id}-${firstArg.sourceUri.toString()}`, { + execute: async () => { + if (typeof data.execute === 'function') { + await data.execute([...context]); + } + }, + }); + } + break; + default: + break; + } + + return () => { + if (dispose) { + dispose.dispose(); + } + }; + }, [data.id]); + const [title, label] = React.useMemo(() => { let title = data.tooltip || data.label; const label = data.label; diff --git a/packages/core-browser/src/components/ai-native/inline-chat/result.tsx b/packages/core-browser/src/components/ai-native/inline-chat/result.tsx index 59346511f6..affe7ee9c0 100644 --- a/packages/core-browser/src/components/ai-native/inline-chat/result.tsx +++ b/packages/core-browser/src/components/ai-native/inline-chat/result.tsx @@ -6,14 +6,14 @@ import { Thumbs } from '../thumbs'; import styles from './styles.module.less'; -export interface IAiInlineResultIconItemsProps { +export interface IAIInlineResultIconItemsProps { text: string | React.ReactNode; onClick: () => void; icon?: string; } export interface IAIInlineResultProps { - iconItems: IAiInlineResultIconItemsProps[]; + iconItems: IAIInlineResultIconItemsProps[]; isRenderThumbs?: boolean; isRenderClose?: boolean; closeClick?: () => void; diff --git a/packages/core-common/src/commands/git.ts b/packages/core-common/src/commands/git.ts new file mode 100644 index 0000000000..d6b44731f3 --- /dev/null +++ b/packages/core-common/src/commands/git.ts @@ -0,0 +1,9 @@ +export const MergeConflictCommands = { + Previous: 'merge-conflict.previous', + Next: 'merge-conflict.next', +} as const; + +export const GitCommands = { + Stage: 'git.stage', + StageAllMerge: 'git.stageAllMerge', +}; diff --git a/packages/core-common/src/types/ai-native/index.ts b/packages/core-common/src/types/ai-native/index.ts index 8c67a4dad9..86a9e5ffda 100644 --- a/packages/core-common/src/types/ai-native/index.ts +++ b/packages/core-common/src/types/ai-native/index.ts @@ -162,15 +162,29 @@ export const ChatAgentViewServiceToken = Symbol('ChatAgentViewServiceToken'); * Contribute Registry */ export interface IConflictContentMetadata { + /** + * @threeWay 当前分支的代码 + * @transitional 当前分支的代码 + */ current: string; + /** + * @threeWay 基础分支的代码 + * @transitional 无 + */ base: string; + /** + * @threeWay 远程分支的代码 + * @transitional 远程分支的代码 + */ incoming: string; - // 各分支的名称 currentName?: string; baseName?: string; incomingName?: string; + /** + * 如果是用户要求 regenerate 的话,这个字段代表上一次 AI 生成的结果 + */ resultContent?: string; } export interface IResolveConflictHandler { diff --git a/packages/editor/src/browser/doc-model/editor-document-model-service.ts b/packages/editor/src/browser/doc-model/editor-document-model-service.ts index 6388c57721..36e25496e2 100644 --- a/packages/editor/src/browser/doc-model/editor-document-model-service.ts +++ b/packages/editor/src/browser/doc-model/editor-document-model-service.ts @@ -1,5 +1,7 @@ import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di'; import { + Dispatcher, + IDisposable, IEditorDocumentChange, IEditorDocumentModelSaveResult, ILogger, @@ -27,6 +29,7 @@ import { EditorDocumentModelCreationEvent, EditorDocumentModelOptionExternalUpdatedEvent, IEditorDocumentModelContentRegistry, + IEditorDocumentModelCreationEventPayload, IEditorDocumentModelService, IPreferredModelOptions, } from './types'; @@ -67,7 +70,9 @@ export class EditorDocumentModelServiceImpl extends WithEventBus implements IEdi private preferredModelOptions = new Map(); - private _ready = new ReadyEvent(); + private _ready = this.registerDispose(new ReadyEvent()); + + private modelCreationEventDispatcher = this.registerDispose(new Dispatcher()); constructor() { super(); @@ -83,6 +88,7 @@ export class EditorDocumentModelServiceImpl extends WithEventBus implements IEdi this._delete(key); }); this._modelReferenceManager.onInstanceCreated((model) => { + this.modelCreationEventDispatcher.dispatch(model.uri.toString()); this.eventBus.fire( new EditorDocumentModelCreationEvent({ uri: model.uri, @@ -106,6 +112,10 @@ export class EditorDocumentModelServiceImpl extends WithEventBus implements IEdi ); } + onDocumentModelCreated(uri: string, listener: () => void): IDisposable { + return this.modelCreationEventDispatcher.on(uri)(listener); + } + private _delete(uri: string | URI): void { const modelDisposeDebounceTime = this.preferenceService.get('editor.modelDisposeTime', 3000); // debounce diff --git a/packages/editor/src/browser/doc-model/types.ts b/packages/editor/src/browser/doc-model/types.ts index 9297f93ffe..84271645dc 100644 --- a/packages/editor/src/browser/doc-model/types.ts +++ b/packages/editor/src/browser/doc-model/types.ts @@ -14,6 +14,8 @@ import { EOL, EndOfLineSequence } from '@opensumi/ide-monaco/lib/browser/monaco- import { IEditorDocumentModelContentChange, SaveReason } from '../../common'; import { IEditorDocumentModel, IEditorDocumentModelRef } from '../../common/editor'; +import { EditorDocumentModel } from './editor-document-model'; + export interface IDocModelUpdateOptions extends monaco.editor.ITextModelUpdateOptions { detectIndentation?: boolean; } @@ -119,6 +121,8 @@ export interface IPreferredModelOptions { } export interface IEditorDocumentModelService { + onDocumentModelCreated(uri: string, listener: () => void): IDisposable; + hasLanguage(languageId: string): boolean; createModelReference(uri: URI, reason?: string): Promise; diff --git a/packages/editor/src/browser/editor-collection.service.ts b/packages/editor/src/browser/editor-collection.service.ts index a236dc36c2..a8ef3069ba 100644 --- a/packages/editor/src/browser/editor-collection.service.ts +++ b/packages/editor/src/browser/editor-collection.service.ts @@ -62,7 +62,7 @@ export class EditorCollectionServiceImpl extends WithEventBus implements EditorC @Autowired(IEditorFeatureRegistry) protected readonly editorFeatureRegistry: EditorFeatureRegistryImpl; - private _editors: Set = new Set(); + private _editors: Set = new Set(); private _diffEditors: Set = new Set(); private _onCodeEditorCreate = new Emitter(); @@ -100,11 +100,11 @@ export class EditorCollectionServiceImpl extends WithEventBus implements EditorC return editor; } - public listEditors(): IMonacoImplEditor[] { + public listEditors(): ISumiEditor[] { return Array.from(this._editors.values()); } - public addEditors(editors: IMonacoImplEditor[]) { + public addEditors(editors: ISumiEditor[]) { const beforeSize = this._editors.size; editors.forEach((editor) => { if (!this._editors.has(editor)) { @@ -123,7 +123,7 @@ export class EditorCollectionServiceImpl extends WithEventBus implements EditorC } } - public removeEditors(editors: IMonacoImplEditor[]) { + public removeEditors(editors: ISumiEditor[]) { const beforeSize = this._editors.size; editors.forEach((editor) => { this._editors.delete(editor); @@ -199,7 +199,7 @@ export class EditorCollectionServiceImpl extends WithEventBus implements EditorC } } -export type IMonacoImplEditor = IEditor; +export type ISumiEditor = IEditor; export function insertSnippetWithMonacoEditor( editor: IMonacoCodeEditor, @@ -578,9 +578,9 @@ export class BrowserDiffEditor extends WithEventBus implements IDiffEditor { return null; } - public originalEditor: IMonacoImplEditor; + public originalEditor: ISumiEditor; - public modifiedEditor: IMonacoImplEditor; + public modifiedEditor: ISumiEditor; public _disposed: boolean; diff --git a/packages/editor/src/browser/editor-electron.contribution.ts b/packages/editor/src/browser/editor-electron.contribution.ts index 35d6d34e61..f98f2d1e20 100644 --- a/packages/editor/src/browser/editor-electron.contribution.ts +++ b/packages/editor/src/browser/editor-electron.contribution.ts @@ -3,7 +3,6 @@ import { ClientAppContribution, Domain, EDITOR_COMMANDS, - IClientApp, KeybindingContribution, KeybindingRegistry, electronEnv, @@ -44,7 +43,7 @@ export class EditorElectronContribution extends WithEventBus implements ClientAp /** * Return true in order to prevent exit */ - async onWillStop(app: IClientApp) { + async onWillStop() { if (await this.workbenchEditorService.closeAllOnlyConfirmOnce()) { return true; } diff --git a/packages/editor/src/browser/editor.decoration.service.ts b/packages/editor/src/browser/editor.decoration.service.ts index 8fcc9e361e..e69966b076 100644 --- a/packages/editor/src/browser/editor.decoration.service.ts +++ b/packages/editor/src/browser/editor.decoration.service.ts @@ -127,14 +127,16 @@ export class EditorDecorationCollectionService implements IEditorDecorationColle disposer.add(this.cssManager.addClass(className, styles)); disposer.add(this.cssManager.addClass(inlineClassName, inlineStyles)); if (options.after) { + const afterClassName = `${key}-after`; const styles = this.resolveContentCSSStyle(options.after); - disposer.add(this.cssManager.addClass(key + '::after', styles)); - afterContentClassName = `${inlineBlockSelector} ${key}`; + disposer.add(this.cssManager.addClass(afterClassName + '::after', styles)); + afterContentClassName = `${inlineBlockSelector} ${afterClassName}`; } if (options.before) { + const beforeClassName = `${key}-before`; const styles = this.resolveContentCSSStyle(options.before); - disposer.add(this.cssManager.addClass(key + '::before', styles)); - beforeContentClassName = `${inlineBlockSelector} ${key}`; + disposer.add(this.cssManager.addClass(beforeClassName + '::before', styles)); + beforeContentClassName = `${inlineBlockSelector} ${beforeClassName}`; } if (options.gutterIconPath) { const glyphMarginStyle = this.resolveCSSStyle({ diff --git a/packages/editor/src/browser/editor.module.less b/packages/editor/src/browser/editor.module.less index 6cbf03ce42..6e6814f071 100644 --- a/packages/editor/src/browser/editor.module.less +++ b/packages/editor/src/browser/editor.module.less @@ -568,57 +568,3 @@ } // -------------- styles for editor empty component ends -------------- - -// -------------- styles for merge editor component starts -------------- -.merge_editor_float_container { - display: flex; - background: var(--kt-panelTab-activeBackground); - box-shadow: inset 1px 1px 3px 0px var(--kt-panelTab-border); - padding: 16px 28px; - justify-content: end; - white-space: nowrap; - .merge_conflict_bottom_btn { - border: 1px solid var(--kt-button-disableForeground); - border-radius: 8px; - padding: 5px 16px; - background: var(--editor-background); - margin: 0 4px; - line-height: 22px; - justify-content: end; - white-space: nowrap; - cursor: pointer; - :global { - .kt-icon { - font-size: 12px; - } - } - :first-child { - margin-right: 8px; - } - } - - .magic_btn { - background-image: radial-gradient(circle at -21% -22%, #19cfff, #8429ff); - border: none; - font-weight: 500; - span { - color: #fff; - } - :global { - .kt-icon { - color: #fff; - font-size: 12px; - margin-right: 8px; - } - } - } - - .line_vertical { - background-color: var(--design-borderColor-common); - width: 1px; - min-width: 1px; - height: 24px; - margin: 4px; - } -} -// -------------- styles for merge editor component ends -------------- diff --git a/packages/editor/src/browser/hooks/useEditor.ts b/packages/editor/src/browser/hooks/useEditor.ts new file mode 100644 index 0000000000..aa5ff038fb --- /dev/null +++ b/packages/editor/src/browser/hooks/useEditor.ts @@ -0,0 +1,34 @@ +import { useEffect, useState } from 'react'; + +import { URI, useInjectable } from '@opensumi/ide-core-browser'; + +import { IEditorDocumentModelService } from '../doc-model/types'; +import { IEditorDocumentModelRef } from '../types'; + +export function useEditorDocumentModelRef(uri: URI) { + const documentService: IEditorDocumentModelService = useInjectable(IEditorDocumentModelService); + const [ref, setRef] = useState(null); + + useEffect(() => { + const run = () => { + const ref = documentService.getModelReference(uri); + if (ref) { + setRef(ref); + } + }; + + const toDispose = documentService.onDocumentModelCreated(uri.toString(), () => { + run(); + }); + + run(); + return () => { + toDispose.dispose(); + if (ref) { + ref.dispose(); + } + }; + }, [uri]); + + return ref; +} diff --git a/packages/editor/src/browser/hooks/useInMergeChanges.ts b/packages/editor/src/browser/hooks/useInMergeChanges.ts new file mode 100644 index 0000000000..dcc31be422 --- /dev/null +++ b/packages/editor/src/browser/hooks/useInMergeChanges.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react'; + +import { IContextKeyService, useInjectable } from '@opensumi/ide-core-browser'; + +const contextKey = 'git.mergeChangesObj'; + +export const gitMergeChangesSet = new Set([contextKey]); + +export function useInMergeChanges(uriStr: string) { + const contextKeyService = useInjectable(IContextKeyService); + + const [inMergeChanges, setInMergeChanges] = useState(false); + + useEffect(() => { + function run() { + const mergeChanges = contextKeyService.getValue>(contextKey) || {}; + setInMergeChanges(mergeChanges[uriStr] || false); + } + run(); + + const disposed = contextKeyService.onDidChangeContext(({ payload }) => { + if (payload.affectsSome(gitMergeChangesSet)) { + run(); + } + }); + return () => disposed.dispose(); + }, [uriStr]); + + return inMergeChanges; +} diff --git a/packages/ai-native/src/browser/merge-conflict/cache-conflicts.ts b/packages/editor/src/browser/merge-conflict/conflict-parser.ts similarity index 94% rename from packages/ai-native/src/browser/merge-conflict/cache-conflicts.ts rename to packages/editor/src/browser/merge-conflict/conflict-parser.ts index f15c3ce432..c28f72849f 100644 --- a/packages/ai-native/src/browser/merge-conflict/cache-conflicts.ts +++ b/packages/editor/src/browser/merge-conflict/conflict-parser.ts @@ -5,7 +5,7 @@ // Some code copied and modified from https://github.com/microsoft/vscode/blob/main/extensions/merge-conflict/src/mergeConflictParser.ts import { Injectable } from '@opensumi/di'; -import { Disposable, uuid } from '@opensumi/ide-core-common'; +import { Disposable, LRUCache, uuid } from '@opensumi/ide-core-common'; import * as monaco from '@opensumi/ide-monaco'; import { ICacheDocumentMergeConflict, IDocumentMergeConflictDescriptor, IMergeRegion } from './types'; @@ -50,14 +50,24 @@ export class TextLine { } } -// 内置 MergeConflict 插件 以支持AI交互 @Injectable() -export class CacheConflict extends Disposable { +export class MergeConflictParser extends Disposable { + cache = new LRUCache(100); + private _conflictTextCaches = new Map(); private _conflictRangeCaches = new Map(); + private static createCacheKey(document: monaco.editor.ITextModel) { + return `${document.uri.toString()}-${document.getAlternativeVersionId()}`; + } + scanDocument(document: monaco.editor.ITextModel) { + const cacheKey = MergeConflictParser.createCacheKey(document); + if (this.cache.has(cacheKey)) { + return this.cache.get(cacheKey)!; + } + // Scan each line in the document, we already know there is at least a <<<<<<< and // >>>>>> marker within the document, we need to group these into conflict ranges. // We initially build a scan match, that references the lines of the header, splitter @@ -129,7 +139,10 @@ export class CacheConflict extends Disposable { this._conflictRangeCaches.set(document.uri.toString(), conflictRanges); } - return conflictDescriptors?.filter(Boolean).map((descriptor) => new DocumentMergeConflict(descriptor)); + const result = conflictDescriptors?.filter(Boolean).map((descriptor) => new DocumentMergeConflict(descriptor)); + this.cache.set(cacheKey, result); + + return result; } getConflictText(uri: string) { return this._conflictTextCaches.get(uri); diff --git a/packages/editor/src/browser/merge-conflict/index.ts b/packages/editor/src/browser/merge-conflict/index.ts new file mode 100644 index 0000000000..ebfcb139d9 --- /dev/null +++ b/packages/editor/src/browser/merge-conflict/index.ts @@ -0,0 +1,2 @@ +export * from './conflict-parser'; +export * from './types'; diff --git a/packages/ai-native/src/browser/merge-conflict/types.ts b/packages/editor/src/browser/merge-conflict/types.ts similarity index 100% rename from packages/ai-native/src/browser/merge-conflict/types.ts rename to packages/editor/src/browser/merge-conflict/types.ts diff --git a/packages/editor/src/browser/merge-editor/MergeEditorFloatComponents.tsx b/packages/editor/src/browser/merge-editor/MergeEditorFloatComponents.tsx index c16d04b535..bef5b1fab5 100644 --- a/packages/editor/src/browser/merge-editor/MergeEditorFloatComponents.tsx +++ b/packages/editor/src/browser/merge-editor/MergeEditorFloatComponents.tsx @@ -5,43 +5,65 @@ import { AINativeConfigService, CommandRegistry, CommandService, - IContextKeyService, + DisposableStore, + MERGE_CONFLICT_COMMANDS, SCM_COMMANDS, URI, - Uri, localize, useInjectable, } from '@opensumi/ide-core-browser'; +import { formatLocalize } from '@opensumi/ide-core-common'; +import { MergeConflictCommands } from '@opensumi/ide-core-common/lib/commands/git'; -import styles from '../editor.module.less'; +import { useEditorDocumentModelRef } from '../hooks/useEditor'; +import { useInMergeChanges } from '../hooks/useInMergeChanges'; +import { DocumentMergeConflict, MergeConflictParser } from '../merge-conflict'; import { ReactEditorComponent } from '../types'; +import styles from './merge-editor.module.less'; + export const MergeEditorFloatComponents: ReactEditorComponent<{ uri: URI }> = ({ resource }) => { const aiNativeConfigService = useInjectable(AINativeConfigService); const commandService = useInjectable(CommandService); const commandRegistry = useInjectable(CommandRegistry); - const contextKeyService = useInjectable(IContextKeyService); + const mergeConflictParser: MergeConflictParser = useInjectable(MergeConflictParser); + + const editorModel = useEditorDocumentModelRef(resource.uri); const [isVisiable, setIsVisiable] = useState(false); + const [conflicts, setConflicts] = useState([]); - const gitMergeChangesSet = new Set(['git.mergeChanges']); + const inMergeChanges = useInMergeChanges(resource.uri.toString()); useEffect(() => { - const run = () => { - const mergeChanges = contextKeyService.getValue('git.mergeChanges') || []; - setIsVisiable(mergeChanges.some((value) => value.toString() === resource.uri.toString())); - }; - - const disposed = contextKeyService.onDidChangeContext(({ payload }) => { - if (payload.affectsSome(gitMergeChangesSet)) { - run(); - } - }); - run(); - return () => disposed.dispose(); - }, [resource]); + const disposables = new DisposableStore(); + + if (editorModel) { + const { instance } = editorModel; + const run = () => { + const conflicts = mergeConflictParser.scanDocument(instance.getMonacoModel()); + if (conflicts.length > 0) { + setIsVisiable(true); + setConflicts(conflicts); + } else { + setIsVisiable(false); + setConflicts([]); + } + }; + + disposables.add( + editorModel.instance.getMonacoModel().onDidChangeContent(() => { + run(); + }), + ); + run(); + return () => { + disposables.dispose(); + }; + } + }, [editorModel]); - const [isAiResolving, setIsAiResolving] = useState(false); + const [isAIResolving, setIsAIResolving] = useState(false); const handleOpenMergeEditor = useCallback(async () => { const { uri } = resource; @@ -52,31 +74,35 @@ export const MergeEditorFloatComponents: ReactEditorComponent<{ uri: URI }> = ({ }); }, [resource]); - const isSupportAiResolve = useCallback( + const isSupportAIResolve = useCallback( () => aiNativeConfigService.capabilities.supportsConflictResolve, [aiNativeConfigService], ); - const handlePrev = () => { - commandService.tryExecuteCommand('merge-conflict.previous'); - }; + const handlePrev = useCallback(() => { + commandService.tryExecuteCommand(MergeConflictCommands.Previous).then(() => { + // TODO: 编辑器向上滚动一行 + }); + }, []); - const handleNext = () => { - commandService.tryExecuteCommand('merge-conflict.next'); - }; + const handleNext = useCallback(() => { + commandService.tryExecuteCommand(MergeConflictCommands.Next).then(() => { + // TODO: 编辑器向上滚动一行 + }); + }, []); const handleAIResolve = useCallback(async () => { - setIsAiResolving(true); - if (isAiResolving) { - await commandService.executeCommand('merge-conflict.ai.all-accept-stop', resource.uri); + setIsAIResolving(true); + if (isAIResolving) { + await commandService.executeCommand(MERGE_CONFLICT_COMMANDS.AI_ALL_ACCEPT_STOP.id, resource.uri); } else { - await commandService.executeCommand('merge-conflict.ai.all-accept', resource.uri); + await commandService.executeCommand(MERGE_CONFLICT_COMMANDS.AI_ALL_ACCEPT.id, resource.uri); } - setIsAiResolving(false); - }, [resource, isAiResolving]); + setIsAIResolving(false); + }, [resource, isAIResolving]); const handleReset = useCallback(() => { - commandService.executeCommand('merge-conflict.ai.all-reset', resource.uri); + commandService.executeCommand(MERGE_CONFLICT_COMMANDS.ALL_RESET.id, resource.uri); }, [resource]); if (!isVisiable) { @@ -85,50 +111,62 @@ export const MergeEditorFloatComponents: ReactEditorComponent<{ uri: URI }> = ({ return (
-
- - +
+ {formatLocalize('merge-conflicts.merge.conflict.remain', conflicts.length)}
- - - - {isSupportAiResolve() && ( +
+
+ + +
+ + {inMergeChanges && ( + + )} - )} + {isSupportAIResolve() && ( + + )} +
); }; diff --git a/packages/editor/src/browser/merge-editor/merge-editor.module.less b/packages/editor/src/browser/merge-editor/merge-editor.module.less new file mode 100644 index 0000000000..643767dea7 --- /dev/null +++ b/packages/editor/src/browser/merge-editor/merge-editor.module.less @@ -0,0 +1,71 @@ +.merge_editor_float_container { + display: flex; + flex-direction: column; + background: var(--kt-panelTab-activeBackground); + box-shadow: inset 1px 1px 3px 0px var(--kt-panelTab-border); + justify-content: space-between; + white-space: nowrap; + // minimap's z-index is 5 + z-index: 6; + padding: 10px; + + .merge_editor_float_container_info { + width: 100%; + display: flex; + padding-left: 20px; + } + + .merge_editor_float_container_operation_bar { + width: 100%; + display: flex; + justify-content: flex-end; + white-space: nowrap; + padding-top: 4px; + padding-right: 20px; + } + + .merge_conflict_bottom_btn { + border: 1px solid var(--kt-button-disableForeground); + border-radius: 8px; + padding: 5px 16px; + background: var(--editor-background); + color: var(--editor-foreground); + margin: 0 4px; + line-height: 22px; + justify-content: end; + white-space: nowrap; + cursor: pointer; + :global { + .kt-icon { + font-size: 12px; + } + } + :first-child { + margin-right: 8px; + } + } + + .magic_btn { + background-image: radial-gradient(circle at -21% -22%, #19cfff, #8429ff); + border: none; + font-weight: 500; + span { + color: #fff; + } + :global { + .kt-icon { + color: #fff; + font-size: 12px; + margin-right: 8px; + } + } + } + + .line_vertical { + background-color: var(--design-borderColor-common); + width: 1px; + min-width: 1px; + height: 24px; + margin: 4px; + } +} diff --git a/packages/editor/src/browser/merge-editor/merge-editor.provider.ts b/packages/editor/src/browser/merge-editor/merge-editor.provider.ts index 7aaee7bd27..480099117d 100644 --- a/packages/editor/src/browser/merge-editor/merge-editor.provider.ts +++ b/packages/editor/src/browser/merge-editor/merge-editor.provider.ts @@ -22,6 +22,9 @@ export class MergeEditorResourceProvider extends WithEventBus implements IResour const resultEditorUri = new URI(output); const icon = this.labelService.getIcon(resultEditorUri); return { + // 如果设置为 true,再打开时没有找到对应的 provider 会报错 + // TODO: 需要增加一个标记,说明这个资源要在某个插件加载后才能 revive + supportsRevive: false, name, icon, uri, diff --git a/packages/editor/src/common/editor.ts b/packages/editor/src/common/editor.ts index 2588d65ad3..0d4834979b 100644 --- a/packages/editor/src/common/editor.ts +++ b/packages/editor/src/common/editor.ts @@ -149,7 +149,7 @@ export enum EditorType { } /** - * 一个IEditor代表了一个最小的编辑器单元,可以是CodeEditor中的一个,也可以是DiffEditor中的两个 + * 一个IEditor代表了一个最小的编辑器单元,可以是 CodeEditor 中的一个,也可以是 DiffEditor 中的两个 */ export interface IEditor { /** diff --git a/packages/extension/src/browser/vscode/api/main.thread.editor.ts b/packages/extension/src/browser/vscode/api/main.thread.editor.ts index c1b900ad15..f5344637c7 100644 --- a/packages/extension/src/browser/vscode/api/main.thread.editor.ts +++ b/packages/extension/src/browser/vscode/api/main.thread.editor.ts @@ -31,7 +31,7 @@ import { import { BrowserDiffEditor, EditorCollectionServiceImpl, - IMonacoImplEditor, + ISumiEditor, } from '@opensumi/ide-editor/lib/browser/editor-collection.service'; import { WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service'; import * as monaco from '@opensumi/ide-monaco'; @@ -89,7 +89,7 @@ export class MainThreadEditorService extends WithEventBus implements IMainThread const editors = this.editorService.editorGroups .map((group) => { if (group.currentOpenType && isEditor(group.currentOpenType)) { - const editor = group.currentEditor as IMonacoImplEditor; + const editor = group.currentEditor as ISumiEditor; if (!editor.currentDocumentModel) { return undefined; } @@ -195,7 +195,7 @@ export class MainThreadEditorService extends WithEventBus implements IMainThread } } - private getEditor(id: string): IMonacoImplEditor | undefined { + private getEditor(id: string): ISumiEditor | undefined { const group = this.getGroup(id); if (!group) { return; @@ -203,7 +203,7 @@ export class MainThreadEditorService extends WithEventBus implements IMainThread const currentResource = group.currentResource; if (currentResource && group.currentOpenType && isEditor(group.currentOpenType)) { if (id === getTextEditorId(group, currentResource.uri)) { - return group.currentEditor as IMonacoImplEditor; + return group.currentEditor as ISumiEditor; } if ( group.currentOpenType?.type === EditorOpenType.diff && @@ -277,7 +277,7 @@ export class MainThreadEditorService extends WithEventBus implements IMainThread payload.newOpenType && (payload.newOpenType.type === EditorOpenType.code || payload.newOpenType.type === EditorOpenType.diff) ) { - const editor = payload.group.currentEditor as IMonacoImplEditor; + const editor = payload.group.currentEditor as ISumiEditor; if (!editor.currentDocumentModel) { // noop } else if (!this.documents.isDocSyncEnabled(editor.currentDocumentModel.uri)) { @@ -423,13 +423,10 @@ export class MainThreadEditorService extends WithEventBus implements IMainThread this.addDispose( this.eventBus.on(EditorConfigurationChangedEvent, (e: EditorConfigurationChangedEvent) => { const editorId = getTextEditorId(e.payload.group, e.payload.resource.uri); - if ( - e.payload.group.currentEditor && - (e.payload.group.currentEditor as IMonacoImplEditor).monacoEditor.getModel() - ) { + if (e.payload.group.currentEditor && (e.payload.group.currentEditor as ISumiEditor).monacoEditor.getModel()) { this.batchPropertiesChanges({ id: editorId, - options: getEditorOption((e.payload.group.currentEditor as IMonacoImplEditor).monacoEditor), + options: getEditorOption((e.payload.group.currentEditor as ISumiEditor).monacoEditor), }); } }), diff --git a/packages/i18n/src/common/en-US.lang.ts b/packages/i18n/src/common/en-US.lang.ts index 9d978c9f6f..d9ab81f189 100644 --- a/packages/i18n/src/common/en-US.lang.ts +++ b/packages/i18n/src/common/en-US.lang.ts @@ -2,6 +2,7 @@ import { LOCALE_TYPES } from '@opensumi/ide-core-common/lib/const'; import { browserViews } from './contributes/en-US.lang'; import { editorLocalizations } from './editor/en-US'; +import { enUS as mergeConflicts } from './merge-conflicts/en-US.lang'; export const localizationBundle = { languageId: LOCALE_TYPES.EN_US, @@ -1434,14 +1435,15 @@ export const localizationBundle = { 'mergeEditor.conflict.action.apply.confirm.continue': 'Continue Merge', 'mergeEditor.conflict.action.apply.confirm.complete': 'Apply Changes', 'mergeEditor.action.button.apply': 'Apply', - 'mergeEditor.action.button.accept.left': 'Accept left', - 'mergeEditor.action.button.accept.right': 'Accept right', - 'mergeEditor.open.3way': '3-way Editor', - 'mergeEditor.conflict.prev': 'Previous conflict', - 'mergeEditor.conflict.next': 'Next conflict', - 'mergeEditor.conflict.resolve.all': 'AI one click solution', - 'mergeEditor.conflict.resolve.all.stop': 'Stop All', - 'mergeEditor.open.tradition': 'Tradition editor', + 'mergeEditor.action.button.apply-and-stash': 'Apply and Stash', + 'mergeEditor.action.button.accept.left': 'Accept Left', + 'mergeEditor.action.button.accept.right': 'Accept Right', + 'mergeEditor.open.3way': '3-Way Editor', + 'mergeEditor.conflict.prev': 'Previous Conflict', + 'mergeEditor.conflict.next': 'Next Conflict', + 'mergeEditor.conflict.ai.resolve.all': 'AI Resolution', + 'mergeEditor.conflict.ai.resolve.all.stop': 'Stop All', + 'mergeEditor.open.tradition': 'Tradition Editor', // #region AI Native 'aiNative.chat.ai.assistant.name': 'AI Assistant', @@ -1491,5 +1493,6 @@ export const localizationBundle = { ...browserViews, ...editorLocalizations, + ...mergeConflicts, }, }; diff --git a/packages/i18n/src/common/merge-conflicts/en-US.lang.ts b/packages/i18n/src/common/merge-conflicts/en-US.lang.ts new file mode 100644 index 0000000000..bf2397ab27 --- /dev/null +++ b/packages/i18n/src/common/merge-conflicts/en-US.lang.ts @@ -0,0 +1,13 @@ +export const enUS: Record = { + 'merge-conflicts.merge.type.auto': 'auto merged', + 'merge-conflicts.merge.type.manual': 'merged', + 'merge-conflicts.conflicts.summary': 'Total {0} conflicts, ({1})', + 'merge-conflicts.conflicts.all-resolved': 'All resolved', + 'merge-conflicts.conflicts.partial-resolved': 'Resolved {0} conflicts, {1} remaining', + 'merge-conflicts.non-conflicts.summary': 'Non-conflict changes {0} in total', + 'merge-conflicts.non-conflicts.progress': '{0} non-conflict changes have been {1}', + 'merge-conflicts.non-conflicts.from.left': 'target branch: {0} places', + 'merge-conflicts.non-conflicts.from.right': 'source branch: {0} places', + 'merge-conflicts.non-conflicts.from.base': 'base branch: {0} places', + 'merge-conflicts.merge.conflict.remain': 'Remaining unresolved conflicts {0} places', +}; diff --git a/packages/i18n/src/common/merge-conflicts/zh-CN.lang.ts b/packages/i18n/src/common/merge-conflicts/zh-CN.lang.ts new file mode 100644 index 0000000000..02ba47fada --- /dev/null +++ b/packages/i18n/src/common/merge-conflicts/zh-CN.lang.ts @@ -0,0 +1,13 @@ +export const zhCN: Record = { + 'merge-conflicts.merge.type.auto': '自动合并', + 'merge-conflicts.merge.type.manual': '合并', + 'merge-conflicts.conflicts.summary': '冲突变更共 {0} 处, ({1})', + 'merge-conflicts.conflicts.all-resolved': '已全部解决', + 'merge-conflicts.conflicts.partial-resolved': '已解决 {0} 处, 剩余 {1} 处', + 'merge-conflicts.non-conflicts.summary': '非冲突变更共 {0} 处', + 'merge-conflicts.non-conflicts.progress': '{0} 处非冲突变更已{1}', + 'merge-conflicts.non-conflicts.from.left': '目标分支: {0} 处', + 'merge-conflicts.non-conflicts.from.right': '来源分支: {0} 处', + 'merge-conflicts.non-conflicts.from.base': '两者: {0} 处', + 'merge-conflicts.merge.conflict.remain': '剩余未解决冲突 {0} 处', +}; diff --git a/packages/i18n/src/common/zh-CN.lang.ts b/packages/i18n/src/common/zh-CN.lang.ts index cdbe26d4e7..8e948266c7 100644 --- a/packages/i18n/src/common/zh-CN.lang.ts +++ b/packages/i18n/src/common/zh-CN.lang.ts @@ -2,6 +2,7 @@ import { LOCALE_TYPES } from '@opensumi/ide-core-common/lib/const'; import { browserViews } from './contributes/zh-CN.lang'; import { editorLocalizations } from './editor/zh-CN'; +import { zhCN as mergeConflicts } from './merge-conflicts/zh-CN.lang'; export const localizationBundle = { languageId: LOCALE_TYPES.ZH_CN, @@ -1203,13 +1204,14 @@ export const localizationBundle = { 'mergeEditor.conflict.action.apply.confirm.continue': '继续合并', 'mergeEditor.conflict.action.apply.confirm.complete': '确认保存并更改', 'mergeEditor.action.button.apply': '应用更改', + 'mergeEditor.action.button.apply-and-stash': '应用并暂存', 'mergeEditor.action.button.accept.left': '接受左边', 'mergeEditor.action.button.accept.right': '接受右边', 'mergeEditor.open.3way': '3-way 编辑器', 'mergeEditor.conflict.prev': '上一处冲突', 'mergeEditor.conflict.next': '下一处冲突', - 'mergeEditor.conflict.resolve.all': 'AI一键解决', - 'mergeEditor.conflict.resolve.all.stop': '全部停止', + 'mergeEditor.conflict.ai.resolve.all': 'AI 解决', + 'mergeEditor.conflict.ai.resolve.all.stop': '全部停止', 'mergeEditor.open.tradition': '传统编辑器', 'workbench.quickOpen.preserveInput': '是否在 QuickOpen 的输入框(包括命令面板)中保留上次输入的内容', @@ -1256,5 +1258,6 @@ export const localizationBundle = { ...browserViews, ...editorLocalizations, + ...mergeConflicts, }, }; diff --git a/packages/monaco/src/browser/contrib/merge-editor/mapping-manager.service.ts b/packages/monaco/src/browser/contrib/merge-editor/mapping-manager.service.ts index a172bdb3dd..ed587b65c9 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/mapping-manager.service.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/mapping-manager.service.ts @@ -1,25 +1,46 @@ import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di'; import { Disposable } from '@opensumi/ide-core-common'; +import { distinct } from '@opensumi/monaco-editor-core/esm/vs/base/common/arrays'; import { DetailedLineRangeMapping } from '../../../common/diff'; +import { MappingManagerDataStore } from './mapping-manager.store'; import { DocumentMapping } from './model/document-mapping'; import { LineRange } from './model/line-range'; -import { EDiffRangeTurn, ETurnDirection, EditorViewType } from './types'; +import { ECompleteReason, EDiffRangeTurn, ETurnDirection, EditorViewType } from './types'; @Injectable() export class MappingManagerService extends Disposable { @Autowired(INJECTOR_TOKEN) private readonly injector: Injector; + @Autowired(MappingManagerDataStore) + private readonly dataStore: MappingManagerDataStore; + + /** + * 与左侧编辑器的映射关系,见 {@link EDiffRangeTurn.ORIGIN} + */ public documentMappingTurnLeft: DocumentMapping; + /** + * 与右侧编辑器的映射关系,见 {@link EDiffRangeTurn.MODIFIED} + */ public documentMappingTurnRight: DocumentMapping; + protected markCompleteFactoryTurnLeft: (range: LineRange, reason: ECompleteReason) => void; + protected markCompleteFactoryTurnRight: (range: LineRange, reason: ECompleteReason) => void; + protected revokeActionsFactoryTurnLeft: (oppositeRange: LineRange) => void; + protected revokeActionsFactoryTurnRight: (oppositeRange: LineRange) => void; + constructor() { super(); this.documentMappingTurnLeft = this.injector.get(DocumentMapping, [EDiffRangeTurn.ORIGIN]); this.documentMappingTurnRight = this.injector.get(DocumentMapping, [EDiffRangeTurn.MODIFIED]); + + this.markCompleteFactoryTurnLeft = this.markCompleteFactory(EDiffRangeTurn.ORIGIN); + this.markCompleteFactoryTurnRight = this.markCompleteFactory(EDiffRangeTurn.MODIFIED); + this.revokeActionsFactoryTurnLeft = this.revokeActionsFactory(EDiffRangeTurn.ORIGIN); + this.revokeActionsFactoryTurnRight = this.revokeActionsFactory(EDiffRangeTurn.MODIFIED); } private revokeActionsFactory(turn: EDiffRangeTurn): (oppositeRange: LineRange) => void { @@ -31,29 +52,29 @@ export class MappingManagerService extends Disposable { return; } - range.setComplete(false); + range.cancel(); /** * 这里需要从 mapping 的 adjacentComputeRangeMap 集合里获取并修改 complete 状态,否则变量内存就不是指向同一引用 */ const realOppositeRange = mapping.adjacentComputeRangeMap.get(range.id); if (realOppositeRange) { - realOppositeRange.setComplete(false); + realOppositeRange.cancel(); } }; } - private markCompleteFactory(turn: EDiffRangeTurn): (range: LineRange) => void { + private markCompleteFactory(turn: EDiffRangeTurn): (range: LineRange, reason: ECompleteReason) => void { const mapping = turn === EDiffRangeTurn.ORIGIN ? this.documentMappingTurnLeft : this.documentMappingTurnRight; - return (range: LineRange) => { + return (range: LineRange, reason: ECompleteReason) => { const oppositeRange = mapping.adjacentComputeRangeMap.get(range.id); if (!oppositeRange) { return; } // 标记该 range 区域已经解决完成 - range.setComplete(true); - oppositeRange.setComplete(true); + range.done(reason); + oppositeRange.done(reason); /** * 如果被标记 complete 的 range 是 merge range 合成的,则需要将另一个 mapping 的对应关系也标记成 complete @@ -72,7 +93,7 @@ export class MappingManagerService extends Disposable { return; } - adjacentRange.setComplete(true); + adjacentRange.done(reason); } }; } @@ -85,20 +106,20 @@ export class MappingManagerService extends Disposable { this.documentMappingTurnRight.inputComputeResultRangeMapping(changes); } - public markCompleteTurnLeft(range: LineRange): void { - this.markCompleteFactory(EDiffRangeTurn.ORIGIN)(range); + public markCompleteTurnLeft(range: LineRange, reason: ECompleteReason): void { + this.markCompleteFactoryTurnLeft(range, reason); } - public markCompleteTurnRight(range: LineRange): void { - this.markCompleteFactory(EDiffRangeTurn.MODIFIED)(range); + public markCompleteTurnRight(range: LineRange, reason: ECompleteReason): void { + this.markCompleteFactoryTurnRight(range, reason); } public revokeActionsTurnLeft(oppositeRange: LineRange): void { - this.revokeActionsFactory(EDiffRangeTurn.ORIGIN)(oppositeRange); + this.revokeActionsFactoryTurnLeft(oppositeRange); } public revokeActionsTurnRight(oppositeRange: LineRange): void { - this.revokeActionsFactory(EDiffRangeTurn.MODIFIED)(oppositeRange); + this.revokeActionsFactoryTurnRight(oppositeRange); } public clearMapping(): void { @@ -154,4 +175,16 @@ export class MappingManagerService extends Disposable { [EditorViewType.INCOMING]: turnRightRange, }; } + + /** + * 去重相同 id 或位置一样的 line range + * + * 返回所有的 diff range + */ + public getAllDiffRanges(): LineRange[] { + return distinct( + this.documentMappingTurnLeft.getModifiedRange().concat(this.documentMappingTurnRight.getOriginalRange()), + (range) => range.id, + ); + } } diff --git a/packages/monaco/src/browser/contrib/merge-editor/mapping-manager.store.ts b/packages/monaco/src/browser/contrib/merge-editor/mapping-manager.store.ts new file mode 100644 index 0000000000..b792069eb5 --- /dev/null +++ b/packages/monaco/src/browser/contrib/merge-editor/mapping-manager.store.ts @@ -0,0 +1,98 @@ +import { action, computed, makeAutoObservable, observable } from 'mobx'; + +import { Injectable } from '@opensumi/di'; + +export interface IConflictsCount { + total: number; + resolved: number; + lefted: number; + nonConflicts: number; +} + +export interface INonConflictingChangesResolvedCount { + total: number; + left: number; + right: number; + both: number; + userManualResolveNonConflicts: boolean; +} + +@Injectable() +export class MappingManagerDataStore { + constructor() { + makeAutoObservable(this); + } + + @observable + conflictsTotal = 0; + + @observable + nonConflictsUnresolvedCount = 0; + + @observable + conflictsResolvedCount = 0; + + @computed + get conflictsCount(): IConflictsCount { + return { + total: this.conflictsTotal, + resolved: this.conflictsResolvedCount, + lefted: this.conflictsTotal - this.conflictsResolvedCount, + nonConflicts: this.nonConflictsUnresolvedCount, + }; + } + + @observable + protected nonConflictingChangesResolvedTotal = 0; + @observable + protected nonConflictingChangesResolvedLeft = 0; + @observable + protected nonConflictingChangesResolvedRight = 0; + @observable + protected nonConflictingChangesResolvedBoth = 0; + @observable + protected userManualResolveNonConflicts = false; + + @action + updateConflictsCount(data: Partial>) { + if (typeof data.total !== 'undefined') { + this.conflictsTotal = data.total; + } + if (typeof data.resolved !== 'undefined') { + this.conflictsResolvedCount = data.resolved; + } + if (typeof data.nonConflicts !== 'undefined') { + this.nonConflictsUnresolvedCount = data.nonConflicts; + } + } + + @computed + get nonConflictingChangesResolvedCount(): INonConflictingChangesResolvedCount { + return { + total: this.nonConflictingChangesResolvedTotal, + left: this.nonConflictingChangesResolvedLeft, + right: this.nonConflictingChangesResolvedRight, + both: this.nonConflictingChangesResolvedBoth, + userManualResolveNonConflicts: this.userManualResolveNonConflicts, + }; + } + + @action + updateNonConflictingChangesResolvedCount(data: Partial) { + if (typeof data.total !== 'undefined') { + this.nonConflictingChangesResolvedTotal = data.total; + } + if (typeof data.left !== 'undefined') { + this.nonConflictingChangesResolvedLeft = data.left; + } + if (typeof data.right !== 'undefined') { + this.nonConflictingChangesResolvedRight = data.right; + } + if (typeof data.both !== 'undefined') { + this.nonConflictingChangesResolvedBoth = data.both; + } + if (typeof data.userManualResolveNonConflicts !== 'undefined') { + this.userManualResolveNonConflicts = data.userManualResolveNonConflicts; + } + } +} diff --git a/packages/monaco/src/browser/contrib/merge-editor/merge-editor-widget.tsx b/packages/monaco/src/browser/contrib/merge-editor/merge-editor-widget.tsx index 8a03f158be..eaabbcefae 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/merge-editor-widget.tsx +++ b/packages/monaco/src/browser/contrib/merge-editor/merge-editor-widget.tsx @@ -23,6 +23,7 @@ import { ICodeEditor, IDiffEditorOptions, IEditorOptions, IModelDeltaDecoration import { StandaloneServices } from '../../monaco-api/services'; import { IPosition, Position } from '../../monaco-api/types'; +import { MappingManagerService } from './mapping-manager.service'; import { MergeEditorService } from './merge-editor.service'; import { EditorViewType, IMergeEditorViewState } from './types'; import { Grid } from './view/grid'; @@ -46,6 +47,9 @@ export class MergeEditorWidget extends Disposable implements IMergeEditorEditor @Autowired(MergeEditorService) private readonly mergeEditorService: MergeEditorService; + @Autowired(MappingManagerService) + private readonly mappingManagerService: MappingManagerService; + private readonly _id: number; private readonly viewStateMap: Map = new Map(); private outputUri: URI | undefined; @@ -76,6 +80,12 @@ export class MergeEditorWidget extends Disposable implements IMergeEditorEditor const { ancestor } = nutrition; const { baseContent, textModel } = ancestor!; (textModel as ITextModel).setValue(baseContent); + + // 取出 result editor 的 model, 重置回原来的文档内容 + const resultModel = this.getResultEditor().getModel(); + if (resultModel) { + (resultModel as ITextModel).setValue(baseContent); + } } } }), diff --git a/packages/monaco/src/browser/contrib/merge-editor/merge-editor.service.ts b/packages/monaco/src/browser/contrib/merge-editor/merge-editor.service.ts index ccdcb328c9..a41adf107b 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/merge-editor.service.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/merge-editor.service.ts @@ -11,6 +11,7 @@ import { import { MergeConflictReportService } from '@opensumi/ide-core-browser/lib/ai-native/conflict-report.service'; import { message } from '@opensumi/ide-core-browser/lib/components'; import { URI, formatLocalize, runWhenIdle } from '@opensumi/ide-core-common'; +import { GitCommands } from '@opensumi/ide-core-common/lib/commands/git'; import { IFileServiceClient } from '@opensumi/ide-file-service'; import { IDialogService } from '@opensumi/ide-overlay'; @@ -18,7 +19,7 @@ import { DetailedLineRangeMapping } from '../../../common/diff'; import { MappingManagerService } from './mapping-manager.service'; import { ComputerDiffModel } from './model/computer-diff'; -import { ACCEPT_CURRENT_ACTIONS, APPEND_ACTIONS, IEditorMountParameter } from './types'; +import { ACCEPT_CURRENT_ACTIONS, APPEND_ACTIONS, ECompleteReason, IEditorMountParameter } from './types'; import { ActionsManager } from './view/actions-manager'; import { CurrentCodeEditor } from './view/editors/currentCodeEditor'; import { IncomingCodeEditor } from './view/editors/incomingCodeEditor'; @@ -60,7 +61,7 @@ export class MergeEditorService extends Disposable { private computerDiffModel: ComputerDiffModel; private actionsManager: ActionsManager; - private isCancelAllAiResolveConflict = false; + private isCancelAllAIResolveConflict = false; public scrollSynchronizer: ScrollSynchronizer; public stickinessConnectManager: StickinessConnectManager; @@ -117,7 +118,9 @@ export class MergeEditorService extends Disposable { } runWhenIdle(() => { - const conflictPointRanges = this.resultView.getAllDiffRanges().filter((range) => range.isAiConflictPoint); + const conflictPointRanges = this.mappingManagerService + .getAllDiffRanges() + .filter((range) => range.isConflictPoint); if (flag && conflictPointRanges.every((r) => !!r.getIntelligentStateModel().isLoading === false)) { this._onHasIntelligentLoadingChange.fire(false); this.loadingDispose.dispose(); @@ -170,12 +173,12 @@ export class MergeEditorService extends Disposable { this.mergeConflictReportService.dispose(); } - public async acceptLeft(isIgnoreAi = false): Promise { + public acceptLeft(ignoreConflict = false, reason: ECompleteReason): void { const mappings = this.mappingManagerService.documentMappingTurnLeft; const lineRanges = mappings.getOriginalRange(); lineRanges .filter((range) => range.isComplete === false) - .filter((range) => (isIgnoreAi && range.isAiConflictPoint ? null : range)) + .filter((range) => (ignoreConflict && range.isConflictPoint ? null : range)) .forEach((range) => { if (range.isMerge) { const oppositeRange = this.mappingManagerService.documentMappingTurnLeft.adjacentComputeRangeMap.get( @@ -185,6 +188,7 @@ export class MergeEditorService extends Disposable { this.currentView.launchConflictActionsEvent({ range, action: APPEND_ACTIONS, + reason, }); return; } @@ -193,16 +197,17 @@ export class MergeEditorService extends Disposable { this.currentView.launchConflictActionsEvent({ range, action: ACCEPT_CURRENT_ACTIONS, + reason, }); }); } - public async acceptRight(isIgnoreAi = false): Promise { + public acceptRight(ignoreConflict = false, reason: ECompleteReason): void { const mappings = this.mappingManagerService.documentMappingTurnRight; const lineRanges = mappings.getModifiedRange(); lineRanges .filter((range) => range.isComplete === false) - .filter((range) => (isIgnoreAi && range.isAiConflictPoint ? null : range)) + .filter((range) => (ignoreConflict && range.isConflictPoint ? null : range)) .forEach((range) => { if (range.isMerge) { const oppositeRange = this.mappingManagerService.documentMappingTurnRight.adjacentComputeRangeMap.get( @@ -212,6 +217,7 @@ export class MergeEditorService extends Disposable { this.currentView.launchConflictActionsEvent({ range, action: APPEND_ACTIONS, + reason, }); return; } @@ -220,11 +226,12 @@ export class MergeEditorService extends Disposable { this.incomingView.launchConflictActionsEvent({ range, action: ACCEPT_CURRENT_ACTIONS, + reason, }); }); } - public async accept(): Promise { + public async accept(): Promise { const continueText = localize('mergeEditor.conflict.action.apply.confirm.continue'); const completeText = localize('mergeEditor.conflict.action.apply.confirm.complete'); @@ -244,14 +251,14 @@ export class MergeEditorService extends Disposable { const model = this.resultView.getModel(); - const allRanges = this.resultView.getAllDiffRanges(); - const useAiConflictPointNum = allRanges.filter( + const allRanges = this.mappingManagerService.getAllDiffRanges(); + const useAIConflictPointNum = allRanges.filter( (range) => range.getIntelligentStateModel().isComplete === true, ).length; let receiveNum = 0; allRanges - .filter((range) => range.isAiConflictPoint && range.getIntelligentStateModel().isComplete) + .filter((range) => range.isConflictPoint && range.getIntelligentStateModel().isComplete) .forEach((range) => { const intelligentStateModel = range.getIntelligentStateModel(); const preAnswerCode = intelligentStateModel.answerCode; @@ -263,7 +270,7 @@ export class MergeEditorService extends Disposable { }); this.mergeConflictReportService.report(this.resultView.getUri(), { - useAiConflictPointNum, + useAiConflictPointNum: useAIConflictPointNum, receiveNum, }); @@ -275,6 +282,7 @@ export class MergeEditorService extends Disposable { this.fireRestoreState(uri); await this.commandService.executeCommand(EDITOR_COMMANDS.CLOSE.id); + await this.commandService.executeCommand(`${GitCommands.Stage}-${uri.toString()}`); }; const { completeCount, shouldCount } = this.resultView.completeSituation(); @@ -285,43 +293,44 @@ export class MergeEditorService extends Disposable { ]); if (result === continueText) { - return; + return false; } if (result === completeText) { await saveApply(); } - return; + return true; } else { await saveApply(); + return true; } } - public async stopAllAiResolveConflict(): Promise { - this.isCancelAllAiResolveConflict = true; + public async stopAllAIResolveConflict(): Promise { + this.isCancelAllAIResolveConflict = true; this.resultView.cancelRequestToken(); this.resultView.hideStopWidget(); } - public async handleAiResolveConflict(): Promise { + public async handleAIResolveConflict(): Promise { this.mergeConflictReportService.reportIncrementNum(this.resultView.getUri(), 'clickAllNum'); this.listenIntelligentLoadingChange(); runWhenIdle(() => { - this.acceptLeft(true); + this.acceptLeft(true, ECompleteReason.AutoResolvedNonConflictBeforeRunAI); }, 0); runWhenIdle(() => { - this.acceptRight(true); + this.acceptRight(true, ECompleteReason.AutoResolvedNonConflictBeforeRunAI); }, 1); runWhenIdle(async () => { - this.isCancelAllAiResolveConflict = false; + this.isCancelAllAIResolveConflict = false; - const allRanges = this.resultView.getAllDiffRanges(); + const allRanges = this.mappingManagerService.getAllDiffRanges(); const conflictPointRanges = allRanges.filter( - (range) => range.isAiConflictPoint && !!range.getIntelligentStateModel().isLoading === false, + (range) => range.isConflictPoint && !!range.getIntelligentStateModel().isLoading === false, ); let resolveLen = 0; @@ -329,9 +338,9 @@ export class MergeEditorService extends Disposable { for await (const range of conflictPointRanges) { const flushRange = this.resultView.getFlushRange(range) || range; - const result = await this.actionsManager.handleAiConflictResolve(flushRange, { isRegenerate: false }); - if (this.isCancelAllAiResolveConflict) { - this.isCancelAllAiResolveConflict = false; + const result = await this.actionsManager.handleAIConflictResolve(flushRange, { isRegenerate: false }); + if (this.isCancelAllAIResolveConflict) { + this.isCancelAllAIResolveConflict = false; return; } @@ -410,6 +419,10 @@ export class MergeEditorService extends Disposable { this.incomingView.inputDiffComputingResult(turnRightMapping); this.resultView.inputDiffComputingResult(); + // resolve non conflict ranges + this.acceptLeft(true, ECompleteReason.AutoResolvedNonConflict); + this.acceptRight(true, ECompleteReason.AutoResolvedNonConflict); + this.currentView.updateDecorations().updateActions(); this.incomingView.updateDecorations().updateActions(); this.resultView.updateDecorations().updateActions(); diff --git a/packages/monaco/src/browser/contrib/merge-editor/model/document-mapping.ts b/packages/monaco/src/browser/contrib/merge-editor/model/document-mapping.ts index 47b0141dcf..b0778e2136 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/model/document-mapping.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/model/document-mapping.ts @@ -3,7 +3,6 @@ import { Disposable } from '@opensumi/ide-core-common'; import { DetailedLineRangeMapping } from '../../../../common/diff'; import { EDiffRangeTurn } from '../types'; -import { flatModified, flatOriginal } from '../utils'; import { LineRange } from './line-range'; @@ -14,11 +13,9 @@ import { LineRange } from './line-range'; * 例: 点击左侧视图的 accept 操作,导致 result 视图的文本增加了 3 行(e.g 第 3 行到第 6 行增加了文本), 那么这第 3 行之后的所有源数据的 lineRange offset 都需要增加 3 * 这样再后续处理其他的 conflict 操作时才能计算正确 * - * @param diffRangeTurn - * ORIGIN: 表示 current editor view 与 result editor view 的 lineRangeMapping 映射关系 - * MODIFIED: 表示 result editor view 与 incoming editor view 的 lineRangeMapping 映射关系 + * @param diffRangeTurn {@link EDiffRangeTurn} 用于区分当前的映射关系 */ -@Injectable({ multiple: false }) +@Injectable({ multiple: true }) export class DocumentMapping extends Disposable { public adjacentComputeRangeMap: Map = new Map(); public computeRangeMap: Map = new Map(); @@ -74,16 +71,17 @@ export class DocumentMapping extends Disposable { } public inputComputeResultRangeMapping(changes: readonly DetailedLineRangeMapping[]): void { - const [originalRange, modifiedRange] = [flatOriginal(changes), flatModified(changes)]; - - if (this.diffRangeTurn === EDiffRangeTurn.MODIFIED) { - modifiedRange.forEach((range, idx) => { - this.addRange(range, originalRange[idx]); - }); - } else if (this.diffRangeTurn === EDiffRangeTurn.ORIGIN) { - originalRange.forEach((range, idx) => { - this.addRange(range, modifiedRange[idx]); - }); + switch (this.diffRangeTurn) { + case EDiffRangeTurn.ORIGIN: + changes.forEach(({ modified, original }) => { + this.addRange(original as LineRange, modified as LineRange); + }); + break; + case EDiffRangeTurn.MODIFIED: + changes.forEach(({ modified, original }) => { + this.addRange(modified as LineRange, original as LineRange); + }); + break; } } diff --git a/packages/monaco/src/browser/contrib/merge-editor/model/inner-range.ts b/packages/monaco/src/browser/contrib/merge-editor/model/inner-range.ts index 6efb5be1af..871f572c46 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/model/inner-range.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/model/inner-range.ts @@ -1,7 +1,7 @@ import { Range as MonacoRange } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/range'; import { IPosition } from '../../../monaco-api/types'; -import { ETurnDirection, IRangeContrast, LineRangeType } from '../types'; +import { ECompleteReason, ETurnDirection, IRangeContrast, LineRangeType } from '../types'; export class InnerRange extends MonacoRange implements IRangeContrast { private _isComplete: boolean; @@ -9,6 +9,11 @@ export class InnerRange extends MonacoRange implements IRangeContrast { return this._isComplete; } + private _completeReason: ECompleteReason | undefined; + public get completeReason(): ECompleteReason | undefined { + return this._completeReason; + } + private _turnDirection: ETurnDirection; public get turnDirection(): ETurnDirection { return this._turnDirection; @@ -23,8 +28,15 @@ export class InnerRange extends MonacoRange implements IRangeContrast { return this; } - public setComplete(b: boolean): this { - this._isComplete = b; + public done(reason: ECompleteReason): this { + this._isComplete = true; + this._completeReason = reason; + return this; + } + + public cancel(): this { + this._isComplete = false; + this._completeReason = undefined; return this; } diff --git a/packages/monaco/src/browser/contrib/merge-editor/model/line-range.ts b/packages/monaco/src/browser/contrib/merge-editor/model/line-range.ts index 3110948220..adc59282b0 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/model/line-range.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/model/line-range.ts @@ -4,7 +4,7 @@ import { LineRange as MonacoLineRange } from '@opensumi/monaco-editor-core/esm/v import { Position } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/position'; import { Range as MonacoRange } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/range'; -import { ETurnDirection, IRangeContrast, LineRangeType } from '../types'; +import { ECompleteReason, ETurnDirection, IRangeContrast, LineRangeType } from '../types'; import { InnerRange } from './inner-range'; @@ -85,6 +85,9 @@ class MergeStateModel { */ private metaRanges: LineRange[] = []; + /** + * 左右的代码有重叠,有包含,有接触 + */ public get isMerge(): boolean { return this.metaRanges.length > 0; } @@ -95,9 +98,9 @@ class MergeStateModel { * 例如 * 左边 中间 右边 * ``` ``` ``` - * 1. const a = 1 const a = 2 const a = 1 - * 2. const b = 1 const b = 1 const b = 2 - * 3. const c = 1 const c = 2 const c = 1 + * 1. const a = 1 const a = 2 const a = 1 + * 2. const b = 1 const b = 1 const b = 2 + * 3. const c = 1 const c = 2 const c = 1 * ``` ``` ``` * * 其中第一行中间与两边都有 diff,但不管是接受左边的还是右边的,最终对结果的影响不变(接受左右两边都一样) @@ -163,24 +166,39 @@ export class LineRange extends MonacoLineRange implements IRangeContrast { } private _isComplete: boolean; + private _completeReason: ECompleteReason | undefined; + + /** + * 是否已经解决完成(是否合入) + */ public get isComplete(): boolean { return this._isComplete; } + public get completeReason(): ECompleteReason { + return this._completeReason!; + } + private _turnDirection: ETurnDirection; public get turnDirection(): ETurnDirection { return this._turnDirection; } + /** + * @see {@link MergeStateModel.isMerge} + */ public get isMerge(): boolean { return this.mergeStateModel.isMerge; } + /** + * @see {@link MergeStateModel.isAllowCombination} + */ public get isAllowCombination(): boolean { return this.mergeStateModel.isAllowCombination; } - public get isAiConflictPoint(): boolean { + public get isConflictPoint(): boolean { return this.isMerge && this.type === 'modify'; } @@ -195,7 +213,7 @@ export class LineRange extends MonacoLineRange implements IRangeContrast { this.intelligentStateModel = new IntelligentStateModel(); } - private setId(id: string): this { + setId(id: string): this { this._id = id; return this; } @@ -205,8 +223,18 @@ export class LineRange extends MonacoLineRange implements IRangeContrast { return this; } - public setComplete(b: boolean): this { - this._isComplete = b; + public done(reason: ECompleteReason): this { + this._isComplete = true; + this._completeReason = reason; + return this; + } + + /** + * 置为未完成状态 + */ + public cancel(): this { + this._isComplete = false; + this._completeReason = undefined; return this; } @@ -333,13 +361,20 @@ export class LineRange extends MonacoLineRange implements IRangeContrast { } private retainState(range: LineRange): LineRange { - return range + const newLineRange = range .setId(this._id) .setType(this._type) .setTurnDirection(this._turnDirection) - .setComplete(this._isComplete) .setMergeStateModel(this.mergeStateModel) .setIntelligentStateModel(this.intelligentStateModel); + + if (this._isComplete) { + newLineRange.done(this._completeReason!); + } else { + newLineRange.cancel(); + } + + return newLineRange; } public override delta(offset: number): LineRange { diff --git a/packages/monaco/src/browser/contrib/merge-editor/types.ts b/packages/monaco/src/browser/contrib/merge-editor/types.ts index 0bf3881777..2aca15ceff 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/types.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/types.ts @@ -9,13 +9,23 @@ import { LineRange } from './model/line-range'; import { BaseCodeEditor } from './view/editors/baseCodeEditor'; import styles from './view/merge-editor.module.less'; +export enum ECompleteReason { + UserManual = 'user_manual', + AIResolved = 'ai_resolved', + + AutoResolvedNonConflictBeforeRunAI = 'auto_resolved_non_conflict_before_run_ai', + AutoResolvedNonConflict = 'auto_resolved_non_conflict', +} + export interface IRangeContrast { type: LineRangeType; /** * 是否解决操作完成 */ get isComplete(): boolean; - setComplete: (b: boolean) => this; + get completeReason(): ECompleteReason | undefined; + done: (reason: ECompleteReason) => this; + cancel: () => this; /** * 表示这个 range 区域是倾向于 current editor 还是 incoming editor(如果本身就是在 current editor 则返回 current) * 在 result editor 视图里可以通过该字段来判读它是与 current editor 相比较的还是与 incoming 相比较的 diff @@ -44,7 +54,13 @@ export enum EditorViewType { } export enum EDiffRangeTurn { + /** + * 表示 current editor view 与 result editor view 的 lineRangeMapping 映射关系 + */ ORIGIN = 'origin', + /** + * 表示 result editor view 与 incoming editor view 的 lineRangeMapping 映射关系 + */ MODIFIED = 'modified', } @@ -165,6 +181,7 @@ export interface IConflictActionsEvent { range: LineRange; action: TActionsType; withViewType: EditorViewType; + reason: ECompleteReason; } /** @@ -195,4 +212,4 @@ export interface IEditorMountParameter { /** * 智能解决冲突 result 视图的 id */ -export const AiResolveConflictContentWidget = 'ai-resolve-conflict-content-widget'; +export const AIResolveConflictContentWidget = 'ai-resolve-conflict-content-widget'; diff --git a/packages/monaco/src/browser/contrib/merge-editor/utils.ts b/packages/monaco/src/browser/contrib/merge-editor/utils.ts deleted file mode 100644 index f64493a765..0000000000 --- a/packages/monaco/src/browser/contrib/merge-editor/utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DetailedLineRangeMapping } from '../../../common/diff'; - -import { InnerRange } from './model/inner-range'; -import { LineRange } from './model/line-range'; - -export const flatOriginal = (changes: readonly DetailedLineRangeMapping[]): LineRange[] => - changes.map((c) => c.original as LineRange); - -export const flatInnerOriginal = (changes: DetailedLineRangeMapping[]): InnerRange[][] => - changes - .map((c) => c.innerChanges) - .filter(Boolean) - .map((m) => m!.map((m) => m.originalRange as InnerRange)); - -export const flatModified = (changes: readonly DetailedLineRangeMapping[]): LineRange[] => - changes.map((c) => c.modified as LineRange); - -export const flatInnerModified = (changes: DetailedLineRangeMapping[]): InnerRange[][] => - changes - .map((c) => c.innerChanges) - .filter(Boolean) - .map((m) => m!.map((m) => m.modifiedRange as InnerRange)); diff --git a/packages/monaco/src/browser/contrib/merge-editor/view/actions-manager.ts b/packages/monaco/src/browser/contrib/merge-editor/view/actions-manager.ts index 9ec04c22ff..7ce6bbad87 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/view/actions-manager.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/view/actions-manager.ts @@ -15,6 +15,7 @@ import { AI_RESOLVE_ACTIONS, AI_RESOLVE_REGENERATE_ACTIONS, APPEND_ACTIONS, + ECompleteReason, ETurnDirection, IActionsProvider, IConflictActionsEvent, @@ -91,27 +92,25 @@ export class ActionsManager extends Disposable { } private pickMapping(range: LineRange): DocumentMapping | undefined { - if (range.turnDirection === ETurnDirection.BOTH) { - return; + switch (range.turnDirection) { + case ETurnDirection.BOTH: + return; + case ETurnDirection.CURRENT: + return this.mappingManagerService.documentMappingTurnLeft; + case ETurnDirection.INCOMING: + return this.mappingManagerService.documentMappingTurnRight; } - - return range.turnDirection === ETurnDirection.CURRENT - ? this.mappingManagerService.documentMappingTurnLeft - : this.mappingManagerService.documentMappingTurnRight; } private pickViewEditor(range: LineRange): BaseCodeEditor { - const { turnDirection } = range; - - if (turnDirection === ETurnDirection.INCOMING) { - return this.incomingView!; - } - - if (turnDirection === ETurnDirection.CURRENT) { - return this.currentView!; + switch (range.turnDirection) { + case ETurnDirection.INCOMING: + return this.incomingView!; + case ETurnDirection.CURRENT: + return this.currentView!; + default: + return this.resultView!; } - - return this.resultView!; } /** @@ -127,6 +126,7 @@ export class ActionsManager extends Disposable { eol: string; }, ) => TLineRangeEdit, + reason: ECompleteReason, ): void { const mapping = this.pickMapping(range); if (!mapping) { @@ -152,7 +152,7 @@ export class ActionsManager extends Disposable { }), ); - this.markComplete(range); + this.markComplete(range, reason); /** * 如果 oppositeRange 是 merge range 合成的,则需要更新 current editor 和 incoming editor 的 actions @@ -180,21 +180,19 @@ export class ActionsManager extends Disposable { return; } - if (turnDirection === ETurnDirection.CURRENT) { - this.mappingManagerService.revokeActionsTurnLeft(range); + switch (turnDirection) { + case ETurnDirection.CURRENT: + this.mappingManagerService.revokeActionsTurnLeft(range); + break; + case ETurnDirection.INCOMING: + this.mappingManagerService.revokeActionsTurnRight(range); + break; + case ETurnDirection.BOTH: + this.mappingManagerService.revokeActionsTurnLeft(range); + this.mappingManagerService.revokeActionsTurnRight(range); + break; } - if (turnDirection === ETurnDirection.INCOMING) { - this.mappingManagerService.revokeActionsTurnRight(range); - } - - if (turnDirection === ETurnDirection.BOTH) { - this.mappingManagerService.revokeActionsTurnLeft(range); - this.mappingManagerService.revokeActionsTurnRight(range); - } - - const model = viewEditor.getModel()!; - const eol = model.getEOL(); const { text } = metaData; // 为 null 则说明是删除文本 @@ -214,7 +212,7 @@ export class ActionsManager extends Disposable { /** * 接受 accept combination 时将左右代码内容交替插入中间视图 */ - private handleAcceptCombination(range: LineRange): void { + private handleAcceptCombination(range: LineRange, reason: ECompleteReason): void { const reverseLeftRange = this.mappingManagerService.documentMappingTurnLeft.reverse(range); const reverseRightRange = this.mappingManagerService.documentMappingTurnRight.reverse(range); @@ -262,8 +260,8 @@ export class ActionsManager extends Disposable { }, ]); - this.markComplete(reverseLeftRange); - this.markComplete(reverseRightRange); + this.markComplete(reverseLeftRange, reason); + this.markComplete(reverseRightRange, reason); this.resultView?.updateActions(); } @@ -271,7 +269,7 @@ export class ActionsManager extends Disposable { /** * 处理 AI 智能解决冲突时的逻辑 */ - public async handleAiConflictResolve( + public async handleAIConflictResolve( flushRange: LineRange, options: { isRegenerate: boolean; @@ -313,7 +311,7 @@ export class ActionsManager extends Disposable { this.mergeConflictReportService.reportIncrementNum(this.resultView.getUri(), 'clickNum'); - const resolveConflictResult = await this.resultView.requestAiResolveConflict( + const resolveConflictResult = await this.resultView.requestAIResolveConflict( { base: baseValue || '', current: currentValue || '', incoming: incomingValue || '' }, flushRange, !!options?.isRegenerate, @@ -366,17 +364,20 @@ export class ActionsManager extends Disposable { }; } - private markComplete(range: LineRange): void { + private markComplete(range: LineRange, reason: ECompleteReason): void { const { turnDirection } = range; - if (turnDirection === ETurnDirection.CURRENT) { - this.mappingManagerService.markCompleteTurnLeft(range); - this.currentView!.updateDecorations().updateActions(); - } - - if (turnDirection === ETurnDirection.INCOMING) { - this.mappingManagerService.markCompleteTurnRight(range); - this.incomingView!.updateDecorations().updateActions(); + switch (turnDirection) { + case ETurnDirection.CURRENT: { + this.mappingManagerService.markCompleteTurnLeft(range, reason); + this.currentView!.updateDecorations().updateActions(); + break; + } + case ETurnDirection.INCOMING: { + this.mappingManagerService.markCompleteTurnRight(range, reason); + this.incomingView!.updateDecorations().updateActions(); + break; + } } } @@ -404,58 +405,68 @@ export class ActionsManager extends Disposable { this.currentView.onDidConflictActions, this.resultView.onDidConflictActions, this.incomingView.onDidConflictActions, - )(async ({ range, action }) => { - if (action === ACCEPT_CURRENT_ACTIONS) { - this.handleAcceptChange(range, (_range, oppositeRange, { applyText }) => [ - { range: oppositeRange, text: applyText }, - ]); - } - - if (action === IGNORE_ACTIONS) { - this.markComplete(range); - this.resultView?.updateActions(); - } - - if (action === REVOKE_ACTIONS) { - this.handleAcceptRevoke(range); - } - - if (action === ACCEPT_COMBINATION_ACTIONS) { - this.handleAcceptCombination(range); - } - - /** - * range 如果是 merge range 合成的,当 accept 某一视图的代码变更时,另一边的 accpet 就变成 append 追加内容,而不是覆盖内容 - */ - if (action === APPEND_ACTIONS) { - this.handleAcceptChange(range, (_, oppositeRange, { applyText, eol }) => [ + )(async ({ range, action, reason }) => { + switch (action) { + case ACCEPT_CURRENT_ACTIONS: + this.handleAcceptChange( + range, + (_range, oppositeRange, { applyText }) => [{ range: oppositeRange, text: applyText }], + reason, + ); + break; + case IGNORE_ACTIONS: + this.markComplete(range, reason); + this.resultView?.updateActions(); + break; + case REVOKE_ACTIONS: + this.handleAcceptRevoke(range); + break; + case ACCEPT_COMBINATION_ACTIONS: + this.handleAcceptCombination(range, reason); + break; + case APPEND_ACTIONS: /** - * 在 diff 区域的最后一行追加代码内容 + * range 如果是 merge range 合成的,当 accept 某一视图的代码变更时,另一边的 accpet 就变成 append 追加内容,而不是覆盖内容 */ - { - range: LineRange.fromPositions( - oppositeRange.endLineNumberExclusive, - oppositeRange.endLineNumberExclusive, - ), - text: applyText, - }, - ]); + this.handleAcceptChange( + range, + (_, oppositeRange, { applyText, eol }) => [ + /** + * 在 diff 区域的最后一行追加代码内容 + */ + { + range: LineRange.fromPositions( + oppositeRange.endLineNumberExclusive, + oppositeRange.endLineNumberExclusive, + ), + text: applyText, + }, + ], + reason, + ); + break; + /** + * 处理 AI 智能解决冲突 + */ + case AI_RESOLVE_ACTIONS: + case AI_RESOLVE_REGENERATE_ACTIONS: + if (this.resultView) { + const flushRange = this.resultView.getFlushRange(range) || range; + await this.handleAIConflictResolve(flushRange, { + isRegenerate: action === AI_RESOLVE_REGENERATE_ACTIONS, + }); + } + break; } - - /** - * 处理 AI 智能解决冲突 - */ - if ((action === AI_RESOLVE_ACTIONS || action === AI_RESOLVE_REGENERATE_ACTIONS) && this.resultView) { - const flushRange = this.resultView.getFlushRange(range) || range; - - await this.handleAiConflictResolve(flushRange, { - isRegenerate: action === AI_RESOLVE_REGENERATE_ACTIONS, - }); + if (this.resultView) { + this.resultView.updateDecorations(); + } + if (this.currentView) { + this.currentView.launchChange(); + } + if (this.incomingView) { + this.incomingView.launchChange(); } - - this.resultView!.updateDecorations(); - this.currentView!.launchChange(); - this.incomingView!.launchChange(); }), ); } diff --git a/packages/monaco/src/browser/contrib/merge-editor/view/editors/baseCodeEditor.ts b/packages/monaco/src/browser/contrib/merge-editor/view/editors/baseCodeEditor.ts index ad1321d38f..453229f47b 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/view/editors/baseCodeEditor.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/view/editors/baseCodeEditor.ts @@ -228,6 +228,8 @@ export abstract class BaseCodeEditor extends Disposable implements IBaseCodeEdit } /** + * 接受 diff 结果,用于计算 decorations 等 + * * @param turnType: 表示 computer diff 的结果是以 origin 作为比较还是 modify 作为比较 */ public abstract inputDiffComputingResult(changes: DetailedLineRangeMapping[], turnType?: EDiffRangeTurn): void; diff --git a/packages/monaco/src/browser/contrib/merge-editor/view/editors/currentCodeEditor.ts b/packages/monaco/src/browser/contrib/merge-editor/view/editors/currentCodeEditor.ts index b121da29b3..23c06b2ed6 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/view/editors/currentCodeEditor.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/view/editors/currentCodeEditor.ts @@ -13,6 +13,7 @@ import { APPEND_ACTIONS, CONFLICT_ACTIONS_ICON, DECORATIONS_CLASSNAME, + ECompleteReason, EditorViewType, IActionsDescription, IConflictActionsEvent, @@ -81,10 +82,8 @@ export class CurrentCodeEditor extends BaseCodeEditor { } public override launchConflictActionsEvent(eventData: Omit): void { - const { range, action } = eventData; super.launchConflictActionsEvent({ - range, - action, + ...eventData, withViewType: EditorViewType.CURRENT, }); } @@ -116,6 +115,7 @@ export class CurrentCodeEditor extends BaseCodeEditor { this.launchConflictActionsEvent({ range, action: actionType, + reason: ECompleteReason.UserManual, }); } }, diff --git a/packages/monaco/src/browser/contrib/merge-editor/view/editors/incomingCodeEditor.ts b/packages/monaco/src/browser/contrib/merge-editor/view/editors/incomingCodeEditor.ts index 7ce77ae632..d237e2eae4 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/view/editors/incomingCodeEditor.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/view/editors/incomingCodeEditor.ts @@ -11,6 +11,7 @@ import { APPEND_ACTIONS, CONFLICT_ACTIONS_ICON, DECORATIONS_CLASSNAME, + ECompleteReason, EditorViewType, IActionsDescription, IConflictActionsEvent, @@ -77,10 +78,8 @@ export class IncomingCodeEditor extends BaseCodeEditor { } public override launchConflictActionsEvent(eventData: Omit): void { - const { range, action } = eventData; super.launchConflictActionsEvent({ - range, - action, + ...eventData, withViewType: EditorViewType.INCOMING, }); } @@ -92,7 +91,7 @@ export class IncomingCodeEditor extends BaseCodeEditor { provideActionsItems: this.provideActionsItems, onActionsClick: (range: LineRange, actionType: TActionsType) => { if (actionType === ACCEPT_CURRENT_ACTIONS || actionType === IGNORE_ACTIONS || actionType === APPEND_ACTIONS) { - this.launchConflictActionsEvent({ range, action: actionType }); + this.launchConflictActionsEvent({ range, action: actionType, reason: ECompleteReason.UserManual }); } }, }); diff --git a/packages/monaco/src/browser/contrib/merge-editor/view/editors/resultCodeEditor.ts b/packages/monaco/src/browser/contrib/merge-editor/view/editors/resultCodeEditor.ts index e12a92da9a..fec1ef8d23 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/view/editors/resultCodeEditor.ts +++ b/packages/monaco/src/browser/contrib/merge-editor/view/editors/resultCodeEditor.ts @@ -20,7 +20,6 @@ import { MergeConflictEditorMode, ResolveConflictRegistryToken, } from '@opensumi/ide-core-common'; -import { distinct } from '@opensumi/monaco-editor-core/esm/vs/base/common/arrays'; import { Position } from '@opensumi/monaco-editor-core/esm/vs/editor/common/core/position'; import { IModelDecorationOptions, @@ -37,6 +36,7 @@ import { Progress } from '@opensumi/monaco-editor-core/esm/vs/platform/progress/ import { editor } from '../../../../../browser/monaco-exports'; import * as monaco from '../../../../../common'; +import { MappingManagerDataStore } from '../../mapping-manager.store'; import { DocumentMapping } from '../../model/document-mapping'; import { InnerRange } from '../../model/inner-range'; import { IIntelligentState, LineRange } from '../../model/line-range'; @@ -48,6 +48,7 @@ import { AI_RESOLVE_REGENERATE_ACTIONS, CONFLICT_ACTIONS_ICON, DECORATIONS_CLASSNAME, + ECompleteReason, ETurnDirection, EditorViewType, IActionsDescription, @@ -56,62 +57,14 @@ import { REVOKE_ACTIONS, TActionsType, } from '../../types'; +import { IWidgetFactory, IWidgetPositionFactory, WidgetFactory } from '../../widget/facotry'; import { ResolveResultWidget } from '../../widget/resolve-result-widget'; import { StopWidget } from '../../widget/stop-widget'; import { BaseCodeEditor } from './baseCodeEditor'; -interface IWidgetFactory { - hideWidget(id?: string): void; - addWidget(range: LineRange): void; - hasWidget(range: LineRange): boolean; -} - -class WidgetFactory implements IWidgetFactory { - private widgetMap: Map; - - constructor( - private contentWidget: typeof ResolveResultWidget, - private editor: BaseCodeEditor, - private injector: Injector, - ) { - this.widgetMap = new Map(); - } - - hasWidget(range: LineRange): boolean { - return this.widgetMap.get(range.id) !== undefined; - } - - public hideWidget(id?: string): void { - if (id) { - const widget = this.widgetMap.get(id); - if (widget) { - widget.hide(); - this.widgetMap.delete(id); - } - return; - } - - this.widgetMap.forEach((widget) => { - widget.hide(); - }); - this.widgetMap.clear(); - } - - public addWidget(range: LineRange): void { - const id = range.id; - if (this.widgetMap.has(id)) { - return; - } - - const position = new Position(Math.max(range.startLineNumber, range.endLineNumberExclusive - 1), 1); - - const widget = this.injector.get(this.contentWidget, [this.editor, range]); - widget.show({ position }); - - this.widgetMap.set(id, widget); - } -} +const positionFactory: IWidgetPositionFactory = (range) => + new Position(Math.max(range.startLineNumber, range.endLineNumberExclusive - 1), 1); @Injectable({ multiple: false }) export class ResultCodeEditor extends BaseCodeEditor { @@ -121,6 +74,9 @@ export class ResultCodeEditor extends BaseCodeEditor { @Autowired(MergeConflictReportService) private readonly mergeConflictReportService: MergeConflictReportService; + @Autowired(MappingManagerDataStore) + private readonly dataStore: MappingManagerDataStore; + private readonly _onDidChangeContent = new Emitter(); public readonly onDidChangeContent: Event = this._onDidChangeContent.event; @@ -134,33 +90,28 @@ export class ResultCodeEditor extends BaseCodeEditor { private timeMachineDocument: TimeMachineDocument; private resolveResultWidgetManager: IWidgetFactory; private stopWidgetManager: IWidgetFactory; - private isFirstInputComputeDiff = true; private cancelIndicatorMap: Map = new Map(); protected aiBackService: IAIBackService; protected resolveConflictRegistry: IInternalResolveConflictRegistry; + protected supportAIConflictResolve = false; + /** @deprecated */ public documentMapping: DocumentMapping; - private get documentMappingTurnLeft(): DocumentMapping { - return this.mappingManagerService.documentMappingTurnLeft; - } - private get documentMappingTurnRight(): DocumentMapping { - return this.mappingManagerService.documentMappingTurnRight; - } - constructor(container: HTMLDivElement, monacoService: MonacoService, injector: Injector) { super(container, monacoService, injector); this.timeMachineDocument = injector.get(TimeMachineDocument, []); this.initListenEvent(); - this.resolveResultWidgetManager = new WidgetFactory(ResolveResultWidget, this, this.injector); - this.stopWidgetManager = new WidgetFactory(StopWidget, this, this.injector); + this.resolveResultWidgetManager = new WidgetFactory(ResolveResultWidget, this, this.injector, positionFactory); + this.stopWidgetManager = new WidgetFactory(StopWidget, this, this.injector, positionFactory); if (this.aiNativeConfigService.capabilities.supportsConflictResolve) { this.aiBackService = injector.get(AIBackSerivcePath); this.resolveConflictRegistry = injector.get(ResolveConflictRegistryToken); + this.supportAIConflictResolve = true; } } @@ -195,7 +146,7 @@ export class ResultCodeEditor extends BaseCodeEditor { }; } - public async requestAiResolveConflict( + public async requestAIResolveConflict( metadata: IConflictContentMetadata, range: LineRange, isRegenerate = false, @@ -257,7 +208,7 @@ export class ResultCodeEditor extends BaseCodeEditor { */ public getFlushRange(range: LineRange): LineRange | undefined { const id = range.id; - const allRanges = this.getAllDiffRanges(); + const allRanges = this.mappingManagerService.getAllDiffRanges(); return allRanges.find((range) => range.id === id); } @@ -271,7 +222,7 @@ export class ResultCodeEditor extends BaseCodeEditor { // monaco 内部的 format 无法通过 command 或 api 来指定 range,所以这里需要像这样调用,手动传入 range await instaService.invokeFunction( formatDocumentRangesWithSelectedProvider, - this.editor as any, + this.getModel()!, range.toInclusiveRange() as monaco.Range, FormattingMode.Explicit, Progress.None, @@ -296,7 +247,7 @@ export class ResultCodeEditor extends BaseCodeEditor { }), ); - if (this.aiNativeConfigService.capabilities.supportsConflictResolve) { + if (this.supportAIConflictResolve) { this.addDispose( this.editor.onMouseMove( debounce((event: monaco.editor.IEditorMouseEvent) => { @@ -312,7 +263,7 @@ export class ResultCodeEditor extends BaseCodeEditor { return; } - const allRanges = this.getAllDiffRanges(); + const allRanges = this.mappingManagerService.getAllDiffRanges(); const toLineRange = LineRange.fromPositions(mousePosition.lineNumber); const isTouches = allRanges.some((range) => range.isTouches(toLineRange)); @@ -387,24 +338,24 @@ export class ResultCodeEditor extends BaseCodeEditor { this.mappingManagerService.findNextLineRanges(toLineRange); if (includeLeftRange) { - this.documentMappingTurnLeft.deltaEndAdjacentQueue(includeLeftRange, offset); + this.mappingManagerService.documentMappingTurnLeft.deltaEndAdjacentQueue(includeLeftRange, offset); } else if (touchTurnLeftRange) { - this.documentMappingTurnLeft.deltaEndAdjacentQueue(touchTurnLeftRange, offset); + this.mappingManagerService.documentMappingTurnLeft.deltaEndAdjacentQueue(touchTurnLeftRange, offset); } else if (nextTurnLeftRange) { - const reverse = this.documentMappingTurnLeft.reverse(nextTurnLeftRange); + const reverse = this.mappingManagerService.documentMappingTurnLeft.reverse(nextTurnLeftRange); if (reverse) { - this.documentMappingTurnLeft.deltaAdjacentQueueAfter(reverse, offset, true); + this.mappingManagerService.documentMappingTurnLeft.deltaAdjacentQueueAfter(reverse, offset, true); } } if (includeRightRange) { - this.documentMappingTurnRight.deltaEndAdjacentQueue(includeRightRange, offset); + this.mappingManagerService.documentMappingTurnRight.deltaEndAdjacentQueue(includeRightRange, offset); } else if (touchTurnRightRange) { - this.documentMappingTurnRight.deltaEndAdjacentQueue(touchTurnRightRange, offset); + this.mappingManagerService.documentMappingTurnRight.deltaEndAdjacentQueue(touchTurnRightRange, offset); } else if (nextTurnRightRange) { - const reverse = this.documentMappingTurnRight.reverse(nextTurnRightRange); + const reverse = this.mappingManagerService.documentMappingTurnRight.reverse(nextTurnRightRange); if (reverse) { - this.documentMappingTurnRight.deltaAdjacentQueueAfter(reverse, offset, true); + this.mappingManagerService.documentMappingTurnRight.deltaAdjacentQueueAfter(reverse, offset, true); } } }); @@ -415,27 +366,17 @@ export class ResultCodeEditor extends BaseCodeEditor { ); } - public getAllDiffRanges(): LineRange[] { - // 去重相同 id 或位置一样的 line range - return distinct( - this.documentMappingTurnLeft.getModifiedRange().concat(this.documentMappingTurnRight.getOriginalRange()), - (range) => range.id, - ); - } - protected provideActionsItems(ranges?: LineRange[]): IActionsDescription[] { if (!Array.isArray(ranges)) { return []; } const renderIconClassName = (range: LineRange) => { - const isAiConflictResolve = this.aiNativeConfigService?.capabilities?.supportsConflictResolve; - if (range.isComplete) { return CONFLICT_ACTIONS_ICON.REVOKE; } - if (isAiConflictResolve && range.type === 'modify') { + if (this.supportAIConflictResolve && range.type === 'modify') { const aiModel = range.getIntelligentStateModel(); if (aiModel.isLoading) { @@ -481,7 +422,9 @@ export class ResultCodeEditor extends BaseCodeEditor { let slow = 0; let mergeRange: LineRange | undefined; - /** Two Pointers 算法 */ + diffRanges.sort((a, b) => a.startLineNumber - b.startLineNumber); + + /** Two Pointers 算法, 快慢指针 */ for (let fast = 0; fast < length; fast++) { const slowRange = diffRanges[slow]; const fastRange = diffRanges[fast]; @@ -495,12 +438,12 @@ export class ResultCodeEditor extends BaseCodeEditor { if (mergeRange.isTouches(fastRange)) { mergeRange = mergeRange.merge(fastRange); result.set(slow, { rawRanges: (result.get(slow)?.rawRanges || []).concat(fastRange), mergeRange }); - continue; } else { // 重置 mergeRange = undefined; slow = fast; } + continue; } else if (slowRange.isTouches(fastRange)) { // 如果 range 有接触,则需要合并在一起,同时 slow 指针位置不变 mergeRange = slowRange.merge(fastRange); @@ -521,7 +464,9 @@ export class ResultCodeEditor extends BaseCodeEditor { }[], ): void { const pickMapping = (range: LineRange) => - range.turnDirection === ETurnDirection.CURRENT ? this.documentMappingTurnLeft : this.documentMappingTurnRight; + range.turnDirection === ETurnDirection.CURRENT + ? this.mappingManagerService.documentMappingTurnLeft + : this.mappingManagerService.documentMappingTurnRight; for (let { rawRanges, mergeRange } of needMergeRanges) { // 需要合并的 range 一定多于两个 @@ -546,7 +491,7 @@ export class ResultCodeEditor extends BaseCodeEditor { * 2. { startLine: 20,endLine: 30 } // 方向向左 * 3. { startLine: 30,endLine: 40 } // 方向向右 * - * 首先这三者的 turn directio 方向一定是左右交替的 + * 首先这三者的 turn direction 方向一定是左右交替的 * 那么第二个 lineRange 的对位关系 oppositeLineRange 的起点 startLine 就一定会比第一个 lineRange 的起点 startLine 少一个高度 * 这个高度的差距会影响后续所有的 conflict action 操作(因为缺失的这部分高度会导致 accept 操作后的代码内容丢失) * @@ -604,41 +549,107 @@ export class ResultCodeEditor extends BaseCodeEditor { if (mergeRangeTurnLeft) { const newLineRange = mergeRangeTurnLeft.setTurnDirection(ETurnDirection.CURRENT).setType('modify'); - this.documentMappingTurnLeft.addRange(newLineRange, mergeRange); + this.mappingManagerService.documentMappingTurnLeft.addRange(newLineRange, mergeRange); } if (mergeRangeTurnRight) { const newLineRange = mergeRangeTurnRight.setTurnDirection(ETurnDirection.INCOMING).setType('modify'); - this.documentMappingTurnRight.addRange(newLineRange, mergeRange); + this.mappingManagerService.documentMappingTurnRight.addRange(newLineRange, mergeRange); } } } - protected override prepareRenderDecorations(): [LineRange[], InnerRange[][]] { - const diffRanges: LineRange[] = this.getAllDiffRanges().sort((a, b) => a.startLineNumber - b.startLineNumber); - const innerChangesResult: InnerRange[][] = []; - - let maybeNeedMergeRanges: { - rawRanges: LineRange[]; - mergeRange: LineRange; - }[] = []; + /** + * 用来标记是否进行过初次 diff 计算,如果进行过,则直接返回所有 diff + */ + private isFirstInputComputeDiff = true; + protected getDiffRangesAfterDistill(): LineRange[] { + let diffRanges = this.mappingManagerService.getAllDiffRanges(); if (this.isFirstInputComputeDiff) { - maybeNeedMergeRanges = this.distillNeedMergeRanges(diffRanges); + const maybeNeedMergeRanges = this.distillNeedMergeRanges(diffRanges); this.handleNeedMergeRanges(maybeNeedMergeRanges); + // 数据源 document mapping 的对应关系已经被改变,需要刷新一次 + diffRanges = this.mappingManagerService.getAllDiffRanges(); this.isFirstInputComputeDiff = false; } - /** - * 如果 maybeNeedMergeRanges 大于 0,说明数据源 document mapping 的对应关系被改变 - * 则需要重新获取一次 - */ - const changesResult: LineRange[] = maybeNeedMergeRanges.length > 0 ? this.getAllDiffRanges() : diffRanges; + return diffRanges; + } + + protected override prepareRenderDecorations(): [LineRange[], InnerRange[][]] { + const innerChangesResult: InnerRange[][] = []; + + const changesResult = this.getDiffRangesAfterDistill(); + + let conflictsTotal = 0; + let nonConflictsUnresolved = 0; + let conflictsUserSolved = 0; + let autoResolvedNonConflicts = 0; + let autoResolvedNonConflictsLeft = 0; + let autoResolvedNonConflictsRight = 0; + let autoResolvedNonConflictsBoth = 0; + let userManualResolveNonConflicts = false; + + changesResult.forEach((range) => { + if (range.isComplete) { + switch (range.completeReason) { + case ECompleteReason.AIResolved: + // @ts-expect-error: need fallthrough + case ECompleteReason.UserManual: + if (range.isConflictPoint) { + // user manual resolved conflicts + conflictsTotal++; + conflictsUserSolved++; + break; + } + // it means user manual resolved non-conflicts + // we need to record this situation, and show in UI + userManualResolveNonConflicts = true; + case ECompleteReason.AutoResolvedNonConflictBeforeRunAI: + case ECompleteReason.AutoResolvedNonConflict: + // auto resolved non-conflicts + autoResolvedNonConflicts++; + switch (range.turnDirection) { + case ETurnDirection.CURRENT: + autoResolvedNonConflictsLeft++; + break; + case ETurnDirection.INCOMING: + autoResolvedNonConflictsRight++; + break; + case ETurnDirection.BOTH: + autoResolvedNonConflictsBoth++; + break; + } + break; + } + } else { + // unresolved conflicts + if (range.isConflictPoint) { + conflictsTotal++; + } else { + // unresolved non-conflicts + nonConflictsUnresolved++; + } + } + }); + + this.dataStore.updateConflictsCount({ + total: conflictsTotal, + resolved: conflictsUserSolved, + nonConflicts: nonConflictsUnresolved, + }); + this.dataStore.updateNonConflictingChangesResolvedCount({ + total: autoResolvedNonConflicts, + left: autoResolvedNonConflictsLeft, + right: autoResolvedNonConflictsRight, + both: autoResolvedNonConflictsBoth, + userManualResolveNonConflicts, + }); - const isAiConflictResolve = this.aiNativeConfigService?.capabilities?.supportsConflictResolve; - if (isAiConflictResolve) { + if (this.supportAIConflictResolve) { changesResult - .filter((range) => range.isAiConflictPoint) + .filter((range) => range.isConflictPoint) .forEach((range) => { const model = range.getIntelligentStateModel(); @@ -649,6 +660,7 @@ export class ResultCodeEditor extends BaseCodeEditor { } }); } + return [changesResult, innerChangesResult]; } @@ -656,11 +668,10 @@ export class ResultCodeEditor extends BaseCodeEditor { preDecorations: IModelDecorationOptions, range: LineRange, ): Omit { - const isAiComplete = () => { - const isAiConflictResolve = this.aiNativeConfigService?.capabilities?.supportsConflictResolve; + const isAIComplete = () => { const intelligentModel = range.getIntelligentStateModel(); - if (isAiConflictResolve && intelligentModel.isComplete && !intelligentModel.isLoading) { + if (this.supportAIConflictResolve && intelligentModel.isComplete && !intelligentModel.isLoading) { return true; } @@ -671,14 +682,14 @@ export class ResultCodeEditor extends BaseCodeEditor { linesDecorationsClassName: DECORATIONS_CLASSNAME.combine( preDecorations.className || '', DECORATIONS_CLASSNAME.stretch_right, - isAiComplete() ? DECORATIONS_CLASSNAME.ai_resolve_complete_lines_decorations : '', + isAIComplete() ? DECORATIONS_CLASSNAME.ai_resolve_complete_lines_decorations : '', range.turnDirection === ETurnDirection.CURRENT || range.turnDirection === ETurnDirection.BOTH ? DECORATIONS_CLASSNAME.stretch_left : '', ), className: DECORATIONS_CLASSNAME.combine( preDecorations.className || '', - isAiComplete() ? DECORATIONS_CLASSNAME.ai_resolve_complete : '', + isAIComplete() ? DECORATIONS_CLASSNAME.ai_resolve_complete : '', range.turnDirection === ETurnDirection.CURRENT ? DECORATIONS_CLASSNAME.stretch_left : DECORATIONS_CLASSNAME.combine(DECORATIONS_CLASSNAME.stretch_left, DECORATIONS_CLASSNAME.stretch_right), @@ -695,7 +706,7 @@ export class ResultCodeEditor extends BaseCodeEditor { } public override updateActions(): this { - this.conflictActions.updateActions(this.provideActionsItems(this.getAllDiffRanges())); + this.conflictActions.updateActions(this.provideActionsItems(this.mappingManagerService.getAllDiffRanges())); return this; } @@ -704,7 +715,7 @@ export class ResultCodeEditor extends BaseCodeEditor { } public completeSituation(): { completeCount: number; shouldCount: number } { - const allRanges = this.getAllDiffRanges(); + const allRanges = this.mappingManagerService.getAllDiffRanges(); let completeCount = 0; for (const range of allRanges) { @@ -722,10 +733,8 @@ export class ResultCodeEditor extends BaseCodeEditor { } public override launchConflictActionsEvent(eventData: Omit): void { - const { range, action } = eventData; super.launchConflictActionsEvent({ - range, - action, + ...eventData, withViewType: EditorViewType.RESULT, }); } @@ -735,7 +744,7 @@ export class ResultCodeEditor extends BaseCodeEditor { */ public inputDiffComputingResult(): void { this.updateDecorations(); - const diffRanges = this.getAllDiffRanges(); + const diffRanges = this.mappingManagerService.getAllDiffRanges(); this.registerActionsProvider({ provideActionsItems: () => this.provideActionsItems(diffRanges), @@ -749,6 +758,7 @@ export class ResultCodeEditor extends BaseCodeEditor { this.launchConflictActionsEvent({ range, action: actionType, + reason: ECompleteReason.UserManual, }); } }, @@ -762,7 +772,7 @@ export class ResultCodeEditor extends BaseCodeEditor { }); runWhenIdle(() => { - const aiConflictNum = diffRanges.reduce((pre, cur) => (cur.isAiConflictPoint ? pre + 1 : pre), 0); + const aiConflictNum = diffRanges.reduce((pre, cur) => (cur.isConflictPoint ? pre + 1 : pre), 0); this.mergeConflictReportService.record(this.getUri(), { conflictPointNum: aiConflictNum, editorMode: MergeConflictEditorMode['3way'], diff --git a/packages/monaco/src/browser/contrib/merge-editor/view/grid.tsx b/packages/monaco/src/browser/contrib/merge-editor/view/grid.tsx index 6394b98121..b7a635a121 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/view/grid.tsx +++ b/packages/monaco/src/browser/contrib/merge-editor/view/grid.tsx @@ -1,10 +1,13 @@ +import { observer } from 'mobx-react-lite'; import React, { useCallback, useEffect, useState } from 'react'; import { AINativeConfigService, CommandService, EDITOR_COMMANDS, + ILogger, URI, + formatLocalize, localize, runWhenIdle, useInjectable, @@ -17,10 +20,12 @@ import { IOpenMergeEditorArgs, MergeEditorInputData, } from '@opensumi/ide-core-browser/lib/monaco/merge-editor-widget'; +import { MergeConflictCommands } from '@opensumi/ide-core-common/lib/commands/git'; import { IWorkspaceService } from '@opensumi/ide-workspace'; +import { MappingManagerDataStore } from '../mapping-manager.store'; import { MergeEditorService } from '../merge-editor.service'; -import { EditorViewType } from '../types'; +import { ECompleteReason, EditorViewType } from '../types'; import styles from './merge-editor.module.less'; import { MiniMap } from './mini-map'; @@ -100,69 +105,166 @@ const TitleHead: React.FC<{ contrastType: EditorViewType }> = ({ contrastType }) ); }; -const MergeActions: React.FC = () => { +const MergeActions: React.FC = observer(() => { const aiNativeConfigService = useInjectable(AINativeConfigService); const mergeEditorService = useInjectable(MergeEditorService); const commandService = useInjectable(CommandService); - const [isAiResolving, setIsAiResolving] = useState(false); + const logger = useInjectable(ILogger); + const dataStore = useInjectable(MappingManagerDataStore); + const [isAIResolving, setIsAIResolving] = useState(false); - const isSupportAiResolve = useCallback( + const isSupportAIResolve = useCallback( () => aiNativeConfigService.capabilities.supportsConflictResolve, [aiNativeConfigService], ); useEffect(() => { const dispose = mergeEditorService.onHasIntelligentLoadingChange((isLoading) => { - setIsAiResolving(isLoading); + setIsAIResolving(isLoading); }); return () => dispose.dispose(); }, [mergeEditorService]); - const handleApply = useCallback(() => { - mergeEditorService.accept(); + const [applyLoading, setApplyLoading] = useState(false); + + const handleApply = useCallback(async () => { + setApplyLoading(true); + try { + const result = await mergeEditorService.accept(); + if (result) { + } + } catch (e) { + logger.error(e); + } finally { + setApplyLoading(false); + } }, [mergeEditorService]); const handleAcceptLeft = useCallback(() => { - mergeEditorService.acceptLeft(); + mergeEditorService.acceptLeft(false, ECompleteReason.UserManual); }, [mergeEditorService]); const handleAcceptRight = useCallback(() => { - mergeEditorService.acceptRight(); + mergeEditorService.acceptRight(false, ECompleteReason.UserManual); }, [mergeEditorService]); const handleOpenTradition = useCallback(() => { - const uri = mergeEditorService.getResultEditor()?.getModel()?.uri; - if (uri) { - const fileUri = uri.with({ scheme: 'file', path: uri.path, query: '' }); - commandService.executeCommand(EDITOR_COMMANDS.API_OPEN_EDITOR_COMMAND_ID, fileUri); + let uri = mergeEditorService.getCurrentEditor()?.getModel()?.uri; + if (!uri) { + return; + } + + if (uri.scheme === 'git') { + // replace git:// with file:// + uri = uri.with({ + scheme: 'file', + path: uri.path, + query: '', + }); } + + if (uri.scheme !== 'file') { + // ignore other scheme + logger.warn('Unsupported scheme', uri.scheme); + return; + } + + commandService.executeCommand(EDITOR_COMMANDS.API_OPEN_EDITOR_COMMAND_ID, uri); }, [mergeEditorService]); const handleReset = useCallback(async () => { - await mergeEditorService.stopAllAiResolveConflict(); + await mergeEditorService.stopAllAIResolveConflict(); runWhenIdle(() => { commandService.executeCommand(EDITOR_COMMANDS.MERGEEDITOR_RESET.id); }); }, [mergeEditorService]); const handleAIResolve = useCallback(() => { - if (isAiResolving) { - mergeEditorService.stopAllAiResolveConflict(); + if (isAIResolving) { + mergeEditorService.stopAllAIResolveConflict(); } else { - mergeEditorService.handleAiResolveConflict(); + mergeEditorService.handleAIResolveConflict(); } - }, [mergeEditorService, isAiResolving]); + }, [mergeEditorService, isAIResolving]); + + const handlePrev = useCallback(() => { + commandService.tryExecuteCommand(MergeConflictCommands.Previous); + }, []); + + const handleNext = useCallback(() => { + commandService.tryExecuteCommand(MergeConflictCommands.Next); + }, []); + + const conflictsCount = dataStore.conflictsCount; + const nonConflictingChangesResolvedCount = dataStore.nonConflictingChangesResolvedCount; + + const conflictsAllResolved = conflictsCount.lefted === 0 && conflictsCount.resolved === conflictsCount.total; + const conflictsProgressHint = conflictsAllResolved + ? localize('merge-conflicts.conflicts.all-resolved') + : formatLocalize('merge-conflicts.conflicts.partial-resolved', conflictsCount.resolved, conflictsCount.lefted); + + let nonConflictHint = localize('merge-conflicts.merge.type.auto'); + if (nonConflictingChangesResolvedCount.userManualResolveNonConflicts) { + nonConflictHint = localize('merge-conflicts.merge.type.manual'); + } + + const nonConflictHintInfos = [] as string[]; + if (nonConflictingChangesResolvedCount.total > 0) { + nonConflictHintInfos.push( + formatLocalize( + 'merge-conflicts.non-conflicts.progress', + nonConflictingChangesResolvedCount.total, + nonConflictHint, + ), + ); + + const branchInfos = [] as string[]; + + if (nonConflictingChangesResolvedCount.left > 0) { + branchInfos.push( + formatLocalize('merge-conflicts.non-conflicts.from.left', nonConflictingChangesResolvedCount.left), + ); + } + if (nonConflictingChangesResolvedCount.right > 0) { + branchInfos.push( + formatLocalize('merge-conflicts.non-conflicts.from.right', nonConflictingChangesResolvedCount.right), + ); + } + if (nonConflictingChangesResolvedCount.both > 0) { + branchInfos.push( + formatLocalize('merge-conflicts.non-conflicts.from.base', nonConflictingChangesResolvedCount.both), + ); + } + + if (branchInfos.length > 0) { + const branchInfoString = branchInfos.join(';'); + nonConflictHintInfos.push(` (${branchInfoString})`); + } + } + + const nonConflictHintString = nonConflictHintInfos.join(''); + + const mergeInfo = [ + formatLocalize('merge-conflicts.conflicts.summary', conflictsCount.total, conflictsProgressHint), + conflictsCount.nonConflicts > 0 + ? formatLocalize('merge-conflicts.non-conflicts.summary', conflictsCount.nonConflicts) + : '', + nonConflictHintString, + ] + .filter(Boolean) + .join(' | '); return (
+
{mergeInfo}
-
- - @@ -170,52 +272,71 @@ const MergeActions: React.FC = () => { +
+ + +
+ + + - - {isSupportAiResolve() && ( + {isSupportAIResolve() && ( )}
); -}; +}); export const Grid = () => { const mergeEditorService = useInjectable(MergeEditorService); @@ -230,6 +351,7 @@ export const Grid = () => { resultEditorContainer.current, incomingEditorContainer.current, ]; + if (current && result && incoming) { mergeEditorService.instantiationCodeEditor(current, result, incoming); } @@ -259,13 +381,9 @@ export const Grid = () => {
- +
- +
diff --git a/packages/monaco/src/browser/contrib/merge-editor/view/merge-editor.module.less b/packages/monaco/src/browser/contrib/merge-editor/view/merge-editor.module.less index 35993c9547..1e65861ebb 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/view/merge-editor.module.less +++ b/packages/monaco/src/browser/contrib/merge-editor/view/merge-editor.module.less @@ -29,19 +29,38 @@ left: 0; background: var(--kt-panelTab-activeBackground); box-shadow: inset 1px 1px 3px 0px var(--kt-panelTab-border); - padding: 16px 28px; + padding: 12px; // minimap's z-index is 5 z-index: 6; + display: flex; + flex-direction: column; + + .action_category { + display: flex; + flex-direction: row; + } + + .merge_info { + display: flex; + width: 100%; + } .container_box { display: flex; float: right; white-space: nowrap; + width: 100%; + justify-content: flex-end; + white-space: nowrap; + padding-top: 6px; + padding-right: 20px; .merge_conflict_bottom_btn { border: 1px solid var(--kt-button-disableForeground); border-radius: 8px; background: var(--editor-background); + color: var(--editor-foreground); + margin: 0 4px; cursor: pointer; diff --git a/packages/monaco/src/browser/contrib/merge-editor/widget/facotry.ts b/packages/monaco/src/browser/contrib/merge-editor/widget/facotry.ts new file mode 100644 index 0000000000..09acdea23f --- /dev/null +++ b/packages/monaco/src/browser/contrib/merge-editor/widget/facotry.ts @@ -0,0 +1,64 @@ +import { Injector } from '@opensumi/di'; +import { ConstructorOf, Position } from '@opensumi/ide-core-browser'; + +import { LineRange } from '../model/line-range'; + +import { ResolveResultWidget } from './resolve-result-widget'; +import { IMergeEditorShape } from './types'; + +export interface IWidgetFactory { + hideWidget(id?: string): void; + addWidget(range: LineRange, ...args: any[]): void; + hasWidget(range: LineRange): boolean; +} + +export type IWidgetPositionFactory = (range: LineRange) => Position; + +export const defaultPositionFactory: IWidgetPositionFactory = (range) => new Position(range.endLineNumberExclusive, 1); + +export class WidgetFactory implements IWidgetFactory { + private widgetMap: Map; + + constructor( + private contentWidget: ConstructorOf, + private editor: IMergeEditorShape, + private injector: Injector, + protected positionFactory = defaultPositionFactory, + ) { + this.widgetMap = new Map(); + } + + hasWidget(range: LineRange): boolean { + return this.widgetMap.get(range.id) !== undefined; + } + + public hideWidget(id?: string): void { + if (id) { + const widget = this.widgetMap.get(id); + if (widget) { + widget.hide(); + this.widgetMap.delete(id); + } + return; + } + + this.widgetMap.forEach((widget) => { + widget.hide(); + }); + this.widgetMap.clear(); + } + + public addWidget(range: LineRange, ...args: any[]): void { + const id = range.id; + if (this.widgetMap.has(id)) { + return; + } + + const position = this.positionFactory(range); + + const widget = this.injector.get(this.contentWidget, [id, this.editor, range, ...args]); + widget.show({ position }); + + this.widgetMap.set(id, widget); + } +} diff --git a/packages/monaco/src/browser/contrib/merge-editor/widget/resolve-result-widget.tsx b/packages/monaco/src/browser/contrib/merge-editor/widget/resolve-result-widget.tsx index cff750e27f..c277d47fb5 100644 --- a/packages/monaco/src/browser/contrib/merge-editor/widget/resolve-result-widget.tsx +++ b/packages/monaco/src/browser/contrib/merge-editor/widget/resolve-result-widget.tsx @@ -5,44 +5,56 @@ import { Button, MessageType } from '@opensumi/ide-components'; import { DialogContent, Popover, PopoverPosition } from '@opensumi/ide-core-browser/lib/components'; import { AIInlineResult } from '@opensumi/ide-core-browser/lib/components/ai-native'; import { ContentWidgetContainerPanel } from '@opensumi/ide-core-browser/lib/components/ai-native/content-widget/containerPanel'; -import { IAiInlineResultIconItemsProps } from '@opensumi/ide-core-browser/lib/components/ai-native/inline-chat/result'; +import { IAIInlineResultIconItemsProps } from '@opensumi/ide-core-browser/lib/components/ai-native/inline-chat/result'; import { localize, uuid } from '@opensumi/ide-core-common'; import { ReactInlineContentWidget } from '../../../ai-native/BaseInlineContentWidget'; import { LineRange } from '../model/line-range'; -import { AI_RESOLVE_REGENERATE_ACTIONS, AiResolveConflictContentWidget, REVOKE_ACTIONS } from '../types'; -import { ResultCodeEditor } from '../view/editors/resultCodeEditor'; - -interface IWrapperAiInlineResultProps { - iconItems: IAiInlineResultIconItemsProps[]; +import { + AIResolveConflictContentWidget, + AI_RESOLVE_REGENERATE_ACTIONS, + ECompleteReason, + REVOKE_ACTIONS, +} from '../types'; + +import { IMergeEditorShape } from './types'; + +interface IWrapperAIInlineResultProps { + id: string; + iconItems: IAIInlineResultIconItemsProps[]; isRenderThumbs: boolean; - codeEditor: ResultCodeEditor; + codeEditor: IMergeEditorShape; range: LineRange; closeClick?: () => void; isRenderClose?: boolean; + /** + * 不展示 popover 确认框,用户点击后直接执行 re-generate + */ disablePopover?: boolean; } -export const WapperAiInlineResult = (props: IWrapperAiInlineResultProps) => { - const { iconItems, isRenderThumbs, codeEditor, range, disablePopover = false } = props; +export const WapperAIInlineResult = (props: IWrapperAIInlineResultProps) => { + const { iconItems, isRenderThumbs, codeEditor, range, id, disablePopover = false } = props; const [isVisiablePopover, setIsVisiablePopover] = React.useState(false); const uid = useMemo(() => uuid(4), []); - const onCancel = useCallback( - (event) => { - setIsVisiablePopover(false); + const hidePopover = useCallback( + (event: React.MouseEvent) => { event.stopPropagation(); event.preventDefault(); + + setIsVisiablePopover(false); }, [isVisiablePopover], ); const onOk = useCallback( - (event) => { - onCancel(event); - execGenerate(); + (event: React.MouseEvent) => { event.stopPropagation(); event.preventDefault(); + + hidePopover(event); + execGenerate(); }, [isVisiablePopover], ); @@ -51,9 +63,10 @@ export const WapperAiInlineResult = (props: IWrapperAiInlineResultProps) => { codeEditor.launchConflictActionsEvent({ range, action: AI_RESOLVE_REGENERATE_ACTIONS, + reason: ECompleteReason.UserManual, }); - codeEditor.hideResolveResultWidget(); - }, [range, codeEditor]); + codeEditor.hideResolveResultWidget(id); + }, [range, codeEditor, id]); const popoverContent = useMemo( () => ( @@ -61,7 +74,7 @@ export const WapperAiInlineResult = (props: IWrapperAiInlineResultProps) => { + ,