Revert "chore - Refactor inline chat classes to use private class fields (#29…"

This reverts commit 81f2b5cd2f.
This commit is contained in:
Christof Marti
2026-03-06 17:10:23 +01:00
parent 8734c3f392
commit 3b4da4f334
10 changed files with 503 additions and 592 deletions

View File

@@ -91,11 +91,11 @@ export class StartSessionAction extends Action2 {
logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize());
return;
}
return this.#runEditorCommand(editorAccessor, editor, ...args);
return this._runEditorCommand(editorAccessor, editor, ...args);
});
}
async #runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) {
private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) {
const configServce = accessor.get(IConfigurationService);
@@ -262,15 +262,12 @@ export class FixDiagnosticsAction extends AbstractInlineChatAction {
class KeepOrUndoSessionAction extends AbstractInlineChatAction {
readonly #keep: boolean;
constructor(keep: boolean, desc: IAction2Options) {
constructor(private readonly _keep: boolean, desc: IAction2Options) {
super(desc);
this.#keep = keep;
}
override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise<void> {
if (this.#keep) {
if (this._keep) {
await ctrl.acceptSession();
} else {
await ctrl.rejectSession();

View File

@@ -112,93 +112,63 @@ export class InlineChatController implements IEditorContribution {
* Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session.
* When set, this takes priority over the inlineChat.defaultModel setting.
*/
static #userSelectedModel: string | undefined;
private static _userSelectedModel: string | undefined;
readonly #store = new DisposableStore();
readonly #isActiveController = observableValue(this, false);
readonly #renderMode: IObservable<'zone' | 'hover'>;
readonly #zone: Lazy<InlineChatZoneWidget>;
private readonly _store = new DisposableStore();
private readonly _isActiveController = observableValue(this, false);
private readonly _renderMode: IObservable<'zone' | 'hover'>;
private readonly _zone: Lazy<InlineChatZoneWidget>;
readonly inputOverlayWidget: InlineChatAffordance;
readonly #inputWidget: InlineChatInputWidget;
private readonly _inputWidget: InlineChatInputWidget;
readonly #currentSession: IObservable<IInlineChatSession2 | undefined>;
readonly #editor: ICodeEditor;
readonly #instaService: IInstantiationService;
readonly #notebookEditorService: INotebookEditorService;
readonly #inlineChatSessionService: IInlineChatSessionService;
readonly #configurationService: IConfigurationService;
readonly #webContentExtractorService: ISharedWebContentExtractorService;
readonly #fileService: IFileService;
readonly #chatAttachmentResolveService: IChatAttachmentResolveService;
readonly #editorService: IEditorService;
readonly #markerDecorationsService: IMarkerDecorationsService;
readonly #languageModelService: ILanguageModelsService;
readonly #logService: ILogService;
readonly #chatEditingService: IChatEditingService;
readonly #chatService: IChatService;
private readonly _currentSession: IObservable<IInlineChatSession2 | undefined>;
get widget(): EditorBasedInlineChatWidget {
return this.#zone.value.widget;
return this._zone.value.widget;
}
get isActive() {
return Boolean(this.#currentSession.get());
return Boolean(this._currentSession.get());
}
get inputWidget(): InlineChatInputWidget {
return this.#inputWidget;
return this._inputWidget;
}
constructor(
editor: ICodeEditor,
@IInstantiationService instaService: IInstantiationService,
@INotebookEditorService notebookEditorService: INotebookEditorService,
@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService,
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instaService: IInstantiationService,
@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,
@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService configurationService: IConfigurationService,
@ISharedWebContentExtractorService webContentExtractorService: ISharedWebContentExtractorService,
@IFileService fileService: IFileService,
@IChatAttachmentResolveService chatAttachmentResolveService: IChatAttachmentResolveService,
@IEditorService editorService: IEditorService,
@IMarkerDecorationsService markerDecorationsService: IMarkerDecorationsService,
@ILanguageModelsService languageModelService: ILanguageModelsService,
@ILogService logService: ILogService,
@IChatEditingService chatEditingService: IChatEditingService,
@IChatService chatService: IChatService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,
@IFileService private readonly _fileService: IFileService,
@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService,
@IEditorService private readonly _editorService: IEditorService,
@IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService,
@ILanguageModelsService private readonly _languageModelService: ILanguageModelsService,
@ILogService private readonly _logService: ILogService,
@IChatEditingService private readonly _chatEditingService: IChatEditingService,
@IChatService private readonly _chatService: IChatService,
) {
this.#editor = editor;
this.#instaService = instaService;
this.#notebookEditorService = notebookEditorService;
this.#inlineChatSessionService = inlineChatSessionService;
this.#configurationService = configurationService;
this.#webContentExtractorService = webContentExtractorService;
this.#fileService = fileService;
this.#chatAttachmentResolveService = chatAttachmentResolveService;
this.#editorService = editorService;
this.#markerDecorationsService = markerDecorationsService;
this.#languageModelService = languageModelService;
this.#logService = logService;
this.#chatEditingService = chatEditingService;
this.#chatService = chatService;
const editorObs = observableCodeEditor(editor);
const editorObs = observableCodeEditor(_editor);
const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);
const ctxFileBelongsToChat = CTX_INLINE_CHAT_FILE_BELONGS_TO_CHAT.bindTo(contextKeyService);
const ctxPendingConfirmation = CTX_INLINE_CHAT_PENDING_CONFIRMATION.bindTo(contextKeyService);
const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this.#configurationService);
this.#renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this.#configurationService);
const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService);
this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService);
// Track whether the current editor's file is being edited by any chat editing session
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const model = editorObs.model.read(r);
if (!model) {
ctxFileBelongsToChat.set(false);
return;
}
const sessions = this.#chatEditingService.editingSessionsObs.read(r);
const sessions = this._chatEditingService.editingSessionsObs.read(r);
let hasEdits = false;
for (const session of sessions) {
const entries = session.entries.read(r);
@@ -215,25 +185,25 @@ export class InlineChatController implements IEditorContribution {
ctxFileBelongsToChat.set(hasEdits);
}));
const overlayWidget = this.#inputWidget = this.#store.add(this.#instaService.createInstance(InlineChatInputWidget, editorObs));
const sessionOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatSessionOverlayWidget, editorObs));
this.inputOverlayWidget = this.#store.add(this.#instaService.createInstance(InlineChatAffordance, this.#editor, overlayWidget));
const overlayWidget = this._inputWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs));
const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs));
this.inputOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget));
this.#zone = new Lazy<InlineChatZoneWidget>(() => {
this._zone = new Lazy<InlineChatZoneWidget>(() => {
assertType(this.#editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model');
assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model');
const location: IChatWidgetLocationOptions = {
location: ChatAgentLocation.EditorInline,
resolveData: () => {
assertType(this.#editor.hasModel());
const wholeRange = this.#editor.getSelection();
const document = this.#editor.getModel().uri;
assertType(this._editor.hasModel());
const wholeRange = this._editor.getSelection();
const document = this._editor.getModel().uri;
return {
type: ChatAgentLocation.EditorInline,
id: getEditorId(this.#editor, this.#editor.getModel()),
selection: this.#editor.getSelection(),
id: getEditorId(this._editor, this._editor.getModel()),
selection: this._editor.getSelection(),
document,
wholeRange
};
@@ -243,22 +213,22 @@ export class InlineChatController implements IEditorContribution {
// inline chat in notebooks
// check if this editor is part of a notebook editor
// if so, update the location and use the notebook specific widget
const notebookEditor = this.#notebookEditorService.getNotebookForPossibleCell(this.#editor);
const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor);
if (!!notebookEditor) {
location.location = ChatAgentLocation.Notebook;
if (notebookAgentConfig.get()) {
location.resolveData = () => {
assertType(this.#editor.hasModel());
assertType(this._editor.hasModel());
return {
type: ChatAgentLocation.Notebook,
sessionInputUri: this.#editor.getModel().uri,
sessionInputUri: this._editor.getModel().uri,
};
};
}
}
const result = this.#instaService.createInstance(InlineChatZoneWidget,
const result = this._instaService.createInstance(InlineChatZoneWidget,
location,
{
enableWorkingSet: 'implicit',
@@ -278,33 +248,33 @@ export class InlineChatController implements IEditorContribution {
},
defaultMode: ChatMode.Ask
},
{ editor: this.#editor, notebookEditor },
{ editor: this._editor, notebookEditor },
() => Promise.resolve(),
);
this.#store.add(result);
this._store.add(result);
result.domNode.classList.add('inline-chat-2');
return result;
});
const sessionsSignal = observableSignalFromEvent(this, this.#inlineChatSessionService.onDidChangeSessions);
const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions);
this.#currentSession = derived(r => {
this._currentSession = derived(r => {
sessionsSignal.read(r);
const model = editorObs.model.read(r);
const session = model && this.#inlineChatSessionService.getSessionByTextModel(model.uri);
const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri);
return session ?? undefined;
});
let lastSession: IInlineChatSession2 | undefined = undefined;
this.#store.add(autorun(r => {
const session = this.#currentSession.read(r);
this._store.add(autorun(r => {
const session = this._currentSession.read(r);
if (!session) {
this.#isActiveController.set(false, undefined);
this._isActiveController.set(false, undefined);
if (lastSession && !lastSession.chatModel.hasRequests) {
const state = lastSession.chatModel.inputModel.state.read(undefined);
@@ -320,24 +290,23 @@ export class InlineChatController implements IEditorContribution {
let foundOne = false;
for (const editor of codeEditorService.listCodeEditors()) {
const ctrl = InlineChatController.get(editor);
if (ctrl && ctrl.#isActiveController.read(undefined)) {
if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) {
foundOne = true;
break;
}
}
if (!foundOne && editorObs.isFocused.read(r)) {
this.#isActiveController.set(true, undefined);
this._isActiveController.set(true, undefined);
}
}));
const visibleSessionObs = observableValue<IInlineChatSession2 | undefined>(this, undefined);
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const model = editorObs.model.read(r);
const session = this.#currentSession.read(r);
const isActive = this.#isActiveController.read(r);
const session = this._currentSession.read(r);
const isActive = this._isActiveController.read(r);
if (!session || !isActive || !model) {
visibleSessionObs.set(undefined, undefined);
@@ -353,38 +322,38 @@ export class InlineChatController implements IEditorContribution {
});
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
// HIDE/SHOW
const session = visibleSessionObs.read(r);
const renderMode = this.#renderMode.read(r);
const renderMode = this._renderMode.read(r);
if (!session) {
this.#zone.rawValue?.hide();
this.#zone.rawValue?.widget.chatWidget.setModel(undefined);
this.#editor.focus();
this._zone.rawValue?.hide();
this._zone.rawValue?.widget.chatWidget.setModel(undefined);
_editor.focus();
ctxInlineChatVisible.reset();
} else if (renderMode === 'hover') {
// hover mode: set model but don't show zone, keep focus in editor
this.#zone.value.widget.chatWidget.setModel(session.chatModel);
this.#zone.rawValue?.hide();
this._zone.value.widget.chatWidget.setModel(session.chatModel);
this._zone.rawValue?.hide();
ctxInlineChatVisible.set(true);
} else {
ctxInlineChatVisible.set(true);
this.#zone.value.widget.chatWidget.setModel(session.chatModel);
if (!this.#zone.value.position) {
this.#zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
this.#zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug
this.#zone.value.show(session.initialPosition);
this._zone.value.widget.chatWidget.setModel(session.chatModel);
if (!this._zone.value.position) {
this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug
this._zone.value.show(session.initialPosition);
}
this.#zone.value.reveal(this.#zone.value.position!);
this.#zone.value.widget.focus();
this._zone.value.reveal(this._zone.value.position!);
this._zone.value.widget.focus();
}
}));
// Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const session = visibleSessionObs.read(r);
const renderMode = this.#renderMode.read(r);
const renderMode = this._renderMode.read(r);
if (!session || renderMode !== 'hover') {
ctxPendingConfirmation.set(false);
sessionOverlayWidget.hide();
@@ -406,7 +375,7 @@ export class InlineChatController implements IEditorContribution {
}
}));
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const session = visibleSessionObs.read(r);
if (session) {
const entries = session.editingSession.entries.read(r);
@@ -424,7 +393,7 @@ export class InlineChatController implements IEditorContribution {
for (const entry of otherEntries) {
// OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend
// that modifies other files
this.#editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError);
this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError);
}
}
}));
@@ -445,36 +414,36 @@ export class InlineChatController implements IEditorContribution {
});
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const response = lastResponseObs.read(r);
this.#zone.rawValue?.widget.updateInfo('');
this._zone.rawValue?.widget.updateInfo('');
if (!response?.isInProgress.read(r)) {
if (response?.result?.errorDetails) {
// ERROR case
this.#zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`);
this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`);
alert(response.result.errorDetails.message);
}
// no response or not in progress
this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false);
this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false);
this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
} else {
this.#zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true);
this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true);
let placeholder = response.request?.message.text;
const lastProgress = lastResponseProgressObs.read(r);
if (lastProgress) {
placeholder = renderAsPlaintext(lastProgress.content);
}
this.#zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working..."));
this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working..."));
}
}));
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const session = visibleSessionObs.read(r);
if (!session) {
return;
@@ -487,25 +456,25 @@ export class InlineChatController implements IEditorContribution {
}));
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const session = visibleSessionObs.read(r);
const entry = session?.editingSession.readEntry(session.uri, r);
// make sure there is an editor integration
const pane = this.#editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this.#editor || isNotebookWithCellEditor(candidate, this.#editor));
const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor));
if (pane && entry) {
entry?.getEditorIntegration(pane);
}
// make sure the ZONE isn't inbetween a diff and move above if so
if (entry?.diffInfo && this.#zone.value.position) {
const { position } = this.#zone.value;
if (entry?.diffInfo && this._zone.value.position) {
const { position } = this._zone.value;
const diff = entry.diffInfo.read(r);
for (const change of diff.changes) {
if (change.modified.contains(position.lineNumber)) {
this.#zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1));
this._zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1));
break;
}
}
@@ -514,90 +483,90 @@ export class InlineChatController implements IEditorContribution {
}
dispose(): void {
this.#store.dispose();
this._store.dispose();
}
getWidgetPosition(): Position | undefined {
return this.#zone.rawValue?.position;
return this._zone.rawValue?.position;
}
focus() {
this.#zone.rawValue?.widget.focus();
this._zone.rawValue?.widget.focus();
}
async run(arg?: InlineChatRunOptions): Promise<boolean> {
assertType(this.#editor.hasModel());
const uri = this.#editor.getModel().uri;
assertType(this._editor.hasModel());
const uri = this._editor.getModel().uri;
const existingSession = this.#inlineChatSessionService.getSessionByTextModel(uri);
const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri);
if (existingSession) {
await existingSession.editingSession.accept();
existingSession.dispose();
}
this.#isActiveController.set(true, undefined);
this._isActiveController.set(true, undefined);
const session = this.#inlineChatSessionService.createSession(this.#editor);
const session = this._inlineChatSessionService.createSession(this._editor);
// Store for tracking model changes during this session
const sessionStore = new DisposableStore();
try {
await this.#applyModelDefaults(session, sessionStore);
await this._applyModelDefaults(session, sessionStore);
if (arg) {
arg.attachDiagnostics ??= this.#configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone';
arg.attachDiagnostics ??= this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'zone';
}
// ADD diagnostics (only when explicitly requested)
if (arg?.attachDiagnostics) {
const entries: IChatRequestVariableEntry[] = [];
for (const [range, marker] of this.#markerDecorationsService.getLiveMarkers(uri)) {
if (range.intersectRanges(this.#editor.getSelection())) {
for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) {
if (range.intersectRanges(this._editor.getSelection())) {
const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker);
entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter));
}
}
if (entries.length > 0) {
this.#zone.value.widget.chatWidget.attachmentModel.addContext(...entries);
this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries);
const msg = entries.length > 1
? localize('fixN', "Fix the attached problems")
: localize('fix1', "Fix the attached problem");
this.#zone.value.widget.chatWidget.input.setValue(msg, true);
this._zone.value.widget.chatWidget.input.setValue(msg, true);
arg.message = msg;
this.#zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
}
}
// Check args
if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {
if (arg.initialRange) {
this.#editor.revealRange(arg.initialRange);
this._editor.revealRange(arg.initialRange);
}
if (arg.initialSelection) {
this.#editor.setSelection(arg.initialSelection);
this._editor.setSelection(arg.initialSelection);
}
if (arg.attachments) {
await Promise.all(arg.attachments.map(async attachment => {
await this.#zone.value.widget.chatWidget.attachmentModel.addFile(attachment);
await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment);
}));
delete arg.attachments;
}
if (arg.modelSelector) {
const id = (await this.#languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);
const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);
if (!id) {
throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`);
}
const model = this.#languageModelService.lookupLanguageModel(id);
const model = this._languageModelService.lookupLanguageModel(id);
if (!model) {
throw new Error(`Language model not loaded: ${id}.`);
}
this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });
this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });
}
if (arg.message) {
this.#zone.value.widget.chatWidget.setInput(arg.message);
this._zone.value.widget.chatWidget.setInput(arg.message);
if (arg.autoSend) {
await this.#zone.value.widget.chatWidget.acceptInput();
await this._zone.value.widget.chatWidget.acceptInput();
}
}
}
@@ -623,7 +592,7 @@ export class InlineChatController implements IEditorContribution {
}
async acceptSession() {
const session = this.#currentSession.get();
const session = this._currentSession.get();
if (!session) {
return;
}
@@ -632,23 +601,23 @@ export class InlineChatController implements IEditorContribution {
}
async rejectSession() {
const session = this.#currentSession.get();
const session = this._currentSession.get();
if (!session) {
return;
}
this.#chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject');
this._chatService.cancelCurrentRequestForSession(session.chatModel.sessionResource, 'inlineChatReject');
await session.editingSession.reject();
session.dispose();
}
async #selectVendorDefaultModel(session: IInlineChatSession2): Promise<void> {
const model = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.get();
private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise<void> {
const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get();
if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) {
const ids = await this.#languageModelService.selectLanguageModels({ vendor: model.metadata.vendor });
const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor });
for (const identifier of ids) {
const candidate = this.#languageModelService.lookupLanguageModel(identifier);
const candidate = this._languageModelService.lookupLanguageModel(identifier);
if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) {
this.#zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier });
this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier });
break;
}
}
@@ -659,39 +628,39 @@ export class InlineChatController implements IEditorContribution {
* Applies model defaults based on settings and tracks user model changes.
* Prioritization: user session choice > inlineChat.defaultModel setting > vendor default
*/
async #applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise<void> {
const userSelectedModel = InlineChatController.#userSelectedModel;
const defaultModelSetting = this.#configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise<void> {
const userSelectedModel = InlineChatController._userSelectedModel;
const defaultModelSetting = this._configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
let modelApplied = false;
// 1. Try user's explicitly chosen model from a previous inline chat in the same session
if (userSelectedModel) {
modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]);
modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]);
if (!modelApplied) {
// User's previously selected model is no longer available, clear it
InlineChatController.#userSelectedModel = undefined;
InlineChatController._userSelectedModel = undefined;
}
}
// 2. Try inlineChat.defaultModel setting
if (!modelApplied && defaultModelSetting) {
modelApplied = this.#zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]);
modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]);
if (!modelApplied) {
this.#logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
}
}
// 3. Fall back to vendor default
if (!modelApplied) {
await this.#selectVendorDefaultModel(session);
await this._selectVendorDefaultModel(session);
}
// Track model changes - store user's explicit choice in the given sessions.
// NOTE: This currently detects any model change, not just user-initiated ones.
let initialModelId: string | undefined;
sessionStore.add(autorun(r => {
const newModel = this.#zone.value.widget.chatWidget.input.selectedLanguageModel.read(r);
const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r);
if (!newModel) {
return;
}
@@ -701,25 +670,25 @@ export class InlineChatController implements IEditorContribution {
}
if (initialModelId !== newModel.identifier) {
// User explicitly changed model, store their choice as qualified name
InlineChatController.#userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata);
InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata);
initialModelId = newModel.identifier;
}
}));
}
async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {
const value = this.#currentSession.get();
const value = this._currentSession.get();
if (!value) {
return undefined;
}
if (attachment.scheme === Schemas.file) {
if (await this.#fileService.canHandleResource(attachment)) {
return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
if (await this._fileService.canHandleResource(attachment)) {
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
}
} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {
const extractedImages = await this.#webContentExtractorService.readImage(attachment, CancellationToken.None);
const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);
if (extractedImages) {
return await this.#chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);
}
}
return undefined;

View File

@@ -33,13 +33,12 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
class QuickFixActionViewItem extends MenuEntryActionViewItem {
readonly #lightBulbStore = this._store.add(new MutableDisposable<DisposableStore>());
readonly #editor: ICodeEditor;
#currentTitle: string | undefined;
private readonly _lightBulbStore = this._store.add(new MutableDisposable<DisposableStore>());
private _currentTitle: string | undefined;
constructor(
action: MenuItemAction,
editor: ICodeEditor,
private readonly _editor: ICodeEditor,
@IKeybindingService keybindingService: IKeybindingService,
@INotificationService notificationService: INotificationService,
@IContextKeyService contextKeyService: IContextKeyService,
@@ -56,7 +55,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
elementGetter: () => HTMLElement | undefined = () => undefined;
override async run(...args: unknown[]): Promise<void> {
const controller = CodeActionController.get(editor);
const controller = CodeActionController.get(_editor);
const info = controller?.lightBulbState.get();
const element = this.elementGetter();
if (controller && info && element) {
@@ -68,27 +67,26 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
super(wrappedAction, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService);
this.#editor = editor;
wrappedAction.elementGetter = () => this.element;
}
override render(container: HTMLElement): void {
super.render(container);
this.#updateFromLightBulb();
this._updateFromLightBulb();
}
protected override getTooltip(): string {
return this.#currentTitle ?? super.getTooltip();
return this._currentTitle ?? super.getTooltip();
}
#updateFromLightBulb(): void {
const controller = CodeActionController.get(this.#editor);
private _updateFromLightBulb(): void {
const controller = CodeActionController.get(this._editor);
if (!controller) {
return;
}
const store = new DisposableStore();
this.#lightBulbStore.value = store;
this._lightBulbStore.value = store;
store.add(autorun(reader => {
const info = controller.lightBulbState.read(reader);
@@ -101,7 +99,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
}
// Update tooltip
this.#currentTitle = info?.title;
this._currentTitle = info?.title;
this.updateTooltip();
}));
}
@@ -109,7 +107,7 @@ class QuickFixActionViewItem extends MenuEntryActionViewItem {
class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem {
readonly #kbLabel: string | undefined;
private readonly _kbLabel: string | undefined;
constructor(
action: MenuItemAction,
@@ -123,14 +121,14 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem {
super(action, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService);
this.options.label = true;
this.options.icon = false;
this.#kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined;
this._kbLabel = keybindingService.lookupKeybinding(action.id)?.getLabel() ?? undefined;
}
protected override updateLabel(): void {
if (this.label) {
dom.reset(this.label,
this.action.label,
...(this.#kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this.#kbLabel)] : [])
...(this._kbLabel ? [dom.$('span.inline-chat-keybinding', undefined, this._kbLabel)] : [])
);
}
}
@@ -142,42 +140,38 @@ class LabelWithKeybindingActionViewItem extends MenuEntryActionViewItem {
*/
export class InlineChatEditorAffordance extends Disposable implements IContentWidget {
static #idPool = 0;
private static _idPool = 0;
readonly #id = `inline-chat-content-widget-${InlineChatEditorAffordance.#idPool++}`;
readonly #domNode: HTMLElement;
#position: IContentWidgetPosition | null = null;
#isVisible = false;
private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`;
private readonly _domNode: HTMLElement;
private _position: IContentWidgetPosition | null = null;
private _isVisible = false;
readonly #onDidRunAction = this._store.add(new Emitter<string>());
readonly onDidRunAction: Event<string> = this.#onDidRunAction.event;
private readonly _onDidRunAction = this._store.add(new Emitter<string>());
readonly onDidRunAction: Event<string> = this._onDidRunAction.event;
readonly allowEditorOverflow = true;
readonly suppressMouseDown = false;
readonly #editor: ICodeEditor;
constructor(
editor: ICodeEditor,
private readonly _editor: ICodeEditor,
selection: IObservable<Selection | undefined>,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this.#editor = editor;
// Create the widget DOM
this.#domNode = dom.$('.inline-chat-content-widget');
this._domNode = dom.$('.inline-chat-content-widget');
// Create toolbar with the inline chat start action
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#domNode, MenuId.InlineChatEditorAffordance, {
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.InlineChatEditorAffordance, {
telemetrySource: 'inlineChatEditorAffordance',
hiddenItemStrategy: HiddenItemStrategy.Ignore,
menuOptions: { renderShortTitle: true },
toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true },
actionViewItemProvider: (action: IAction) => {
if (action instanceof MenuItemAction && action.id === quickFixCommandId) {
return instantiationService.createInstance(QuickFixActionViewItem, action, this.#editor);
return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor);
}
if (action instanceof MenuItemAction && (action.id === ACTION_START || action.id === ACTION_ASK_IN_CHAT || action.id === 'inlineChat.fixDiagnostics')) {
return instantiationService.createInstance(LabelWithKeybindingActionViewItem, action);
@@ -186,37 +180,37 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi
}
}));
this._store.add(toolbar.actionRunner.onDidRun((e) => {
this.#onDidRunAction.fire(e.action.id);
this.#hide();
this._onDidRunAction.fire(e.action.id);
this._hide();
}));
this._store.add(autorun(r => {
const sel = selection.read(r);
if (sel) {
this.#show(sel);
this._show(sel);
} else {
this.#hide();
this._hide();
}
}));
}
#show(selection: Selection): void {
private _show(selection: Selection): void {
if (selection.isEmpty()) {
this.#showAtLineStart(selection.getPosition().lineNumber);
this._showAtLineStart(selection.getPosition().lineNumber);
} else {
this.#showAtSelection(selection);
this._showAtSelection(selection);
}
if (this.#isVisible) {
this.#editor.layoutContentWidget(this);
if (this._isVisible) {
this._editor.layoutContentWidget(this);
} else {
this.#editor.addContentWidget(this);
this.#isVisible = true;
this._editor.addContentWidget(this);
this._isVisible = true;
}
}
#showAtSelection(selection: Selection): void {
private _showAtSelection(selection: Selection): void {
const cursorPosition = selection.getPosition();
const direction = selection.getDirection();
@@ -224,20 +218,20 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi
? ContentWidgetPositionPreference.ABOVE
: ContentWidgetPositionPreference.BELOW;
this.#position = {
this._position = {
position: cursorPosition,
preference: [preference],
};
}
#showAtLineStart(lineNumber: number): void {
const model = this.#editor.getModel();
private _showAtLineStart(lineNumber: number): void {
const model = this._editor.getModel();
if (!model) {
return;
}
const tabSize = model.getOptions().tabSize;
const fontInfo = this.#editor.getOptions().get(EditorOption.fontInfo);
const fontInfo = this._editor.getOptions().get(EditorOption.fontInfo);
const lineContent = model.getLineContent(lineNumber);
const indent = computeIndentLevel(lineContent, tabSize);
const lineHasSpace = indent < 0 ? true : fontInfo.spaceWidth * indent > 22;
@@ -260,43 +254,43 @@ export class InlineChatEditorAffordance extends Disposable implements IContentWi
const effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1;
this.#position = {
this._position = {
position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber },
preference: [ContentWidgetPositionPreference.EXACT],
};
}
#hide(): void {
if (this.#isVisible) {
this.#isVisible = false;
this.#editor.removeContentWidget(this);
private _hide(): void {
if (this._isVisible) {
this._isVisible = false;
this._editor.removeContentWidget(this);
}
}
getId(): string {
return this.#id;
return this._id;
}
getDomNode(): HTMLElement {
return this.#domNode;
return this._domNode;
}
getPosition(): IContentWidgetPosition | null {
return this.#position;
return this._position;
}
beforeRender(): IDimension | null {
const position = this.#editor.getPosition();
const lineHeight = position ? this.#editor.getLineHeightForPosition(position) : this.#editor.getOption(EditorOption.lineHeight);
const position = this._editor.getPosition();
const lineHeight = position ? this._editor.getLineHeightForPosition(position) : this._editor.getOption(EditorOption.lineHeight);
this.#domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`);
this._domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`);
return null;
}
override dispose(): void {
if (this.#isVisible) {
this.#editor.removeContentWidget(this);
if (this._isVisible) {
this._editor.removeContentWidget(this);
}
super.dispose();
}

View File

@@ -26,8 +26,8 @@ import { IUserInteractionService } from '../../../../platform/userInteraction/br
export class InlineChatGutterAffordance extends InlineEditsGutterIndicator {
readonly #onDidRunAction = this._store.add(new Emitter<string>());
readonly onDidRunAction: Event<string> = this.#onDidRunAction.event;
private readonly _onDidRunAction = this._store.add(new Emitter<string>());
readonly onDidRunAction: Event<string> = this._onDidRunAction.event;
constructor(
myEditorObs: ObservableCodeEditor,
@@ -108,6 +108,6 @@ export class InlineChatGutterAffordance extends InlineEditsGutterIndicator {
this._store.add(menu);
this._store.add(this.onDidCloseWithCommand(commandId => this.#onDidRunAction.fire(commandId)));
this._store.add(this.onDidCloseWithCommand(commandId => this._onDidRunAction.fire(commandId)));
}
}

View File

@@ -14,7 +14,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js
export class InlineChatNotebookContribution {
readonly #store = new DisposableStore();
private readonly _store = new DisposableStore();
constructor(
@IInlineChatSessionService sessionService: IInlineChatSessionService,
@@ -22,7 +22,7 @@ export class InlineChatNotebookContribution {
@INotebookEditorService notebookEditorService: INotebookEditorService,
) {
this.#store.add(sessionService.onWillStartSession(newSessionEditor => {
this._store.add(sessionService.onWillStartSession(newSessionEditor => {
const candidate = CellUri.parse(newSessionEditor.getModel().uri);
if (!candidate) {
return;
@@ -51,6 +51,6 @@ export class InlineChatNotebookContribution {
}
dispose(): void {
this.#store.dispose();
this._store.dispose();
}
}

View File

@@ -44,58 +44,51 @@ import { assertType } from '../../../../base/common/types.js';
*/
export class InlineChatInputWidget extends Disposable {
readonly #domNode: HTMLElement;
readonly #container: HTMLElement;
readonly #inputContainer: HTMLElement;
readonly #toolbarContainer: HTMLElement;
readonly #input: IActiveCodeEditor;
readonly #position = observableValue<IOverlayWidgetPosition | null>(this, null);
readonly position: IObservable<IOverlayWidgetPosition | null> = this.#position;
private readonly _domNode: HTMLElement;
private readonly _container: HTMLElement;
private readonly _inputContainer: HTMLElement;
private readonly _toolbarContainer: HTMLElement;
private readonly _input: IActiveCodeEditor;
private readonly _position = observableValue<IOverlayWidgetPosition | null>(this, null);
readonly position: IObservable<IOverlayWidgetPosition | null> = this._position;
readonly #showStore = this._store.add(new DisposableStore());
readonly #stickyScrollHeight: IObservable<number>;
readonly #layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>;
#anchorLineNumber: number = 0;
#anchorLeft: number = 0;
#anchorAbove: boolean = false;
private readonly _showStore = this._store.add(new DisposableStore());
private readonly _stickyScrollHeight: IObservable<number>;
private readonly _layoutData: IObservable<{ totalWidth: number; toolbarWidth: number; height: number; editorPad: number }>;
private _anchorLineNumber: number = 0;
private _anchorLeft: number = 0;
private _anchorAbove: boolean = false;
readonly #editorObs: ObservableCodeEditor;
readonly #contextKeyService: IContextKeyService;
readonly #menuService: IMenuService;
constructor(
editorObs: ObservableCodeEditor,
@IContextKeyService contextKeyService: IContextKeyService,
@IMenuService menuService: IMenuService,
private readonly _editorObs: ObservableCodeEditor,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IMenuService private readonly _menuService: IMenuService,
@IInstantiationService instantiationService: IInstantiationService,
@IModelService modelService: IModelService,
@IConfigurationService configurationService: IConfigurationService,
) {
super();
this.#editorObs = editorObs;
this.#contextKeyService = contextKeyService;
this.#menuService = menuService;
// Create container
this.#domNode = dom.$('.inline-chat-gutter-menu');
this._domNode = dom.$('.inline-chat-gutter-menu');
// Create inner container (background + focus border)
this.#container = dom.append(this.#domNode, dom.$('.inline-chat-gutter-container'));
this._container = dom.append(this._domNode, dom.$('.inline-chat-gutter-container'));
// Create input editor container
this.#inputContainer = dom.append(this.#container, dom.$('.input'));
this._inputContainer = dom.append(this._container, dom.$('.input'));
// Create toolbar container
this.#toolbarContainer = dom.append(this.#container, dom.$('.toolbar'));
this._toolbarContainer = dom.append(this._container, dom.$('.toolbar'));
// Create vertical actions bar below the input container
const actionsContainer = dom.append(this.#domNode, dom.$('.inline-chat-gutter-actions'));
const actionsContainer = dom.append(this._domNode, dom.$('.inline-chat-gutter-actions'));
const actionBar = this._store.add(new ActionBar(actionsContainer, {
orientation: ActionsOrientation.VERTICAL,
preventLoopNavigation: true,
}));
const actionsMenu = this._store.add(this.#menuService.createMenu(MenuId.ChatEditorInlineMenu, this.#contextKeyService));
const actionsMenu = this._store.add(this._menuService.createMenu(MenuId.ChatEditorInlineMenu, this._contextKeyService));
const updateActions = () => {
const actions = getFlatActionBarActions(actionsMenu.getActions({ shouldForwardArgs: true }));
actionBar.clear();
@@ -130,13 +123,13 @@ export class InlineChatInputWidget extends Disposable {
])
};
this.#input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this.#inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor;
this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor;
const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true));
this.#input.setModel(model);
this._input.setModel(model);
// Create toolbar
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this.#toolbarContainer, MenuId.InlineChatInput, {
const toolbar = this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MenuId.InlineChatInput, {
telemetrySource: 'inlineChatInput.toolbar',
hiddenItemStrategy: HiddenItemStrategy.NoHide,
toolbarOptions: {
@@ -146,8 +139,8 @@ export class InlineChatInputWidget extends Disposable {
}));
// Initialize sticky scroll height observable
const stickyScrollController = StickyScrollController.get(this.#editorObs.editor);
this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
const stickyScrollController = StickyScrollController.get(this._editorObs.editor);
this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
// Track toolbar width changes
const toolbarWidth = observableValue<number>(this, 0);
@@ -157,24 +150,24 @@ export class InlineChatInputWidget extends Disposable {
this._store.add(resizeObserver);
this._store.add(resizeObserver.observe(toolbar.getElement()));
const contentWidth = observableFromEvent(this, this.#input.onDidChangeModelContent, () => this.#input.getContentWidth());
const contentHeight = observableFromEvent(this, this.#input.onDidContentSizeChange, () => this.#input.getContentHeight());
const contentWidth = observableFromEvent(this, this._input.onDidChangeModelContent, () => this._input.getContentWidth());
const contentHeight = observableFromEvent(this, this._input.onDidContentSizeChange, () => this._input.getContentHeight());
this.#layoutData = derived(r => {
this._layoutData = derived(r => {
const editorPad = 6;
const totalWidth = contentWidth.read(r) + editorPad + toolbarWidth.read(r);
const minWidth = 220;
const maxWidth = 600;
const clampedWidth = this.#input.getOption(EditorOption.wordWrap) === 'on'
const clampedWidth = this._input.getOption(EditorOption.wordWrap) === 'on'
? maxWidth
: Math.max(minWidth, Math.min(totalWidth, maxWidth));
const lineHeight = this.#input.getOption(EditorOption.lineHeight);
const lineHeight = this._input.getOption(EditorOption.lineHeight);
const clampedHeight = Math.min(contentHeight.read(r), (3 * lineHeight));
if (totalWidth > clampedWidth) {
// enable word wrap
this.#input.updateOptions({ wordWrap: 'on', });
this._input.updateOptions({ wordWrap: 'on', });
}
return {
@@ -187,42 +180,42 @@ export class InlineChatInputWidget extends Disposable {
// Update container width and editor layout when width changes
this._store.add(autorun(r => {
const { editorPad, toolbarWidth, totalWidth, height } = this.#layoutData.read(r);
const { editorPad, toolbarWidth, totalWidth, height } = this._layoutData.read(r);
const inputWidth = totalWidth - toolbarWidth - editorPad;
this.#container.style.width = `${totalWidth}px`;
this.#inputContainer.style.width = `${inputWidth}px`;
this.#input.layout({ width: inputWidth, height });
this._container.style.width = `${totalWidth}px`;
this._inputContainer.style.width = `${inputWidth}px`;
this._input.layout({ width: inputWidth, height });
}));
// Toggle focus class on the container
this._store.add(this.#input.onDidFocusEditorText(() => this.#container.classList.add('focused')));
this._store.add(this.#input.onDidBlurEditorText(() => this.#container.classList.remove('focused')));
this._store.add(this._input.onDidFocusEditorText(() => this._container.classList.add('focused')));
this._store.add(this._input.onDidBlurEditorText(() => this._container.classList.remove('focused')));
// Toggle scroll decoration on the toolbar
this._store.add(this.#input.onDidScrollChange(e => {
this.#toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0);
this._store.add(this._input.onDidScrollChange(e => {
this._toolbarContainer.classList.toggle('fake-scroll-decoration', e.scrollTop > 0);
}));
// Track input text for context key and adjust width based on content
const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this.#contextKeyService);
this._store.add(this.#input.onDidChangeModelContent(() => {
inputHasText.set(this.#input.getModel().getValue().trim().length > 0);
const inputHasText = CTX_INLINE_CHAT_INPUT_HAS_TEXT.bindTo(this._contextKeyService);
this._store.add(this._input.onDidChangeModelContent(() => {
inputHasText.set(this._input.getModel().getValue().trim().length > 0);
}));
this._store.add(toDisposable(() => inputHasText.reset()));
// Track focus state
const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this.#contextKeyService);
this._store.add(this.#input.onDidFocusEditorText(() => inputWidgetFocused.set(true)));
this._store.add(this.#input.onDidBlurEditorText(() => inputWidgetFocused.set(false)));
const inputWidgetFocused = CTX_INLINE_CHAT_INPUT_WIDGET_FOCUSED.bindTo(this._contextKeyService);
this._store.add(this._input.onDidFocusEditorText(() => inputWidgetFocused.set(true)));
this._store.add(this._input.onDidBlurEditorText(() => inputWidgetFocused.set(false)));
this._store.add(toDisposable(() => inputWidgetFocused.reset()));
// Handle key events: ArrowDown to move to actions
this._store.add(this.#input.onKeyDown(e => {
this._store.add(this._input.onKeyDown(e => {
if (e.keyCode === KeyCode.DownArrow && !actionBar.isEmpty()) {
const model = this.#input.getModel();
const position = this.#input.getPosition();
const model = this._input.getModel();
const position = this._input.getPosition();
if (position && position.lineNumber === model.getLineCount()) {
e.preventDefault();
e.stopPropagation();
@@ -244,18 +237,18 @@ export class InlineChatInputWidget extends Disposable {
if (firstItem?.element && dom.isAncestorOfActiveElement(firstItem.element)) {
event.preventDefault();
event.stopPropagation();
this.#input.focus();
this._input.focus();
}
}
}, true));
// Track focus - hide when focus leaves
const focusTracker = this._store.add(dom.trackFocus(this.#domNode));
const focusTracker = this._store.add(dom.trackFocus(this._domNode));
this._store.add(focusTracker.onDidBlur(() => this.hide()));
}
get value(): string {
return this.#input.getModel().getValue().trim();
return this._input.getModel().getValue().trim();
}
/**
@@ -265,77 +258,77 @@ export class InlineChatInputWidget extends Disposable {
* @param anchorAbove Whether to anchor above the position (widget grows upward)
*/
show(lineNumber: number, left: number, anchorAbove: boolean, placeholder: string): void {
this.#showStore.clear();
this._showStore.clear();
// Clear input state
this.#input.updateOptions({ wordWrap: 'off', placeholder });
this.#input.getModel().setValue('');
this._input.updateOptions({ wordWrap: 'off', placeholder });
this._input.getModel().setValue('');
// Store anchor info for scroll updates
this.#anchorLineNumber = lineNumber;
this.#anchorLeft = left;
this.#anchorAbove = anchorAbove;
this._anchorLineNumber = lineNumber;
this._anchorLeft = left;
this._anchorAbove = anchorAbove;
// Set initial position
this.#updatePosition();
this._updatePosition();
// Create overlay widget via observable pattern
this.#showStore.add(this.#editorObs.createOverlayWidget({
domNode: this.#domNode,
position: this.#position,
this._showStore.add(this._editorObs.createOverlayWidget({
domNode: this._domNode,
position: this._position,
minContentWidthInPx: constObservable(0),
allowEditorOverflow: true,
}));
// If anchoring above, adjust position after render to account for widget height
if (anchorAbove) {
this.#updatePosition();
this._updatePosition();
}
// Update position on scroll, hide if anchor line is out of view (only when input is empty)
this.#showStore.add(this.#editorObs.editor.onDidScrollChange(() => {
const visibleRanges = this.#editorObs.editor.getVisibleRanges();
this._showStore.add(this._editorObs.editor.onDidScrollChange(() => {
const visibleRanges = this._editorObs.editor.getVisibleRanges();
const isLineVisible = visibleRanges.some(range =>
this.#anchorLineNumber >= range.startLineNumber && this.#anchorLineNumber <= range.endLineNumber
this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber
);
const hasContent = !!this.#input.getModel().getValue();
const hasContent = !!this._input.getModel().getValue();
if (!isLineVisible && !hasContent) {
this.hide();
} else {
this.#updatePosition();
this._updatePosition();
}
}));
// Focus the input editor
setTimeout(() => this.#input.focus(), 0);
setTimeout(() => this._input.focus(), 0);
}
#updatePosition(): void {
const editor = this.#editorObs.editor;
private _updatePosition(): void {
const editor = this._editorObs.editor;
const lineHeight = editor.getOption(EditorOption.lineHeight);
const top = editor.getTopForLineNumber(this.#anchorLineNumber) - editor.getScrollTop();
const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop();
let adjustedTop = top;
if (this.#anchorAbove) {
const widgetHeight = this.#domNode.offsetHeight;
if (this._anchorAbove) {
const widgetHeight = this._domNode.offsetHeight;
adjustedTop = top - widgetHeight;
} else {
adjustedTop = top + lineHeight;
}
// Clamp to viewport bounds when anchor line is out of view
const stickyScrollHeight = this.#stickyScrollHeight.get();
const stickyScrollHeight = this._stickyScrollHeight.get();
const layoutInfo = editor.getLayoutInfo();
const widgetHeight = this.#domNode.offsetHeight;
const widgetHeight = this._domNode.offsetHeight;
const minTop = stickyScrollHeight;
const maxTop = layoutInfo.height - widgetHeight;
const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop));
const isClamped = clampedTop !== adjustedTop;
this.#domNode.classList.toggle('clamped', isClamped);
this._domNode.classList.toggle('clamped', isClamped);
this.#position.set({
preference: { top: clampedTop, left: this.#anchorLeft },
this._position.set({
preference: { top: clampedTop, left: this._anchorLeft },
stackOrdinal: 10000,
}, undefined);
}
@@ -345,13 +338,13 @@ export class InlineChatInputWidget extends Disposable {
*/
hide(): void {
// Focus editor if focus is still within the editor's DOM
const editorDomNode = this.#editorObs.editor.getDomNode();
const editorDomNode = this._editorObs.editor.getDomNode();
if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) {
this.#editorObs.editor.focus();
this._editorObs.editor.focus();
}
this.#position.set(null, undefined);
this.#input.getModel().setValue('');
this.#showStore.clear();
this._position.set(null, undefined);
this._input.getModel().setValue('');
this._showStore.clear();
}
}
@@ -360,62 +353,52 @@ export class InlineChatInputWidget extends Disposable {
*/
export class InlineChatSessionOverlayWidget extends Disposable {
readonly #domNode: HTMLElement = document.createElement('div');
readonly #container: HTMLElement;
readonly #statusNode: HTMLElement;
readonly #icon: HTMLElement;
readonly #message: HTMLElement;
readonly #toolbarNode: HTMLElement;
private readonly _domNode: HTMLElement = document.createElement('div');
private readonly _container: HTMLElement;
private readonly _statusNode: HTMLElement;
private readonly _icon: HTMLElement;
private readonly _message: HTMLElement;
private readonly _toolbarNode: HTMLElement;
readonly #showStore = this._store.add(new DisposableStore());
readonly #position = observableValue<IOverlayWidgetPosition | null>(this, null);
readonly #minContentWidthInPx = constObservable(0);
private readonly _showStore = this._store.add(new DisposableStore());
private readonly _position = observableValue<IOverlayWidgetPosition | null>(this, null);
private readonly _minContentWidthInPx = constObservable(0);
readonly #stickyScrollHeight: IObservable<number>;
readonly #editorObs: ObservableCodeEditor;
readonly #instaService: IInstantiationService;
readonly #keybindingService: IKeybindingService;
readonly #logService: ILogService;
private readonly _stickyScrollHeight: IObservable<number>;
constructor(
editorObs: ObservableCodeEditor,
@IInstantiationService instaService: IInstantiationService,
@IKeybindingService keybindingService: IKeybindingService,
@ILogService logService: ILogService,
private readonly _editorObs: ObservableCodeEditor,
@IInstantiationService private readonly _instaService: IInstantiationService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@ILogService private readonly _logService: ILogService,
) {
super();
this.#editorObs = editorObs;
this.#instaService = instaService;
this.#keybindingService = keybindingService;
this.#logService = logService;
this._domNode.classList.add('inline-chat-session-overlay-widget');
this.#domNode.classList.add('inline-chat-session-overlay-widget');
this.#container = document.createElement('div');
this.#domNode.appendChild(this.#container);
this.#container.classList.add('inline-chat-session-overlay-container');
this._container = document.createElement('div');
this._domNode.appendChild(this._container);
this._container.classList.add('inline-chat-session-overlay-container');
// Create status node with icon and message
this.#statusNode = document.createElement('div');
this.#statusNode.classList.add('status');
this.#icon = dom.append(this.#statusNode, dom.$('span'));
this.#message = dom.append(this.#statusNode, dom.$('span.message'));
this.#container.appendChild(this.#statusNode);
this._statusNode = document.createElement('div');
this._statusNode.classList.add('status');
this._icon = dom.append(this._statusNode, dom.$('span'));
this._message = dom.append(this._statusNode, dom.$('span.message'));
this._container.appendChild(this._statusNode);
// Create toolbar node
this.#toolbarNode = document.createElement('div');
this.#toolbarNode.classList.add('toolbar');
this._toolbarNode = document.createElement('div');
this._toolbarNode.classList.add('toolbar');
// Initialize sticky scroll height observable
const stickyScrollController = StickyScrollController.get(this.#editorObs.editor);
this.#stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
const stickyScrollController = StickyScrollController.get(this._editorObs.editor);
this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0);
}
show(session: IInlineChatSession2): void {
assertType(this.#editorObs.editor.hasModel());
this.#showStore.clear();
assertType(this._editorObs.editor.hasModel());
this._showStore.clear();
// Derived entry observable for this session
const entry = derived(r => session.editingSession.readEntry(session.uri, r));
@@ -475,34 +458,34 @@ export class InlineChatSessionOverlayWidget extends Disposable {
}
});
this.#showStore.add(autorun(r => {
this._showStore.add(autorun(r => {
const value = requestMessage.read(r);
if (value) {
this.#message.innerText = renderAsPlaintext(value.message);
this.#icon.className = '';
this.#icon.classList.add(...ThemeIcon.asClassNameArray(value.icon));
this._message.innerText = renderAsPlaintext(value.message);
this._icon.className = '';
this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon));
} else {
this.#message.innerText = '';
this.#icon.className = '';
this._message.innerText = '';
this._icon.className = '';
}
}));
// Log when pending confirmation changes
this.#showStore.add(autorun(r => {
this._showStore.add(autorun(r => {
const response = session.chatModel.lastRequestObs.read(r)?.response;
const pending = response?.isPendingConfirmation.read(r);
if (pending) {
this.#logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`);
this._logService.info(`[InlineChat] UNEXPECTED approval needed: ${pending.detail ?? 'unknown'}`);
}
}));
// Add toolbar
this.#container.appendChild(this.#toolbarNode);
this.#showStore.add(toDisposable(() => this.#toolbarNode.remove()));
this._container.appendChild(this._toolbarNode);
this._showStore.add(toDisposable(() => this._toolbarNode.remove()));
const that = this;
this.#showStore.add(this.#instaService.createInstance(MenuWorkbenchToolBar, this.#toolbarNode, MenuId.ChatEditorInlineExecute, {
this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditorInlineExecute, {
telemetrySource: 'inlineChatProgress.overlayToolbar',
hiddenItemStrategy: HiddenItemStrategy.Ignore,
toolbarOptions: {
@@ -518,52 +501,52 @@ export class InlineChatSessionOverlayWidget extends Disposable {
return undefined; // use default action view item with label
}
return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that.#keybindingService, primaryActions);
return new ChatEditingAcceptRejectActionViewItem(action, { ...options, keybinding: undefined }, entry, undefined, that._keybindingService, primaryActions);
}
}));
// Position in top right of editor, below sticky scroll
const lineHeight = this.#editorObs.getOption(EditorOption.lineHeight);
const lineHeight = this._editorObs.getOption(EditorOption.lineHeight);
// Track widget width changes
const widgetWidth = observableValue<number>(this, 0);
const resizeObserver = new dom.DisposableResizeObserver(() => {
widgetWidth.set(this.#domNode.offsetWidth, undefined);
widgetWidth.set(this._domNode.offsetWidth, undefined);
});
this.#showStore.add(resizeObserver);
this.#showStore.add(resizeObserver.observe(this.#domNode));
this._showStore.add(resizeObserver);
this._showStore.add(resizeObserver.observe(this._domNode));
this.#showStore.add(autorun(r => {
const layoutInfo = this.#editorObs.layoutInfo.read(r);
const stickyScrollHeight = this.#stickyScrollHeight.read(r);
this._showStore.add(autorun(r => {
const layoutInfo = this._editorObs.layoutInfo.read(r);
const stickyScrollHeight = this._stickyScrollHeight.read(r);
const width = widgetWidth.read(r);
const padding = Math.round(lineHeight.read(r) * 2 / 3);
// Cap max-width to the editor viewport (content area)
const maxWidth = layoutInfo.contentWidth - 2 * padding;
this.#domNode.style.maxWidth = `${maxWidth}px`;
this._domNode.style.maxWidth = `${maxWidth}px`;
// Position: top right, below sticky scroll with padding, left of minimap and scrollbar
const top = stickyScrollHeight + padding;
const left = layoutInfo.width - width - layoutInfo.verticalScrollbarWidth - layoutInfo.minimap.minimapWidth - padding;
this.#position.set({
this._position.set({
preference: { top, left },
stackOrdinal: 10000,
}, undefined);
}));
// Create overlay widget
this.#showStore.add(this.#editorObs.createOverlayWidget({
domNode: this.#domNode,
position: this.#position,
minContentWidthInPx: this.#minContentWidthInPx,
this._showStore.add(this._editorObs.createOverlayWidget({
domNode: this._domNode,
position: this._position,
minContentWidthInPx: this._minContentWidthInPx,
allowEditorOverflow: false,
}));
}
hide(): void {
this.#position.set(null, undefined);
this.#showStore.clear();
this._position.set(null, undefined);
this._showStore.clear();
}
}

View File

@@ -41,58 +41,55 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
declare _serviceBrand: undefined;
readonly #store = new DisposableStore();
readonly #sessions = new ResourceMap<IInlineChatSession2>();
private readonly _store = new DisposableStore();
private readonly _sessions = new ResourceMap<IInlineChatSession2>();
readonly #onWillStartSession = this.#store.add(new Emitter<IActiveCodeEditor>());
readonly onWillStartSession: Event<IActiveCodeEditor> = this.#onWillStartSession.event;
private readonly _onWillStartSession = this._store.add(new Emitter<IActiveCodeEditor>());
readonly onWillStartSession: Event<IActiveCodeEditor> = this._onWillStartSession.event;
readonly #onDidChangeSessions = this.#store.add(new Emitter<this>());
readonly onDidChangeSessions: Event<this> = this.#onDidChangeSessions.event;
readonly #chatService: IChatService;
private readonly _onDidChangeSessions = this._store.add(new Emitter<this>());
readonly onDidChangeSessions: Event<this> = this._onDidChangeSessions.event;
constructor(
@IChatService chatService: IChatService,
@IChatService private readonly _chatService: IChatService,
@IChatAgentService chatAgentService: IChatAgentService,
) {
this.#chatService = chatService;
// Listen for agent changes and dispose all sessions when there is no agent
const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline));
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const agent = agentObs.read(r);
if (!agent) {
// No agent available, dispose all sessions
dispose(this.#sessions.values());
this.#sessions.clear();
dispose(this._sessions.values());
this._sessions.clear();
}
}));
}
dispose() {
this.#store.dispose();
this._store.dispose();
}
createSession(editor: IActiveCodeEditor): IInlineChatSession2 {
const uri = editor.getModel().uri;
if (this.#sessions.has(uri)) {
if (this._sessions.has(uri)) {
throw new Error('Session already exists');
}
this.#onWillStartSession.fire(editor);
this._onWillStartSession.fire(editor);
const chatModelRef = this.#chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ });
const chatModelRef = this._chatService.startNewLocalSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ });
const chatModel = chatModelRef.object;
chatModel.startEditingSession(false);
const store = new DisposableStore();
store.add(toDisposable(() => {
this.#chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession');
this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource, 'inlineChatSession');
chatModel.editingSession?.reject();
this.#sessions.delete(uri);
this.#onDidChangeSessions.fire(this);
this._sessions.delete(uri);
this._onDidChangeSessions.fire(this);
}));
store.add(chatModelRef);
@@ -107,7 +104,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) {
const response = chatModel.getRequests().at(-1)?.response;
if (response) {
this.#chatService.notifyUserAction({
this._chatService.notifyUserAction({
sessionResource: response.session.sessionResource,
requestId: response.requestId,
agentId: response.agent?.id,
@@ -141,16 +138,16 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
editingSession: chatModel.editingSession!,
dispose: store.dispose.bind(store)
};
this.#sessions.set(uri, result);
this.#onDidChangeSessions.fire(this);
this._sessions.set(uri, result);
this._onDidChangeSessions.fire(this);
return result;
}
getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined {
let result = this.#sessions.get(uri);
let result = this._sessions.get(uri);
if (!result) {
// no direct session, try to find an editing session which has a file entry for the uri
for (const [_, candidate] of this.#sessions) {
for (const [_, candidate] of this._sessions) {
const entry = candidate.editingSession.getEntry(uri);
if (entry) {
result = candidate;
@@ -162,7 +159,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
}
getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined {
for (const session of this.#sessions.values()) {
for (const session of this._sessions.values()) {
if (isEqual(session.chatModel.sessionResource, sessionResource)) {
return session;
}
@@ -175,11 +172,11 @@ export class InlineChatEnabler {
static Id = 'inlineChat.enabler';
readonly #ctxHasProvider2: IContextKey<boolean>;
readonly #ctxHasNotebookProvider: IContextKey<boolean>;
readonly #ctxPossible: IContextKey<boolean>;
private readonly _ctxHasProvider2: IContextKey<boolean>;
private readonly _ctxHasNotebookProvider: IContextKey<boolean>;
private readonly _ctxPossible: IContextKey<boolean>;
readonly #store = new DisposableStore();
private readonly _store = new DisposableStore();
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@@ -187,41 +184,41 @@ export class InlineChatEnabler {
@IEditorService editorService: IEditorService,
@IConfigurationService configService: IConfigurationService,
) {
this.#ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService);
this.#ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService);
this.#ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService);
this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService);
this._ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService);
this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService);
const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline));
const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook));
const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService);
this.#store.add(autorun(r => {
this._store.add(autorun(r => {
const agent = agentObs.read(r);
if (!agent) {
this.#ctxHasProvider2.reset();
this._ctxHasProvider2.reset();
} else {
this.#ctxHasProvider2.set(true);
this._ctxHasProvider2.set(true);
}
}));
this.#store.add(autorun(r => {
this.#ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r));
this._store.add(autorun(r => {
this._ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r));
}));
const updateEditor = () => {
const ctrl = editorService.activeEditorPane?.getControl();
const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl);
this.#ctxPossible.set(isCodeEditorLike);
this._ctxPossible.set(isCodeEditorLike);
};
this.#store.add(editorService.onDidActiveEditorChange(updateEditor));
this._store.add(editorService.onDidActiveEditorChange(updateEditor));
updateEditor();
}
dispose() {
this.#ctxPossible.reset();
this.#ctxHasProvider2.reset();
this.#store.dispose();
this._ctxPossible.reset();
this._ctxHasProvider2.reset();
this._store.dispose();
}
}
@@ -232,7 +229,7 @@ export class InlineChatEscapeToolContribution extends Disposable {
static readonly DONT_ASK_AGAIN_KEY = 'inlineChat.dontAskMoveToPanelChat';
static readonly #data: IToolData = {
private static readonly _data: IToolData = {
id: 'inline_chat_exit',
source: ToolDataSource.Internal,
canBeReferencedInPrompt: false,
@@ -254,7 +251,7 @@ export class InlineChatEscapeToolContribution extends Disposable {
super();
this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution.#data, {
this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution._data, {
invoke: async (invocation, _tokenCountFn, _progress, _token) => {
const sessionResource = invocation.context?.sessionResource;

View File

@@ -95,59 +95,37 @@ export class InlineChatWidget {
protected readonly _store = new DisposableStore();
readonly #ctxInputEditorFocused: IContextKey<boolean>;
readonly #ctxResponseFocused: IContextKey<boolean>;
private readonly _ctxInputEditorFocused: IContextKey<boolean>;
private readonly _ctxResponseFocused: IContextKey<boolean>;
readonly #chatWidget: ChatWidget;
private readonly _chatWidget: ChatWidget;
protected readonly _onDidChangeHeight = this._store.add(new Emitter<void>());
readonly onDidChangeHeight: Event<void> = Event.filter(this._onDidChangeHeight.event, _ => !this.#isLayouting);
readonly onDidChangeHeight: Event<void> = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting);
readonly #requestInProgress = observableValue(this, false);
readonly requestInProgress: IObservable<boolean> = this.#requestInProgress;
private readonly _requestInProgress = observableValue(this, false);
readonly requestInProgress: IObservable<boolean> = this._requestInProgress;
#isLayouting: boolean = false;
private _isLayouting: boolean = false;
readonly scopedContextKeyService: IContextKeyService;
readonly #options: IInlineChatWidgetConstructionOptions;
readonly #contextKeyService: IContextKeyService;
readonly #keybindingService: IKeybindingService;
readonly #accessibilityService: IAccessibilityService;
readonly #configurationService: IConfigurationService;
readonly #accessibleViewService: IAccessibleViewService;
readonly #chatService: IChatService;
readonly #hoverService: IHoverService;
readonly #chatEntitlementService: IChatEntitlementService;
readonly #markdownRendererService: IMarkdownRendererService;
constructor(
location: IChatWidgetLocationOptions,
options: IInlineChatWidgetConstructionOptions,
private readonly _options: IInlineChatWidgetConstructionOptions,
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService,
@IAccessibilityService accessibilityService: IAccessibilityService,
@IConfigurationService configurationService: IConfigurationService,
@IAccessibleViewService accessibleViewService: IAccessibleViewService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService,
@ITextModelService protected readonly _textModelResolverService: ITextModelService,
@IChatService chatService: IChatService,
@IHoverService hoverService: IHoverService,
@IChatEntitlementService chatEntitlementService: IChatEntitlementService,
@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,
@IChatService private readonly _chatService: IChatService,
@IHoverService private readonly _hoverService: IHoverService,
@IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService,
@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,
) {
this.#options = options;
this.#contextKeyService = contextKeyService;
this.#keybindingService = keybindingService;
this.#accessibilityService = accessibilityService;
this.#configurationService = configurationService;
this.#accessibleViewService = accessibleViewService;
this.#chatService = chatService;
this.#hoverService = hoverService;
this.#chatEntitlementService = chatEntitlementService;
this.#markdownRendererService = markdownRendererService;
this.scopedContextKeyService = this._store.add(contextKeyService.createScoped(this._elements.chatWidget));
this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget));
const scopedInstaService = _instantiationService.createChild(
new ServiceCollection([
IContextKeyService,
@@ -156,7 +134,7 @@ export class InlineChatWidget {
this._store
);
this.#chatWidget = scopedInstaService.createInstance(
this._chatWidget = scopedInstaService.createInstance(
ChatWidget,
location,
{ isInlineChat: true },
@@ -176,14 +154,14 @@ export class InlineChatWidget {
if (emptyResponse) {
return false;
}
if (item.response.value.every(item => item.kind === 'textEditGroup' && this.#options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) {
if (item.response.value.every(item => item.kind === 'textEditGroup' && _options.chatWidgetViewOptions?.rendererOptions?.renderTextEditsAsSummary?.(item.uri))) {
return false;
}
return true;
},
dndContainer: this._elements.root,
defaultMode: ChatMode.Ask,
...this.#options.chatWidgetViewOptions
..._options.chatWidgetViewOptions
},
{
listForeground: inlineChatForeground,
@@ -193,11 +171,11 @@ export class InlineChatWidget {
resultEditorBackground: editorBackground
}
);
this._elements.root.classList.toggle('in-zone-widget', !!this.#options.inZoneWidget);
this.#chatWidget.render(this._elements.chatWidget);
this._elements.root.classList.toggle('in-zone-widget', !!_options.inZoneWidget);
this._chatWidget.render(this._elements.chatWidget);
this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground));
this.#chatWidget.setVisible(true);
this._store.add(this.#chatWidget);
this._chatWidget.setVisible(true);
this._store.add(this._chatWidget);
const ctxResponse = ChatContextKeys.isResponse.bindTo(this.scopedContextKeyService);
const ctxResponseVote = ChatContextKeys.responseVote.bindTo(this.scopedContextKeyService);
@@ -206,10 +184,10 @@ export class InlineChatWidget {
const ctxResponseErrorFiltered = ChatContextKeys.responseIsFiltered.bindTo(this.scopedContextKeyService);
const viewModelStore = this._store.add(new DisposableStore());
this._store.add(this.#chatWidget.onDidChangeViewModel(() => {
this._store.add(this._chatWidget.onDidChangeViewModel(() => {
viewModelStore.clear();
const viewModel = this.#chatWidget.viewModel;
const viewModel = this._chatWidget.viewModel;
if (!viewModel) {
return;
}
@@ -225,7 +203,7 @@ export class InlineChatWidget {
viewModelStore.add(viewModel.onDidChange(() => {
this.#requestInProgress.set(viewModel.model.requestInProgress.get(), undefined);
this._requestInProgress.set(viewModel.model.requestInProgress.get(), undefined);
const last = viewModel.getItems().at(-1);
toolbar2.context = last;
@@ -246,22 +224,22 @@ export class InlineChatWidget {
}));
// context keys
this.#ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this.#contextKeyService);
this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService);
const tracker = this._store.add(trackFocus(this.domNode));
this._store.add(tracker.onDidBlur(() => this.#ctxResponseFocused.set(false)));
this._store.add(tracker.onDidFocus(() => this.#ctxResponseFocused.set(true)));
this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false)));
this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true)));
this.#ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this.#contextKeyService);
this._store.add(this.#chatWidget.inputEditor.onDidFocusEditorWidget(() => this.#ctxInputEditorFocused.set(true)));
this._store.add(this.#chatWidget.inputEditor.onDidBlurEditorWidget(() => this.#ctxInputEditorFocused.set(false)));
this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService);
this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true)));
this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false)));
const statusMenuId = this.#options.statusMenuId instanceof MenuId ? this.#options.statusMenuId : this.#options.statusMenuId.menu;
const statusMenuId = _options.statusMenuId instanceof MenuId ? _options.statusMenuId : _options.statusMenuId.menu;
// BUTTON bar
const statusMenuOptions = this.#options.statusMenuId instanceof MenuId ? undefined : this.#options.statusMenuId.options;
const statusMenuOptions = _options.statusMenuId instanceof MenuId ? undefined : _options.statusMenuId.options;
const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.toolbar1, statusMenuId, {
toolbarOptions: { primaryGroup: '0_main' },
telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource,
telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource,
menuOptions: { renderShortTitle: true },
...statusMenuOptions,
});
@@ -269,8 +247,8 @@ export class InlineChatWidget {
this._store.add(statusButtonBar);
// secondary toolbar
const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, this.#options.secondaryMenuId ?? MenuId.for(''), {
telemetrySource: this.#options.chatWidgetViewOptions?.menus?.telemetrySource,
const toolbar2 = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.toolbar2, _options.secondaryMenuId ?? MenuId.for(''), {
telemetrySource: _options.chatWidgetViewOptions?.menus?.telemetrySource,
menuOptions: { renderShortTitle: true, shouldForwardArgs: true },
actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => {
if (action instanceof MenuItemAction && action.item.id === MarkUnhelpfulActionId) {
@@ -283,60 +261,60 @@ export class InlineChatWidget {
this._store.add(toolbar2);
this._store.add(this.#configurationService.onDidChangeConfiguration(e => {
this._store.add(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) {
this.#updateAriaLabel();
this._updateAriaLabel();
}
}));
this._elements.root.tabIndex = 0;
this._elements.statusLabel.tabIndex = 0;
this.#updateAriaLabel();
this.#setupDisclaimer();
this._updateAriaLabel();
this._setupDisclaimer();
this._store.add(this.#hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => {
this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => {
return this._elements.statusLabel.dataset['title'];
}));
this._store.add(this.#chatService.onDidPerformUserAction(e => {
if (isEqual(e.sessionResource, this.#chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') {
this._store.add(this._chatService.onDidPerformUserAction(e => {
if (isEqual(e.sessionResource, this._chatWidget.viewModel?.model.sessionResource) && e.action.kind === 'vote') {
this.updateStatus(localize('feedbackThanks', "Thank you for your feedback!"), { resetAfter: 1250 });
}
}));
}
#updateAriaLabel(): void {
private _updateAriaLabel(): void {
this._elements.root.ariaLabel = this.#accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
this._elements.root.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
if (this.#accessibilityService.isScreenReaderOptimized()) {
if (this._accessibilityService.isScreenReaderOptimized()) {
let label = defaultAriaLabel;
if (this.#configurationService.getValue<boolean>(AccessibilityVerbositySettingId.InlineChat)) {
const kbLabel = this.#keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
if (this._configurationService.getValue<boolean>(AccessibilityVerbositySettingId.InlineChat)) {
const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
label = kbLabel
? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel)
: localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information.");
}
this.#chatWidget.inputEditor.updateOptions({ ariaLabel: label });
this._chatWidget.inputEditor.updateOptions({ ariaLabel: label });
}
}
#setupDisclaimer(): void {
private _setupDisclaimer(): void {
const disposables = this._store.add(new DisposableStore());
this._store.add(autorun(reader => {
disposables.clear();
reset(this._elements.disclaimerLabel);
const sentiment = this.#chatEntitlementService.sentimentObs.read(reader);
const anonymous = this.#chatEntitlementService.anonymousObs.read(reader);
const requestInProgress = this.#chatService.requestInProgressObs.read(reader);
const sentiment = this._chatEntitlementService.sentimentObs.read(reader);
const anonymous = this._chatEntitlementService.anonymousObs.read(reader);
const requestInProgress = this._chatService.requestInProgressObs.read(reader);
const showDisclaimer = !sentiment.installed && anonymous && !requestInProgress;
this._elements.disclaimerLabel.classList.toggle('hidden', !showDisclaimer);
if (showDisclaimer) {
const renderedMarkdown = disposables.add(this.#markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true })));
const renderedMarkdown = disposables.add(this._markdownRendererService.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true })));
this._elements.disclaimerLabel.appendChild(renderedMarkdown.element);
}
@@ -353,20 +331,20 @@ export class InlineChatWidget {
}
get chatWidget(): ChatWidget {
return this.#chatWidget;
return this._chatWidget;
}
saveState() {
this.#chatWidget.saveState();
this._chatWidget.saveState();
}
layout(widgetDim: Dimension) {
const contentHeight = this.contentHeight;
this.#isLayouting = true;
this._isLayouting = true;
try {
this._doLayout(widgetDim);
} finally {
this.#isLayouting = false;
this._isLayouting = false;
if (this.contentHeight !== contentHeight) {
this._onDidChangeHeight.fire();
@@ -383,7 +361,7 @@ export class InlineChatWidget {
this._elements.root.style.height = `${dimension.height - extraHeight}px`;
this._elements.root.style.width = `${dimension.width}px`;
this.#chatWidget.layout(
this._chatWidget.layout(
dimension.height - statusHeight - extraHeight,
dimension.width
);
@@ -394,7 +372,7 @@ export class InlineChatWidget {
*/
get contentHeight(): number {
const data = {
chatWidgetContentHeight: this.#chatWidget.contentHeight,
chatWidgetContentHeight: this._chatWidget.contentHeight,
statusHeight: getTotalHeight(this._elements.status),
extraHeight: this._getExtraHeight()
};
@@ -407,7 +385,7 @@ export class InlineChatWidget {
// at least "maxWidgetHeight" high and at most the content height.
let maxWidgetOutputHeight = 100;
for (const item of this.#chatWidget.viewModel?.getItems() ?? []) {
for (const item of this._chatWidget.viewModel?.getItems() ?? []) {
if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) {
maxWidgetOutputHeight = 270;
break;
@@ -415,29 +393,29 @@ export class InlineChatWidget {
}
let value = this.contentHeight;
value -= this.#chatWidget.contentHeight;
value += Math.min(this.#chatWidget.input.height.get() + maxWidgetOutputHeight, this.#chatWidget.contentHeight);
value -= this._chatWidget.contentHeight;
value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight);
return value;
}
protected _getExtraHeight(): number {
return this.#options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/);
return this._options.inZoneWidget ? 1 : (2 /*border*/ + 4 /*shadow*/);
}
get value(): string {
return this.#chatWidget.getInput();
return this._chatWidget.getInput();
}
set value(value: string) {
this.#chatWidget.setInput(value);
this._chatWidget.setInput(value);
}
selectAll() {
this.#chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
}
set placeholder(value: string) {
this.#chatWidget.setInputPlaceholder(value);
this._chatWidget.setInputPlaceholder(value);
}
toggleStatus(show: boolean) {
@@ -458,7 +436,7 @@ export class InlineChatWidget {
}
async getCodeBlockInfo(codeBlockIndex: number): Promise<ITextModel | undefined> {
const { viewModel } = this.#chatWidget;
const { viewModel } = this._chatWidget;
if (!viewModel) {
return undefined;
}
@@ -505,18 +483,18 @@ export class InlineChatWidget {
}
get responseContent(): string | undefined {
const requests = this.#chatWidget.viewModel?.model.getRequests();
const requests = this._chatWidget.viewModel?.model.getRequests();
return requests?.at(-1)?.response?.response.toString();
}
getChatModel(): IChatModel | undefined {
return this.#chatWidget.viewModel?.model;
return this._chatWidget.viewModel?.model;
}
setChatModel(chatModel: IChatModel) {
chatModel.inputModel.setState({ inputText: '', selections: [] });
this.#chatWidget.setModel(chatModel);
this._chatWidget.setModel(chatModel);
}
updateInfo(message: string): void {
@@ -555,8 +533,8 @@ export class InlineChatWidget {
}
reset() {
this.#chatWidget.attachmentModel.clear(true);
this.#chatWidget.saveState();
this._chatWidget.attachmentModel.clear(true);
this._chatWidget.saveState();
reset(this._elements.statusLabel);
this._elements.statusLabel.classList.toggle('hidden', true);
@@ -569,7 +547,7 @@ export class InlineChatWidget {
}
focus() {
this.#chatWidget.focusInput();
this._chatWidget.focusInput();
}
hasFocus() {

View File

@@ -28,7 +28,7 @@ import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';
export class InlineChatZoneWidget extends ZoneWidget {
static readonly #options: IOptions = {
private static readonly _options: IOptions = {
showFrame: true,
frameWidth: 1,
// frameColor: 'var(--vscode-inlineChat-border)',
@@ -43,12 +43,9 @@ export class InlineChatZoneWidget extends ZoneWidget {
readonly widget: EditorBasedInlineChatWidget;
readonly #ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
#dimension?: Dimension;
#notebookEditor?: INotebookEditor;
readonly #instaService: IInstantiationService;
#logService: ILogService;
private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
private _dimension?: Dimension;
private notebookEditor?: INotebookEditor;
constructor(
location: IChatWidgetLocationOptions,
@@ -56,22 +53,20 @@ export class InlineChatZoneWidget extends ZoneWidget {
editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor },
/** @deprecated should go away with inline2 */
clearDelegate: () => Promise<void>,
@IInstantiationService instaService: IInstantiationService,
@ILogService logService: ILogService,
@IInstantiationService private readonly _instaService: IInstantiationService,
@ILogService private _logService: ILogService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super(editors.editor, InlineChatZoneWidget.#options);
this.#instaService = instaService;
this.#logService = logService;
this.#notebookEditor = editors.notebookEditor;
super(editors.editor, InlineChatZoneWidget._options);
this.notebookEditor = editors.notebookEditor;
this.#ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
this._disposables.add(toDisposable(() => {
this.#ctxCursorPosition.reset();
this._ctxCursorPosition.reset();
}));
this.widget = this.#instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {
this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {
statusMenuId: {
menu: MENU_INLINE_CHAT_WIDGET_STATUS,
options: {
@@ -110,14 +105,14 @@ export class InlineChatZoneWidget extends ZoneWidget {
let revealFn: (() => void) | undefined;
this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => {
if (this.position) {
revealFn = this.#createZoneAndScrollRestoreFn(this.position);
revealFn = this._createZoneAndScrollRestoreFn(this.position);
}
}));
this._disposables.add(this.widget.onDidChangeHeight(() => {
if (this.position && !this._usesResizeHeight) {
// only relayout when visible
revealFn ??= this.#createZoneAndScrollRestoreFn(this.position);
const height = this.#computeHeight();
revealFn ??= this._createZoneAndScrollRestoreFn(this.position);
const height = this._computeHeight();
this._relayout(height.linesValue);
revealFn?.();
revealFn = undefined;
@@ -141,13 +136,13 @@ export class InlineChatZoneWidget extends ZoneWidget {
// todo@jrieken listen ONLY when showing
const updateCursorIsAboveContextKey = () => {
if (!this.position || !this.editor.hasModel()) {
this.#ctxCursorPosition.reset();
this._ctxCursorPosition.reset();
} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {
this.#ctxCursorPosition.set('above');
this._ctxCursorPosition.set('above');
} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {
this.#ctxCursorPosition.set('below');
this._ctxCursorPosition.set('below');
} else {
this.#ctxCursorPosition.reset();
this._ctxCursorPosition.reset();
}
};
this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));
@@ -164,19 +159,19 @@ export class InlineChatZoneWidget extends ZoneWidget {
protected override _doLayout(heightInPixel: number): void {
this.#updatePadding();
this._updatePadding();
const info = this.editor.getLayoutInfo();
const width = info.contentWidth - info.verticalScrollbarWidth;
// width = Math.min(850, width);
this.#dimension = new Dimension(width, heightInPixel);
this.widget.layout(this.#dimension);
this._dimension = new Dimension(width, heightInPixel);
this.widget.layout(this._dimension);
}
#computeHeight(): { linesValue: number; pixelsValue: number } {
private _computeHeight(): { linesValue: number; pixelsValue: number } {
const chatContentHeight = this.widget.contentHeight;
const editorHeight = this.#notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height;
const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height;
const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42));
const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight);
@@ -197,25 +192,25 @@ export class InlineChatZoneWidget extends ZoneWidget {
}
protected override _onWidth(_widthInPixel: number): void {
if (this.#dimension) {
this._doLayout(this.#dimension.height);
if (this._dimension) {
this._doLayout(this._dimension.height);
}
}
override show(position: Position): void {
assertType(this.container);
this.#updatePadding();
this._updatePadding();
const revealZone = this.#createZoneAndScrollRestoreFn(position);
super.show(position, this.#computeHeight().linesValue);
const revealZone = this._createZoneAndScrollRestoreFn(position);
super.show(position, this._computeHeight().linesValue);
this.widget.chatWidget.setVisible(true);
this.widget.focus();
revealZone();
}
#updatePadding() {
private _updatePadding() {
assertType(this.container);
const info = this.editor.getLayoutInfo();
@@ -231,12 +226,12 @@ export class InlineChatZoneWidget extends ZoneWidget {
}
override updatePositionAndHeight(position: Position): void {
const revealZone = this.#createZoneAndScrollRestoreFn(position);
super.updatePositionAndHeight(position, !this._usesResizeHeight ? this.#computeHeight().linesValue : undefined);
const revealZone = this._createZoneAndScrollRestoreFn(position);
super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined);
revealZone();
}
#createZoneAndScrollRestoreFn(position: Position): () => void {
private _createZoneAndScrollRestoreFn(position: Position): () => void {
const scrollState = StableEditorBottomScrollState.capture(this.editor);
@@ -247,7 +242,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
const scrollTop = this.editor.getScrollTop();
const lineTop = this.editor.getTopForLineNumber(lineNumber);
const zoneTop = lineTop - this.#computeHeight().pixelsValue;
const zoneTop = lineTop - this._computeHeight().pixelsValue;
const editorHeight = this.editor.getLayoutInfo().height;
const lineBottom = this.editor.getBottomForLineNumber(lineNumber);
@@ -262,7 +257,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
}
if (newScrollTop < scrollTop || forceScrollTop) {
this.#logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });
this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });
this.editor.setScrollTop(newScrollTop, ScrollType.Immediate);
}
};
@@ -274,7 +269,7 @@ export class InlineChatZoneWidget extends ZoneWidget {
override hide(): void {
const scrollState = StableEditorBottomScrollState.capture(this.editor);
this.#ctxCursorPosition.reset();
this._ctxCursorPosition.reset();
this.widget.chatWidget.setVisible(false);
super.hide();
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));

View File

@@ -20,49 +20,47 @@ import { DisposableStore, IDisposable } from '../../../../../base/common/lifecyc
export class TestWorkerService extends mock<IEditorWorkerService>() implements IDisposable {
readonly #store = new DisposableStore();
readonly #worker = this.#store.add(new EditorWorker());
readonly #modelService: IModelService;
private readonly _store = new DisposableStore();
private readonly _worker = this._store.add(new EditorWorker());
constructor(@IModelService modelService: IModelService) {
constructor(@IModelService private readonly _modelService: IModelService) {
super();
this.#modelService = modelService;
}
dispose(): void {
this.#store.dispose();
this._store.dispose();
}
override async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined, pretty?: boolean | undefined): Promise<TextEdit[] | undefined> {
return undefined;
}
override async computeDiff(original: URI, modified: URI, options: IDocumentDiffProviderOptions, algorithm: DiffAlgorithmName): Promise<IDocumentDiff | null> {
await new Promise<void>(resolve => disposableTimeout(() => resolve(), 0, this.#store));
if (this.#store.isDisposed) {
await new Promise<void>(resolve => disposableTimeout(() => resolve(), 0, this._store));
if (this._store.isDisposed) {
return null;
}
const originalModel = this.#modelService.getModel(original);
const modifiedModel = this.#modelService.getModel(modified);
const originalModel = this._modelService.getModel(original);
const modifiedModel = this._modelService.getModel(modified);
assertType(originalModel);
assertType(modifiedModel);
this.#worker.$acceptNewModel({
this._worker.$acceptNewModel({
url: originalModel.uri.toString(),
versionId: originalModel.getVersionId(),
lines: originalModel.getLinesContent(),
EOL: originalModel.getEOL(),
});
this.#worker.$acceptNewModel({
this._worker.$acceptNewModel({
url: modifiedModel.uri.toString(),
versionId: modifiedModel.getVersionId(),
lines: modifiedModel.getLinesContent(),
EOL: modifiedModel.getEOL(),
});
const result = await this.#worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm);
const result = await this._worker.$computeDiff(originalModel.uri.toString(), modifiedModel.uri.toString(), options, algorithm);
if (!result) {
return result;
}