Skip to content

Commit

Permalink
Make untitled editor hint accessible (#190214)
Browse files Browse the repository at this point in the history
  • Loading branch information
joyceerhl authored Aug 11, 2023
1 parent 52840e3 commit 8f9432b
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const enum AccessibilityVerbositySettingId {
Notebook = 'accessibility.verbosity.notebook',
Editor = 'accessibility.verbosity.editor',
Hover = 'accessibility.verbosity.hover',
Notification = 'accessibility.verbosity.notification'
Notification = 'accessibility.verbosity.notification',
EditorUntitledHint = 'accessibility.verbosity.editor.untitledHint'
}

const baseProperty: object = {
Expand Down Expand Up @@ -70,6 +71,10 @@ const configuration: IConfigurationNode = {
description: localize('verbosity.notification', 'Provide information about how to open the notification in an accessible view.'),
...baseProperty
},
[AccessibilityVerbositySettingId.EditorUntitledHint]: {
description: localize('verbosity.editor.untitledhint', 'Provide information about relevant actions in an untitled text editor.'),
...baseProperty
},
[AccessibilitySettingId.UnfocusedViewOpacity]: {
description: localize('unfocusedViewOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will dim inactive views to make the focused views more obvious.'),
type: 'number',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { Schemas } from 'vs/base/common/network';
import { Event } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions';
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
Expand All @@ -25,6 +26,10 @@ import { IInlineChatService, IInlineChatSessionProvider } from 'vs/workbench/con
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
import { IProductService } from 'vs/platform/product/common/productService';
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
import { OS } from 'vs/base/common/platform';
import { status } from 'vs/base/browser/ui/aria/aria';
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';

const $ = dom.$;

Expand Down Expand Up @@ -98,6 +103,8 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {

private domNode: HTMLElement | undefined;
private toDispose: DisposableStore;
private isVisible = false;
private ariaLabel: string = '';

constructor(
private readonly editor: ICodeEditor,
Expand All @@ -111,20 +118,28 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
) {
this.toDispose = new DisposableStore();
this.toDispose.add(this.inlineChatService.onDidChangeProviders(() => this.onDidChangeModelContent()));
this.toDispose.add(editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
this.toDispose.add(this.editor.onDidChangeModelContent(() => this.onDidChangeModelContent()));
this.toDispose.add(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
if (this.domNode && e.hasChanged(EditorOption.fontInfo)) {
this.editor.applyFontInfo(this.domNode);
}
}));
const onDidFocusEditorText = Event.debounce(this.editor.onDidFocusEditorText, () => undefined, 500);
this.toDispose.add(onDidFocusEditorText(() => {
if (this.editor.hasTextFocus() && this.isVisible && this.ariaLabel && this.configurationService.getValue(AccessibilityVerbositySettingId.EditorUntitledHint)) {
status(this.ariaLabel);
}
}));
this.onDidChangeModelContent();
}

private onDidChangeModelContent(): void {
if (this.editor.getValue() === '') {
this.editor.addContentWidget(this);
this.isVisible = true;
} else {
this.editor.removeContentWidget(this);
this.isVisible = false;
}
}

Expand All @@ -133,42 +148,79 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
}

private _getHintInlineChat(providers: IInlineChatSessionProvider[]) {
const providerName = providers.length === 1 ? providers[0].label : undefined;
const providerName = (providers.length === 1 ? providers[0].label : undefined) ?? this.productService.nameShort;

const hintMsg = localize({
key: 'inlineChatHint',
comment: [
'Preserve double-square brackets and their order',
]
}, '[[Ask {0} to do something]] or start typing to dismiss.', providerName ?? this.productService.nameShort);
const inlineChatId = 'inlineChat.start';
let ariaLabel = `Ask ${providerName} to do something or start typing to dismiss.`;

const handleClick = () => {
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: 'inlineChat.hintAction',
from: 'hint'
});
void this.commandService.executeCommand(inlineChatId, { from: 'hint' });
};

const hintHandler: IContentActionHandler = {
disposables: this.toDispose,
callback: (index, _event) => {
switch (index) {
case '0':
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', {
id: 'inlineChat.hintAction',
from: 'hint'
});
void this.commandService.executeCommand('inlineChat.start', { from: 'hint' });
handleClick();
break;
}
}
};

return { hintMsg, hintHandler, keybindingsLookup: ['inlineChat.start'] };
const hintElement = $('untitled-hint-text');
hintElement.style.display = 'block';

const keybindingHint = this.keybindingService.lookupKeybinding(inlineChatId);
const keybindingHintLabel = keybindingHint?.getLabel();

