Merge pull request #205514 from microsoft/joh/awful-quelea

Show accessible diff viewer in line chat widget
This commit is contained in:
Johannes Rieken
2024-02-19 13:14:18 +01:00
committed by GitHub
6 changed files with 163 additions and 32 deletions
@@ -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;
}
@@ -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 }));
@@ -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 {
@@ -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<EditMode>(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<EditMode>(InlineChatConfigKeys.Mode);
}
getWidgetPosition(): Position | undefined {
@@ -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();
@@ -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<EmbeddedDiffEditorWidget>;
private readonly _previewDiffModel = this._store.add(new MutableDisposable());
private readonly _accessibleViewer = this._store.add(new MutableDisposable<HunkAccessibleDiffViewer>());
private readonly _previewCreateTitle: ResourceLabel;
private readonly _previewCreateEditor: Lazy<ICodeEditor>;
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<number>;
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();
}
}