Merge pull request #129809 from microsoft/hediet/inline-suggestion-screen-reader

Improves inline suggestions to work with screenreaders.
This commit is contained in:
Henning Dieterichs 2021-08-02 09:49:40 +02:00 committed by GitHub
commit c42de908f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 72 additions and 14 deletions

View file

@ -28,9 +28,9 @@ export class GhostText {
this.parts.every((part, index) => part.equals(other.parts[index]));
}
render(text: string, debug: boolean = false): string {
render(documentText: string, debug: boolean = false): string {
const l = this.lineNumber;
return applyEdits(text,
return applyEdits(documentText,
[
...this.parts.map(p => ({
range: { startLineNumber: l, endLineNumber: l, startColumn: p.column, endColumn: p.column },
@ -39,6 +39,23 @@ export class GhostText {
]
);
}
renderForScreenReader(lineText: string): string {
if (this.parts.length === 0) {
return '';
}
const lastPart = this.parts[this.parts.length - 1];
const cappedLineText = lineText.substr(0, lastPart.column - 1);
const text = applyEdits(cappedLineText,
this.parts.map(p => ({
range: { startLineNumber: 1, endLineNumber: 1, startColumn: p.column, endColumn: p.column },
text: p.lines.join('\n')
}))
);
return text.substring(this.parts[0].column - 1);
}
}
class PositionOffsetTransformer {

View file

@ -15,6 +15,7 @@ import * as nls from 'vs/nls';
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { GhostTextModel } from 'vs/editor/contrib/inlineCompletions/ghostTextModel';
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
export class GhostTextController extends Disposable {
public static readonly inlineSuggestionVisible = new RawContextKey<boolean>('inlineSuggestionVisible', false, nls.localize('inlineSuggestionVisible', "Whether an inline suggestion is visible"));
@ -28,7 +29,7 @@ export class GhostTextController extends Disposable {
private triggeredExplicitly = false;
protected readonly activeController = this._register(new MutableDisposable<ActiveGhostTextController>());
private get activeModel(): GhostTextModel | undefined {
public get activeModel(): GhostTextModel | undefined {
return this.activeController.value?.model;
}
@ -168,21 +169,23 @@ const GhostTextCommand = EditorCommand.bindToContribution(GhostTextController.ge
export const commitInlineSuggestionAction = new GhostTextCommand({
id: 'editor.action.inlineSuggest.commit',
precondition: ContextKeyExpr.and(
GhostTextController.inlineSuggestionVisible,
GhostTextController.inlineSuggestionHasIndentation.toNegated(),
EditorContextKeys.tabMovesFocus.toNegated()
),
kbOpts: {
weight: 200,
primary: KeyCode.Tab,
},
precondition: GhostTextController.inlineSuggestionVisible,
handler(x) {
x.commit();
x.editor.focus();
}
});
registerEditorCommand(commitInlineSuggestionAction);
KeybindingsRegistry.registerKeybindingRule({
primary: KeyCode.Tab,
weight: 200,
id: commitInlineSuggestionAction.id,
when: ContextKeyExpr.and(
commitInlineSuggestionAction.precondition,
EditorContextKeys.tabMovesFocus.toNegated(),
GhostTextController.inlineSuggestionHasIndentation.toNegated()
),
});
registerEditorCommand(new GhostTextCommand({
id: 'editor.action.inlineSuggest.hide',

View file

@ -14,6 +14,12 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ITextContentData, IViewZoneData } from 'vs/editor/browser/controller/mouseTarget';
import * as dom from 'vs/base/browser/dom';
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
export class InlineCompletionsHover implements IHoverPart {
constructor(
@ -38,10 +44,13 @@ export class InlineCompletionsHover implements IHoverPart {
export class InlineCompletionsHoverParticipant implements IEditorHoverParticipant<InlineCompletionsHover> {
constructor(
private readonly _editor: ICodeEditor,
hover: IEditorHover,
private readonly _hover: IEditorHover,
@ICommandService private readonly _commandService: ICommandService,
@IMenuService private readonly _menuService: IMenuService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IModeService private readonly _modeService: IModeService,
@IOpenerService private readonly _openerService: IOpenerService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
) { }
suggestHoverAnchor(mouseEvent: IEditorMouseEvent): HoverAnchor | null {
@ -82,6 +91,11 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
renderHoverParts(hoverParts: InlineCompletionsHover[], fragment: DocumentFragment, statusBar: IEditorHoverStatusBar): IDisposable {
const disposableStore = new DisposableStore();
const part = hoverParts[0];
if (this.accessibilityService.isScreenReaderOptimized()) {
this.renderScreenReaderText(part, fragment, disposableStore);
}
const menu = disposableStore.add(this._menuService.createMenu(
MenuId.InlineCompletionsActions,
@ -108,7 +122,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
for (const action of actions) {
action.setEnabled(false);
}
hoverParts[0].hasMultipleSuggestions().then(hasMore => {
part.hasMultipleSuggestions().then(hasMore => {
for (const action of actions) {
action.setEnabled(hasMore);
}
@ -128,4 +142,28 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
return disposableStore;
}
private renderScreenReaderText(part: InlineCompletionsHover, fragment: DocumentFragment, disposableStore: DisposableStore) {
const $ = dom.$;
const markdownHoverElement = $('div.hover-row.markdown-hover');
const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents'));
const renderer = disposableStore.add(new MarkdownRenderer({ editor: this._editor }, this._modeService, this._openerService));
const render = (code: string) => {
disposableStore.add(renderer.onDidRenderAsync(() => {
hoverContentsElement.className = 'hover-contents code-hover-contents';
this._hover.onContentsChanged();
}));
const inlineSuggestionAvailable = nls.localize('inlineSuggestionFollows', "Inline Suggestion:");
const renderedContents = disposableStore.add(renderer.render(new MarkdownString().appendText(inlineSuggestionAvailable).appendCodeblock('text', code)));
hoverContentsElement.replaceChildren(renderedContents.element);
};
const ghostText = part.controller.activeModel?.inlineCompletionsModel?.ghostText;
if (ghostText) {
const lineText = this._editor.getModel()!.getLineContent(ghostText.lineNumber);
render(ghostText.renderForScreenReader(lineText));
}
fragment.appendChild(markdownHoverElement);
}
}