Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve accessibility of inline completions #190531

Merged
merged 12 commits into from
Aug 16, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { alert } from 'vs/base/browser/ui/aria/aria';
import { alert, status } from 'vs/base/browser/ui/aria/aria';
import { Event } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { ITransaction, autorun, constObservable, disposableObservableValue, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable';
Expand All @@ -22,11 +22,13 @@ import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions
import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget';
import { InlineCompletionsModel, VersionIdChangeReason } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel';
import { SuggestWidgetAdaptor } from 'vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider';
import { localize } from 'vs/nls';
import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';

export class InlineCompletionsController extends Disposable {
static ID = 'editor.contrib.inlineCompletionsController';
Expand Down Expand Up @@ -73,6 +75,7 @@ export class InlineCompletionsController extends Disposable {
@ILanguageFeatureDebounceService private readonly debounceService: ILanguageFeatureDebounceService,
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IAudioCueService private readonly audioCueService: IAudioCueService,
@IKeybindingService private readonly _keybindingService: IKeybindingService
) {
super();

Expand Down Expand Up @@ -191,6 +194,7 @@ export class InlineCompletionsController extends Disposable {
this.audioCueService.playAudioCue(AudioCue.inlineSuggestion).then(() => {
if (this.editor.getOption(EditorOption.screenReaderAnnounceInlineSuggestion)) {
alert(state.ghostText.renderForScreenReader(lineText));
this.provideScreenReaderHint();
}
});
}
Expand All @@ -199,6 +203,24 @@ export class InlineCompletionsController extends Disposable {
this._register(new InlineCompletionsHintsWidget(this.editor, this.model, this.instantiationService));
}

private provideScreenReaderHint(): void {
const showHoverKeybinding = this._keybindingService.lookupKeybinding('editor.action.showHover');
const accessibleViewKeybinding = this._keybindingService.lookupKeybinding('editor.action.accessibleView');
if (this.configurationService.getValue('accessibility.verbosity.inlineCompletions')) {
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
let hint: string | undefined;
if (showHoverKeybinding && accessibleViewKeybinding) {
hint = localize('showBothHints', "View more actions ({0}) or inspect this in the accessible view ({1})", showHoverKeybinding.getAriaLabel(), accessibleViewKeybinding.getAriaLabel());
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
} else if (showHoverKeybinding) {
hint = localize('showHoverHint', "View more actions ({0})", showHoverKeybinding.getAriaLabel());
} else if (accessibleViewKeybinding) {
hint = localize('showAccessibleViewHint', "Inspect this in the accessible view ({0})", accessibleViewKeybinding.getAriaLabel());
}
if (hint) {
status(hint);
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

/**
* Copies over the relevant state from the text model to observables.
* This solves all kind of eventing issues, as we make sure we always operate on the latest state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { Registry } from 'vs/platform/registry/common/platform';
import { IAccessibleViewService, AccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution';
import { EditorAccessibilityHelpContribution, HoverAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions';
import { EditorAccessibilityHelpContribution, HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions';

registerAccessibilityConfiguration();
registerSingleton(IAccessibleViewService, AccessibleViewService, InstantiationType.Delayed);
Expand All @@ -22,3 +22,4 @@ workbenchRegistry.registerWorkbenchContribution(UnfocusedViewDimmingContribution
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchContributionsRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually);
workbenchContributionsRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually);
workbenchContributionsRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const enum AccessibilityVerbositySettingId {
DiffEditor = 'accessibility.verbosity.diffEditor',
Chat = 'accessibility.verbosity.panelChat',
InlineChat = 'accessibility.verbosity.inlineChat',
InlineCompletions = 'accessibility.verbosity.inlineCompletions',
KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor',
Notebook = 'accessibility.verbosity.notebook',
Editor = 'accessibility.verbosity.editor',
Expand Down Expand Up @@ -55,6 +56,10 @@ const configuration: IConfigurationNode = {
description: localize('verbosity.interactiveEditor.description', 'Provide information about how to access the inline editor chat accessibility help menu and alert with hints which describe how to use the feature when the input is focused'),
...baseProperty
},
[AccessibilityVerbositySettingId.InlineCompletions]: {
description: localize('verbosity.inlineCompletions.description', 'Provide information about how to access the inline completions hover and accessible view'),
...baseProperty
},
[AccessibilityVerbositySettingId.KeybindingsEditor]: {
description: localize('verbosity.keybindingsEditor.description', 'Provide information about how to change a keybinding in the keybindings editor when a row is focused'),
...baseProperty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import { IAction } from 'vs/base/common/actions';
import { INotificationViewItem } from 'vs/workbench/common/notifications';
import { ThemeIcon } from 'vs/base/common/themables';
import { Codicon } from 'vs/base/common/codicons';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
import { InlineCompletionContextKeys } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionContextKeys';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';

export class EditorAccessibilityHelpContribution extends Disposable {
static ID: 'editorAccessibilityHelpContribution';
Expand Down Expand Up @@ -276,3 +279,44 @@ export function alertFocusChange(index: number | undefined, length: number | und
return;
}

export class InlineCompletionsAccessibleViewContribution extends Disposable {
static ID: 'inlineCompletionsAccessibleViewContribution';
private _options: IAccessibleViewOptions = {
ariaLabel: localize('inlineCompletionsAccessibleView', "Inline Completions Accessible View"), language: 'typescript', type: AccessibleViewType.View
};
constructor() {
super();
this._register(AccessibleViewAction.addImplementation(95, 'inline-completions', accessor => {
const accessibleViewService = accessor.get(IAccessibleViewService);
const codeEditorService = accessor.get(ICodeEditorService);
const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!editor) {
return false;
}
const model = InlineCompletionsController.get(editor)?.model.get();
const state = model?.state.get();
if (!state) {
return false;
}
const lineText = model?.textModel.getLineContent(state.ghostText.lineNumber);
if (!lineText) {
return false;
}
const content = InlineCompletionsController.get(editor)?.model.get()?.ghostText.get()?.renderForScreenReader(lineText);
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
if (!content) {
return false;
}
accessibleViewService.show({
verbositySettingKey: AccessibilityVerbositySettingId.InlineCompletions,
provideContent() { return content; },
onClose() {
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
editor.focus();
},
options: this._options
});
return true;
}, ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionVisible, EditorContextKeys.focus, EditorContextKeys.hasCodeActionsProvider)
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ class AccessibleView extends Disposable {
}
}

const fragment = message + provider.provideContent() + readMoreLink + disableHelpHint + localize('exit-tip', 'Exit this dialog via the Escape key.');
const fragment = message + provider.provideContent() + readMoreLink + disableHelpHint + localize('exit-tip', '\nExit this dialog via the Escape key.');

this._getTextModel(URI.from({ path: `accessible-view-${provider.verbositySettingKey}`, scheme: 'accessible-view', fragment })).then((model) => {
if (!model) {
Expand Down