if (keybindingHint && keybindingHintLabel) {
const actionPart = localize('untitledText', 'Press {0} to ask {1} to do something. ', keybindingHintLabel, providerName);

const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => {
const hintPart = $('a', undefined, fragment);
hintPart.style.fontStyle = 'italic';
hintPart.style.cursor = 'pointer';
hintPart.onclick = handleClick;
return hintPart;
});

hintElement.appendChild(before);

const label = new KeybindingLabel(hintElement, OS);
label.set(keybindingHint);
label.element.style.width = 'min-content';
label.element.style.display = 'inline';
label.element.style.cursor = 'pointer';
label.element.onclick = handleClick;

hintElement.appendChild(after);

const typeToDismiss = localize('untitledText2', 'Start typing to dismiss.');
const textHint2 = $('span', undefined, typeToDismiss);
textHint2.style.fontStyle = 'italic';
hintElement.appendChild(textHint2);

ariaLabel = actionPart.concat(typeToDismiss);
} else {
const hintMsg = localize({
key: 'inlineChatHint',
comment: [
'Preserve double-square brackets and their order',
]
}, '[[Ask {0} to do something]] or start typing to dismiss.', providerName);
const rendered = renderFormattedText(hintMsg, { actionHandler: hintHandler });
hintElement.appendChild(rendered);
}

return { ariaLabel, hintHandler, hintElement };
}

private _getHintDefault() {
const hintMsg = localize({
key: 'message',
comment: [
'Preserve double-square brackets and their order',
'language refers to a programming language'
]
}, '[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.');

const hintHandler: IContentActionHandler = {
disposables: this.toDispose,
callback: (index, event) => {
Expand Down Expand Up @@ -234,37 +286,49 @@ class UntitledTextEditorHintContentWidget implements IContentWidget {
this.editor.focus();
};

return { hintMsg, hintHandler, keybindingsLookup: [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries'] };
const hintMsg = localize({
key: 'message',
comment: [
'Preserve double-square brackets and their order',
'language refers to a programming language'
]
}, '[[Select a language]], or [[fill with template]], or [[open a different editor]] to get started.\nStart typing to dismiss or [[don\'t show]] this again.');
const hintElement = renderFormattedText(hintMsg, {
actionHandler: hintHandler,
renderCodeSegments: false,
});
hintElement.style.fontStyle = 'italic';

// ugly way to associate keybindings...
const keybindingsLookup = [ChangeLanguageAction.ID, ApplyFileSnippetAction.Id, 'welcome.showNewFileEntries'];
const keybindingLabels = keybindingsLookup.map((id) => this.keybindingService.lookupKeybinding(id)?.getLabel() ?? id);
const ariaLabel = localize('defaultHintAriaLabel', 'Execute {0} to select a language, execute {1} to fill with template, or execute {2} to open a different editor and get started. Start typing to dismiss.', ...keybindingLabels);
for (const anchor of hintElement.querySelectorAll('a')) {
anchor.style.cursor = 'pointer';
const id = keybindingsLookup.shift();
const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel();
anchor.title = title ?? '';
}

return { hintElement, ariaLabel };
}

// Select a language to get started. Start typing to dismiss, or don't show this again.
getDomNode(): HTMLElement {
if (!this.domNode) {
this.domNode = $('.untitled-hint');
this.domNode.style.width = 'max-content';
this.domNode.style.paddingLeft = '4px';

const inlineChatProviders = [...this.inlineChatService.getAllProvider()];
const { hintMsg, hintHandler, keybindingsLookup } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders);
const hintElement = renderFormattedText(hintMsg, {
actionHandler: hintHandler,
renderCodeSegments: false,
});
const { hintElement, ariaLabel } = !inlineChatProviders.length ? this._getHintDefault() : this._getHintInlineChat(inlineChatProviders);
this.domNode.append(hintElement);

// ugly way to associate keybindings...
for (const anchor of hintElement.querySelectorAll('a')) {
anchor.style.cursor = 'pointer';
const id = keybindingsLookup.shift();
const title = id && this.keybindingService.lookupKeybinding(id)?.getLabel();
anchor.title = title ?? '';
}
this.ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.EditorUntitledHint));

this.toDispose.add(dom.addDisposableListener(this.domNode, 'click', () => {
this.editor.focus();
}));

this.domNode.style.fontStyle = 'italic';
this.domNode.style.paddingLeft = '4px';
this.editor.applyFontInfo(this.domNode);
}

Expand Down

0 comments on commit 8f9432b

Please sign in to comment.