diff --git a/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts b/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts index 2ed0c779bacef..77e2a6a19d1cf 100644 --- a/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts +++ b/packages/ai-chat-ui/src/browser/ai-chat-ui-frontend-module.ts @@ -18,7 +18,6 @@ import { bindContributionProvider, CommandContribution, MenuContribution } from import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } from '@theia/core/lib/browser'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { ContainerModule, interfaces } from '@theia/core/shared/inversify'; -import { EditorManager } from '@theia/editor/lib/browser'; import { AIChatContribution } from './ai-chat-ui-contribution'; import { AIChatInputConfiguration, AIChatInputWidget } from './chat-input-widget'; import { ChatNodeToolbarActionContribution } from './chat-node-toolbar-action-contribution'; @@ -35,19 +34,17 @@ import { ToolCallPartRenderer, } from './chat-response-renderer'; import { - AIEditorManager, - AIEditorSelectionResolver, GitHubSelectionResolver, TextFragmentSelectionResolver, TypeDocSymbolSelectionResolver, -} from './chat-response-renderer/ai-editor-manager'; +} from './chat-response-renderer/ai-selection-resolver'; import { createChatViewTreeWidget } from './chat-tree-view'; import { ChatViewTreeWidget } from './chat-tree-view/chat-view-tree-widget'; import { ChatViewMenuContribution } from './chat-view-contribution'; import { ChatViewLanguageContribution } from './chat-view-language-contribution'; import { ChatViewWidget } from './chat-view-widget'; import { ChatViewWidgetToolbarContribution } from './chat-view-widget-toolbar-contribution'; -import { EditorPreviewManager } from '@theia/editor-preview/lib/browser/editor-preview-manager'; +import { EditorSelectionResolver } from '@theia/editor-preview/lib/browser/editor-preview-manager'; import { QuestionPartRenderer } from './chat-response-renderer/question-part-renderer'; import '../../src/browser/style/index.css'; @@ -94,14 +91,9 @@ export default new ContainerModule((bind, _unbind, _isBound, rebind) => { bind(InsertCodeAtCursorButtonAction).toSelf().inSingletonScope(); bind(CodePartRendererAction).toService(InsertCodeAtCursorButtonAction); - bind(AIEditorManager).toSelf().inSingletonScope(); - rebind(EditorManager).toService(AIEditorManager); - rebind(EditorPreviewManager).toService(AIEditorManager); - - bindContributionProvider(bind, AIEditorSelectionResolver); - bind(AIEditorSelectionResolver).to(GitHubSelectionResolver).inSingletonScope(); - bind(AIEditorSelectionResolver).to(TypeDocSymbolSelectionResolver).inSingletonScope(); - bind(AIEditorSelectionResolver).to(TextFragmentSelectionResolver).inSingletonScope(); + bind(EditorSelectionResolver).to(GitHubSelectionResolver).inSingletonScope(); + bind(EditorSelectionResolver).to(TypeDocSymbolSelectionResolver).inSingletonScope(); + bind(EditorSelectionResolver).to(TextFragmentSelectionResolver).inSingletonScope(); bind(ChatViewWidgetToolbarContribution).toSelf().inSingletonScope(); bind(TabBarToolbarContribution).toService(ChatViewWidgetToolbarContribution); diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/ai-editor-manager.ts b/packages/ai-chat-ui/src/browser/chat-response-renderer/ai-selection-resolver.ts similarity index 72% rename from packages/ai-chat-ui/src/browser/chat-response-renderer/ai-editor-manager.ts rename to packages/ai-chat-ui/src/browser/chat-response-renderer/ai-selection-resolver.ts index 85f0fbeb95824..5872f804e5136 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/ai-editor-manager.ts +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/ai-selection-resolver.ts @@ -14,11 +14,11 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { CancellationToken, ContributionProvider, Prioritizeable, RecursivePartial, URI } from '@theia/core'; -import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { CancellationToken, RecursivePartial, URI } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { EditorOpenerOptions, EditorWidget, Range } from '@theia/editor/lib/browser'; -import { EditorPreviewManager } from '@theia/editor-preview/lib/browser/editor-preview-manager'; +import { EditorSelectionResolver } from '@theia/editor-preview/lib/browser/editor-preview-manager'; import { DocumentSymbol } from '@theia/monaco-editor-core/esm/vs/editor/common/languages'; import { TextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel'; import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures'; @@ -29,17 +29,8 @@ import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-p /** Regex to match GitHub-style position and range declaration with line (L) and column (C) */ export const LOCATION_REGEX = /#L(\d+)?(?:C(\d+))?(?:-L(\d+)?(?:C(\d+))?)?$/; -export const AIEditorSelectionResolver = Symbol('AIEditorSelectionResolver'); -export interface AIEditorSelectionResolver { - /** - * The priority of the resolver. A higher value resolver will be called before others. - */ - priority?: number; - resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined> -} - @injectable() -export class GitHubSelectionResolver implements AIEditorSelectionResolver { +export class GitHubSelectionResolver implements EditorSelectionResolver { priority = 100; async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined> { @@ -67,7 +58,7 @@ export class GitHubSelectionResolver implements AIEditorSelectionResolver { } @injectable() -export class TypeDocSymbolSelectionResolver implements AIEditorSelectionResolver { +export class TypeDocSymbolSelectionResolver implements EditorSelectionResolver { priority = 50; @inject(MonacoToProtocolConverter) protected readonly m2p: MonacoToProtocolConverter; @@ -123,7 +114,7 @@ export class TypeDocSymbolSelectionResolver implements AIEditorSelectionResolver } @injectable() -export class TextFragmentSelectionResolver implements AIEditorSelectionResolver { +export class TextFragmentSelectionResolver implements EditorSelectionResolver { async resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined> { if (!uri) { return; @@ -151,33 +142,3 @@ export class TextFragmentSelectionResolver implements AIEditorSelectionResolver return uri.fragment; } } - -@injectable() -export class AIEditorManager extends EditorPreviewManager { - @inject(ContributionProvider) @named(AIEditorSelectionResolver) - protected readonly resolvers: ContributionProvider; - - protected override async revealSelection(widget: EditorWidget, options: EditorOpenerOptions = {}, uri?: URI): Promise { - if (!options.selection) { - options.selection = await this.resolveSelection(options, widget, uri); - } - super.revealSelection(widget, options, uri); - } - - protected async resolveSelection(options: EditorOpenerOptions, widget: EditorWidget, uri: URI | undefined): Promise | undefined> { - if (!options.selection) { - const orderedResolvers = Prioritizeable.prioritizeAllSync(this.resolvers.getContributions(), resolver => resolver.priority ?? 1); - for (const linkResolver of orderedResolvers) { - try { - const selection = await linkResolver.value.resolveSelection(widget, options, uri); - if (selection) { - return selection; - } - } catch (error) { - console.error(error); - } - } - } - return undefined; - } -} diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/index.ts b/packages/ai-chat-ui/src/browser/chat-response-renderer/index.ts index b0846e52fe1c2..27d8e7516de32 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/index.ts +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/index.ts @@ -13,7 +13,7 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -export * from './ai-editor-manager'; +export * from './ai-selection-resolver'; export * from './code-part-renderer'; export * from './command-part-renderer'; export * from './error-part-renderer'; diff --git a/packages/editor-preview/src/browser/editor-preview-frontend-module.ts b/packages/editor-preview/src/browser/editor-preview-frontend-module.ts index b0e7bb9431f24..de5523c54e105 100644 --- a/packages/editor-preview/src/browser/editor-preview-frontend-module.ts +++ b/packages/editor-preview/src/browser/editor-preview-frontend-module.ts @@ -18,11 +18,11 @@ import '../../src/browser/style/index.css'; import { FrontendApplicationContribution, KeybindingContribution, WidgetFactory } from '@theia/core/lib/browser'; import { ContainerModule } from '@theia/core/shared/inversify'; import { bindEditorPreviewPreferences } from './editor-preview-preferences'; -import { EditorPreviewManager } from './editor-preview-manager'; +import { EditorPreviewManager, EditorSelectionResolver } from './editor-preview-manager'; import { EditorManager } from '@theia/editor/lib/browser'; import { EditorPreviewWidgetFactory } from './editor-preview-widget-factory'; import { EditorPreviewContribution } from './editor-preview-contribution'; -import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; +import { CommandContribution, MenuContribution, bindContributionProvider } from '@theia/core/lib/common'; import { OpenEditorsTreeDecorator } from '@theia/navigator/lib/browser/open-editors-widget/navigator-open-editors-decorator-service'; import { EditorPreviewTreeDecorator } from './editor-preview-tree-decorator'; @@ -43,4 +43,5 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(OpenEditorsTreeDecorator).toService(EditorPreviewTreeDecorator); bind(FrontendApplicationContribution).toService(EditorPreviewTreeDecorator); bindEditorPreviewPreferences(bind); + bindContributionProvider(bind, EditorSelectionResolver); }); diff --git a/packages/editor-preview/src/browser/editor-preview-manager.ts b/packages/editor-preview/src/browser/editor-preview-manager.ts index d2679b6afecf0..69c131f5d2839 100644 --- a/packages/editor-preview/src/browser/editor-preview-manager.ts +++ b/packages/editor-preview/src/browser/editor-preview-manager.ts @@ -14,21 +14,41 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { EditorManager, EditorOpenerOptions, EditorWidget } from '@theia/editor/lib/browser'; -import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; +import { EditorManager, EditorOpenerOptions, EditorWidget, Range } from '@theia/editor/lib/browser'; +import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; import { EditorPreviewPreferences } from './editor-preview-preferences'; -import { MaybePromise } from '@theia/core/lib/common'; +import { ContributionProvider, MaybePromise, Prioritizeable, RecursivePartial } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; import { EditorPreviewWidgetFactory, EditorPreviewOptions } from './editor-preview-widget-factory'; import { EditorPreviewWidget } from './editor-preview-widget'; import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; +export const EditorSelectionResolver = Symbol('EditorSelectionResolver'); + +/** + * Resolves an initial selection to be revealed in an editor preview. + * + * Implementations may provide a custom initial selection or editor range based on the given widget, opener options, + * and optional URI. The resolver's priority determines the order of execution, with higher values executed first. + * + * @see EditorPreviewManager#revealSelection + */ +export interface EditorSelectionResolver { + /** + * The priority of the resolver. A higher value resolver will be called before others. + */ + priority?: number; + resolveSelection(widget: EditorWidget, options: EditorOpenerOptions, uri?: URI): Promise | undefined> +} + @injectable() export class EditorPreviewManager extends EditorManager { override readonly id = EditorPreviewWidgetFactory.ID; @inject(EditorPreviewPreferences) protected readonly preferences: EditorPreviewPreferences; @inject(FrontendApplicationStateService) protected readonly stateService: FrontendApplicationStateService; + @inject(ContributionProvider) @named(EditorSelectionResolver) + protected readonly resolvers: ContributionProvider; /** * Until the layout has been restored, widget state is not reliable, so we ignore creation events. @@ -122,4 +142,28 @@ export class EditorPreviewManager extends EditorManager { widget.convertToNonPreview(); } } + + protected override async revealSelection(widget: EditorWidget, options: EditorOpenerOptions = {}, uri?: URI): Promise { + if (!options.selection) { + options.selection = await this.resolveSelection(options, widget, uri); + } + super.revealSelection(widget, options, uri); + } + + protected async resolveSelection(options: EditorOpenerOptions, widget: EditorWidget, uri: URI | undefined): Promise | undefined> { + if (!options.selection) { + const orderedResolvers = Prioritizeable.prioritizeAllSync(this.resolvers.getContributions(), resolver => resolver.priority ?? 1); + for (const linkResolver of orderedResolvers) { + try { + const selection = await linkResolver.value.resolveSelection(widget, options, uri); + if (selection) { + return selection; + } + } catch (error) { + console.error(error); + } + } + } + return options.selection; + } }