diff --git a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.css b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.css index cd20cfb12f3..640909467f4 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.css +++ b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.css @@ -3,53 +3,57 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-diff-editor .diff-review-line-number { - text-align: right; - display: inline-block; - color: var(--vscode-editorLineNumber-foreground); -} - -.monaco-diff-editor .diff-review { - position: absolute; +.monaco-component.diff-review { user-select: none; -webkit-user-select: none; z-index: 99; } -.monaco-diff-editor .diff-review-summary { +.monaco-diff-editor .diff-review { + position: absolute; + +} + +.monaco-component.diff-review .diff-review-line-number { + text-align: right; + display: inline-block; + color: var(--vscode-editorLineNumber-foreground); +} + +.monaco-component.diff-review .diff-review-summary { padding-left: 10px; } -.monaco-diff-editor .diff-review-shadow { +.monaco-component.diff-review .diff-review-shadow { position: absolute; box-shadow: var(--vscode-scrollbar-shadow) 0 -6px 6px -6px inset; } -.monaco-diff-editor .diff-review-row { +.monaco-component.diff-review .diff-review-row { white-space: pre; } -.monaco-diff-editor .diff-review-table { +.monaco-component.diff-review .diff-review-table { display: table; min-width: 100%; } -.monaco-diff-editor .diff-review-row { +.monaco-component.diff-review .diff-review-row { display: table-row; width: 100%; } -.monaco-diff-editor .diff-review-spacer { +.monaco-component.diff-review .diff-review-spacer { display: inline-block; width: 10px; vertical-align: middle; } -.monaco-diff-editor .diff-review-spacer > .codicon { +.monaco-component.diff-review .diff-review-spacer > .codicon { font-size: 9px !important; } -.monaco-diff-editor .diff-review-actions { +.monaco-component.diff-review .diff-review-actions { display: inline-block; position: absolute; right: 10px; @@ -57,12 +61,12 @@ z-index: 100; } -.monaco-diff-editor .diff-review-actions .action-label { +.monaco-component.diff-review .diff-review-actions .action-label { width: 16px; height: 16px; margin: 2px 0; } -.monaco-diff-editor .revertButton { +.monaco-component.diff-review .revertButton { cursor: pointer; } diff --git a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts index 3d4e7ef3067..3c27606e5d6 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer.ts @@ -354,7 +354,7 @@ class View extends Disposable { super(); this.domNode = this._element; - this.domNode.className = 'diff-review monaco-editor-background'; + this.domNode.className = 'monaco-component diff-review monaco-editor-background'; const actionBarContainer = document.createElement('div'); actionBarContainer.className = 'diff-review-actions'; @@ -381,6 +381,12 @@ class View extends Disposable { this._scrollbar = this._register(new DomScrollableElement(this._content, {})); reset(this.domNode, this._scrollbar.getDomNode(), actionBarContainer); + this._register(autorun(r => { + this._height.read(r); + this._width.read(r); + this._scrollbar.scanDomNode(); + })); + this._register(toDisposable(() => { reset(this.domNode); })); this._register(applyStyle(this.domNode, { width: this._width, height: this._height })); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css index ee46f6df25e..7d299bbc1e5 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.css @@ -307,6 +307,7 @@ padding-top: 6px; } +.monaco-editor .inline-chat .diff-review.hidden, .monaco-editor .inline-chat .previewDiff.hidden, .monaco-editor .inline-chat .previewCreate.hidden, .monaco-editor .inline-chat .previewCreateTitle.hidden { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index ce432473cfb..39b4563509b 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -27,7 +27,6 @@ import { ProviderResult, TextEdit } from 'vs/editor/common/languages'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController'; import { localize } from 'vs/nls'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -145,7 +144,6 @@ export class InlineChatController implements IEditorContribution { @IConfigurationService private readonly _configurationService: IConfigurationService, @IDialogService private readonly _dialogService: IDialogService, @IContextKeyService contextKeyService: IContextKeyService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, @@ -235,13 +233,7 @@ export class InlineChatController implements IEditorContribution { } private _getMode(): EditMode { - const editMode = this._configurationService.inspect(InlineChatConfigKeys.Mode); - let editModeValue = editMode.value; - if (this._accessibilityService.isScreenReaderOptimized() && editModeValue === editMode.defaultValue) { - // By default, use preview mode for screen reader users - editModeValue = EditMode.Preview; - } - return editModeValue!; + return this._configurationService.getValue(InlineChatConfigKeys.Mode); } getWidgetPosition(): Position | undefined { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts index 78cfd2a3bc5..9659c4cdb28 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts @@ -36,6 +36,7 @@ import { HunkState } from './inlineChatSession'; import { assertType } from 'vs/base/common/types'; import { IModelService } from 'vs/editor/common/services/model'; import { performAsyncTextEdit, asProgressiveEdit } from './utils'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; export interface IEditObserver { start(): void; @@ -404,6 +405,8 @@ type HunkDisplayData = { toggleDiff?: () => any; remove(): void; move: (next: boolean) => void; + + hunk: HunkInformation; }; @@ -441,6 +444,7 @@ export class LiveStrategy extends EditModeStrategy { zone: InlineChatZoneWidget, @IContextKeyService contextKeyService: IContextKeyService, @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IInstantiationService protected readonly _instaService: IInstantiationService, ) { super(session, editor, zone); @@ -652,6 +656,7 @@ export class LiveStrategy extends EditModeStrategy { : zoneLineNumber - hunkRanges[0].endLineNumber; data = { + hunk: hunkData, decorationIds, viewZoneId: '', viewZone: viewZoneData, @@ -661,7 +666,7 @@ export class LiveStrategy extends EditModeStrategy { discardHunk, toggleDiff: !hunkData.isInsertion() ? toggleDiff : undefined, remove, - move + move, }; this._hunkDisplayData.set(hunkData, data); @@ -700,6 +705,10 @@ export class LiveStrategy extends EditModeStrategy { const remainingHunks = this._session.hunkData.pending; this._updateSummaryMessage(remainingHunks); + if (this._accessibilityService.isScreenReaderOptimized()) { + this._zone.widget.showAccessibleHunk(this._session, widgetData.hunk); + } + this._ctxCurrentChangeHasDiff.set(Boolean(widgetData.toggleDiff)); this.toggleDiff = widgetData.toggleDiff; this.acceptHunk = async () => widgetData!.acceptHunk(); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index ca909ba98e0..7fd28a442ac 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -12,6 +12,7 @@ import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { Lazy } from 'vs/base/common/lazy'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ISettableObservable, constObservable, derived, observableValue } from 'vs/base/common/observable'; import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./inlineChat'; @@ -19,11 +20,13 @@ import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfi import { IActiveCodeEditor, ICodeEditor, IDiffEditorConstructionOptions } from 'vs/editor/browser/editorBrowser'; import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; +import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer'; import { EmbeddedCodeEditorWidget, EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; -import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { EditorLayoutInfo, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; +import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon'; import { LanguageSelector } from 'vs/editor/common/languageSelector'; import { CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList, ProviderResult } from 'vs/editor/common/languages'; @@ -58,7 +61,7 @@ import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/cha import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { ExpansionState, HunkData } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; +import { ExpansionState, HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession'; import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils'; import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_VISIBLE, IInlineChatFollowup, IInlineChatSlashCommand, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; @@ -182,6 +185,7 @@ export class InlineChatWidget { h('div.label.status.hidden@statusLabel'), h('div.actions.hidden@feedbackToolbar'), ]), + h('div.accessibleViewer@accessibleViewer') ] ); @@ -204,6 +208,8 @@ export class InlineChatWidget { private readonly _previewDiffEditor: Lazy; private readonly _previewDiffModel = this._store.add(new MutableDisposable()); + private readonly _accessibleViewer = this._store.add(new MutableDisposable()); + private readonly _previewCreateTitle: ResourceLabel; private readonly _previewCreateEditor: Lazy; private readonly _previewCreateDispoable = this._store.add(new MutableDisposable()); @@ -467,6 +473,9 @@ export class InlineChatWidget { layout(_dim: Dimension) { this._isLayouting = true; try { + if (this._accessibleViewer.value) { + this._accessibleViewer.value.width = _dim.width - 12; + } const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar); const editorToolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */; const innerEditorWidth = _dim.width - editorToolbarWidth - widgetToolbarWidth; @@ -489,6 +498,7 @@ export class InlineChatWidget { this._elements.previewCreate.style.height = `${previewCreateDim.height}px`; } + const lineHeight = this.parentEditor.getOption(EditorOption.lineHeight); const editorHeight = this.parentEditor.getLayoutInfo().height; const editorHeightInLines = Math.floor(editorHeight / lineHeight); @@ -510,7 +520,8 @@ export class InlineChatWidget { const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0; const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle); const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0; - return base + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + 18 /* padding */ + 8 /*shadow*/; + const accessibleViewHeight = this._accessibleViewer.value?.height ?? 0; + return base + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + 18 /* padding */ + 8 /*shadow*/; } updateProgress(show: boolean) { @@ -735,6 +746,10 @@ export class InlineChatWidget { this.updateInfo(''); this.hideCreatePreview(); this.hideEditsPreview(); + + this._accessibleViewer.clear(); + this._elements.accessibleViewer.classList.toggle('hidden', true); + this._onDidChangeHeight.fire(); } @@ -908,6 +923,25 @@ export class InlineChatWidget { this._slashCommands.add(this._inputEditor.onDidChangeModelContent(updateSlashDecorations)); updateSlashDecorations(); } + + + // --- accessible viewer + + showAccessibleHunk(session: Session, hunkData: HunkInformation): void { + + this._elements.accessibleViewer.classList.remove('hidden'); + this._accessibleViewer.clear(); + + this._accessibleViewer.value = this._instantiationService.createInstance(HunkAccessibleDiffViewer, + this._elements.accessibleViewer, + session, + hunkData, + new AccessibleHunk(this.parentEditor, session, hunkData) + ); + + this._onDidChangeHeight.fire(); + + } } export class InlineChatZoneWidget extends ZoneWidget { @@ -1063,3 +1097,88 @@ export class InlineChatZoneWidget extends ZoneWidget { aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); } } + +class HunkAccessibleDiffViewer extends AccessibleDiffViewer { + + readonly height: number; + + set width(value: number) { + this._width2.set(value, undefined); + } + + private readonly _width2: ISettableObservable; + + constructor( + parentNode: HTMLElement, + session: Session, + hunk: HunkInformation, + models: IAccessibleDiffViewerModel, + @IInstantiationService instantiationService: IInstantiationService, + ) { + const width = observableValue('width', 0); + const diff = observableValue('diff', HunkAccessibleDiffViewer._asMapping(hunk)); + const diffs = derived(r => [diff.read(r)]); + const lines = Math.min(10, 8 + diff.get().changedLineCount); + const height = models.getModifiedOptions().get(EditorOption.lineHeight) * lines; + + super(parentNode, constObservable(true), () => { }, constObservable(false), width, constObservable(height), diffs, models, instantiationService); + + this.height = height; + this._width2 = width; + + this._store.add(session.textModelN.onDidChangeContent(() => { + diff.set(HunkAccessibleDiffViewer._asMapping(hunk), undefined); + })); + } + + private static _asMapping(hunk: HunkInformation): DetailedLineRangeMapping { + const ranges0 = hunk.getRanges0(); + const rangesN = hunk.getRangesN(); + const originalLineRange = LineRange.fromRangeInclusive(ranges0[0]); + const modifiedLineRange = LineRange.fromRangeInclusive(rangesN[0]); + const innerChanges: RangeMapping[] = []; + for (let i = 1; i < ranges0.length; i++) { + innerChanges.push(new RangeMapping(ranges0[i], rangesN[i])); + } + return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, innerChanges); + } + +} + +class AccessibleHunk implements IAccessibleDiffViewerModel { + + constructor( + private readonly _editor: ICodeEditor, + private readonly _session: Session, + private readonly _hunk: HunkInformation + ) { } + + getOriginalModel(): ITextModel { + return this._session.textModel0; + } + getModifiedModel(): ITextModel { + return this._session.textModelN; + } + getOriginalOptions(): IComputedEditorOptions { + return this._editor.getOptions(); + } + getModifiedOptions(): IComputedEditorOptions { + return this._editor.getOptions(); + } + originalReveal(range: Range): void { + // throw new Error('Method not implemented.'); + } + modifiedReveal(range?: Range | undefined): void { + this._editor.revealRangeInCenterIfOutsideViewport(range || this._hunk.getRangesN()[0], ScrollType.Smooth); + } + modifiedSetSelection(range: Range): void { + // this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth); + // this._editor.setSelection(range); + } + modifiedFocus(): void { + this._editor.focus(); + } + getModifiedPosition(): Position | undefined { + return this._hunk.getRangesN()[0].getStartPosition(); + } +}