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

add comments accessible view #209977

Merged
merged 14 commits into from Apr 15, 2024
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 { HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions';
import { CommentAccessibleViewContribution, HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibleViewContributions';
import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus';
import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp';
import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibilitySignals/browser/saveAccessibilitySignal';
Expand All @@ -30,6 +30,7 @@ workbenchRegistry.registerWorkbenchContribution(UnfocusedViewDimmingContribution

workbenchRegistry.registerWorkbenchContribution(HoverAccessibleViewContribution, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContribution, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(CommentAccessibleViewContribution, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually);

registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore);
Expand Down
Expand Up @@ -30,8 +30,13 @@ 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';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { AccessibilitySignal, IAccessibilitySignalService } from 'vs/platform/accessibilitySignal/browser/accessibilitySignalService';
import { COMMENTS_VIEW_ID, CommentsMenus } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { CommentsPanel, CONTEXT_KEY_HAS_COMMENTS } from 'vs/workbench/contrib/comments/browser/commentsView';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { MarshalledId } from 'vs/base/common/marshallingIds';
import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController';

export function descriptionForCommand(commandId: string, msg: string, noKbMsg: string, keybindingService: IKeybindingService): string {
Expand Down Expand Up @@ -224,6 +229,77 @@ export function alertFocusChange(index: number | undefined, length: number | und
return;
}


export class CommentAccessibleViewContribution extends Disposable {
static ID: 'commentAccessibleViewContribution';
constructor() {
super();
this._register(AccessibleViewAction.addImplementation(90, 'comment', accessor => {
const accessibleViewService = accessor.get(IAccessibleViewService);
const contextKeyService = accessor.get(IContextKeyService);
const viewsService = accessor.get(IViewsService);
const menuService = accessor.get(IMenuService);
const commentsView = viewsService.getActiveViewWithId<CommentsPanel>(COMMENTS_VIEW_ID);
if (!commentsView) {
return false;
}
const menus = this._register(new CommentsMenus(menuService));
menus.setContextKeyService(contextKeyService);

function renderAccessibleView() {
if (!commentsView) {
return false;
}

const commentNode = commentsView?.focusedCommentNode;
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
const content = commentsView.focusedCommentInfo?.toString();
if (!commentNode || !content) {
return false;
}
const menuActions = [...menus.getResourceContextActions(commentNode)].filter(i => i.enabled);
const actions = menuActions.map(action => {
return {
...action,
run: () => {
commentsView.focus();
action.run({
thread: commentNode.thread,
$mid: MarshalledId.CommentThread,
commentControlHandle: commentNode.controllerHandle,
commentThreadHandle: commentNode.threadHandle,
});
}
};
});
accessibleViewService.show({
id: AccessibleViewProviderId.Notification,
provideContent: () => {
return content;
},
onClose(): void {
commentsView.focus();
},
next(): void {
commentsView.focus();
commentsView.focusNextNode();
renderAccessibleView();
},
previous(): void {
commentsView.focus();
commentsView.focusPreviousNode();
renderAccessibleView();
},
verbositySettingKey: AccessibilityVerbositySettingId.Comments,
options: { type: AccessibleViewType.View },
actions
});
return true;
}
return renderAccessibleView();
}, CONTEXT_KEY_HAS_COMMENTS));
}
}

export class InlineCompletionsAccessibleViewContribution extends Disposable {
static ID: 'inlineCompletionsAccessibleViewContribution';
private _options: IAccessibleViewOptions = { type: AccessibleViewType.View };
Expand Down
Expand Up @@ -25,6 +25,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/commentsController';
import { MarshalledCommentThreadInternal } from 'vs/workbench/common/comments';
import { accessibleViewCurrentProviderId, accessibleViewIsShown, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';

registerAction2(class Collapse extends ViewAction<CommentsPanel> {
constructor() {
Expand Down Expand Up @@ -74,11 +75,15 @@ registerAction2(class Reply extends Action2 {
id: 'comments.reply',
title: nls.localize('reply', "Reply"),
icon: Codicon.reply,
menu: {
precondition: ContextKeyExpr.equals('canReply', true),
menu: [{
id: MenuId.CommentsViewThreadActions,
order: 100,
when: ContextKeyExpr.equals('canReply', true)
order: 100
},
{
id: MenuId.AccessibleView,
when: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.Comments)),
}]
});
}

Expand Down
Expand Up @@ -135,7 +135,7 @@ export class ResourceWithCommentsRenderer implements IListRenderer<ITreeNode<Res
}
}

class CommentsMenus implements IDisposable {
export class CommentsMenus implements IDisposable {
private contextKeyService: IContextKeyService | undefined;

constructor(
Expand Down
105 changes: 88 additions & 17 deletions src/vs/workbench/contrib/comments/browser/commentsView.ts
Expand Up @@ -9,8 +9,8 @@ import * as dom from 'vs/base/browser/dom';
import { basename } from 'vs/base/common/resources';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { CommentNode, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel';
import { IWorkspaceCommentThreadsEvent, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
import { CommentNode, ICommentThreadChangedEvent, ResourceWithCommentThreads } from 'vs/workbench/contrib/comments/common/commentModel';
import { ICommentService, IWorkspaceCommentThreadsEvent } from 'vs/workbench/contrib/comments/browser/commentService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ResourceLabels } from 'vs/workbench/browser/labels';
import { CommentsList, COMMENTS_VIEW_TITLE, Filter } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
Expand All @@ -35,6 +35,8 @@ import { revealCommentThread } from 'vs/workbench/contrib/comments/browser/comme
import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands';
import { CommentsModel, ICommentsModel } from 'vs/workbench/contrib/comments/browser/commentsModel';
import { IHoverService } from 'vs/platform/hover/browser/hover';
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions';

export const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey<boolean>('commentsView.hasComments', false);
export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey<boolean>('commentsView.someCommentsExpanded', false);
Expand Down Expand Up @@ -68,6 +70,58 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {

readonly onDidChangeVisibility = this.onDidChangeBodyVisibility;

get focusedCommentNode(): CommentNode | undefined {
const focused = this.tree?.getFocus();
if (focused?.length === 1 && focused[0] instanceof CommentNode) {
return focused[0];
}
return undefined;
}

get focusedCommentInfo(): string | undefined {
const focused = this.focusedCommentNode;
if (!focused) {
return;
}
return this.getScreenReaderInfoForNode(focused, 'accessibleViewContent');
meganrogge marked this conversation as resolved.
Show resolved Hide resolved
}

focusNextNode(): void {
if (!this.tree) {
return;
}
const focused = this.tree.getFocus()?.[0];
if (!focused) {
return;
}
let next = this.tree.navigate(focused).next();
while (next && !(next instanceof CommentNode)) {
next = this.tree.navigate(next).next();
}
if (!next) {
return;
}
this.tree.setFocus([next]);
}

focusPreviousNode(): void {
if (!this.tree) {
return;
}
const focused = this.tree.getFocus()?.[0];
if (!focused) {
return;
}
let previous = this.tree.navigate(focused).previous();
while (previous && !(previous instanceof CommentNode)) {
previous = this.tree.navigate(previous).previous();
}
if (!previous) {
return;
}
this.tree.setFocus([previous]);
}

constructor(
options: IViewPaneOptions,
@IInstantiationService instantiationService: IInstantiationService,
Expand Down Expand Up @@ -263,46 +317,63 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
this.messageBoxContainer.classList.toggle('hidden', this.commentService.commentsModel.hasCommentThreads());
}

private getAriaForNode(element: CommentNode) {
private getScreenReaderInfoForNode(element: CommentNode, type: 'ariaLabel' | 'accessibleViewContent') {
let accessibleViewHint = '';
if (type === 'ariaLabel' && this.configurationService.getValue(AccessibilityVerbositySettingId.Comments)) {
const kbLabel = this.keybindingService.lookupKeybinding(AccessibleViewAction.id)?.getAriaLabel();
accessibleViewHint = kbLabel ? nls.localize('acessibleViewHint', "Inspect this in the accessible view ({0}).\n", kbLabel) : nls.localize('acessibleViewHintNoKbOpen', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.\n");
}
const replies = this.getRepliesAsString(element, type) || '';
if (element.range) {
if (element.threadRelevance === CommentThreadApplicability.Outdated) {
return nls.localize('resourceWithCommentLabelOutdated',
"Outdated from ${0} at line {1} column {2} in {3}, source: {4}",
return accessibleViewHint + nls.localize('resourceWithCommentLabelOutdated',
"Outdated from ${0} at line {1} column {2} in {3}, comment: {4}",
element.comment.userName,
element.range.startLineNumber,
element.range.startColumn,
basename(element.resource),
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
);
) + replies;
} else {
return nls.localize('resourceWithCommentLabel',
"${0} at line {1} column {2} in {3}, source: {4}",
return accessibleViewHint + nls.localize('resourceWithCommentLabel',
"${0} at line {1} column {2} in {3}, comment: {4}",
element.comment.userName,
element.range.startLineNumber,
element.range.startColumn,
basename(element.resource),
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
);
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value,
) + replies;
}
} else {
if (element.threadRelevance === CommentThreadApplicability.Outdated) {
return nls.localize('resourceWithCommentLabelFileOutdated',
"Outdated from {0} in {1}, source: {2}",
return accessibleViewHint + nls.localize('resourceWithCommentLabelFileOutdated',
"Outdated from {0} in {1}, comment: {2}",
element.comment.userName,
basename(element.resource),
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
);
) + replies;
} else {
return nls.localize('resourceWithCommentLabelFile',
"{0} in {1}, source: {2}",
return accessibleViewHint + nls.localize('resourceWithCommentLabelFile',
"{0} in {1}, comment: {2}",
element.comment.userName,
basename(element.resource),
(typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value
);
) + replies;
}
}
}

private getRepliesAsString(node: CommentNode, type: 'ariaLabel' | 'accessibleViewContent'): string {
if (!node.replies.length || type === 'ariaLabel') {
return '';
}
return nls.localize('replies', " {0} replies:\n{1}", node.replies.length, node.replies.map(reply => nls.localize('resourceWithRepliesLabel',
"${0} {1}",
reply.comment.userName,
(typeof reply.comment.body === 'string') ? reply.comment.body : reply.comment.body.value)
).join('\n'));
}

private createTree(): void {
this.treeLabels = this._register(this.instantiationService.createInstance(ResourceLabels, this));
this.tree = this._register(this.instantiationService.createInstance(CommentsList, this.treeLabels, this.treeContainer, {
Expand All @@ -323,7 +394,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView {
return nls.localize('resourceWithCommentThreadsLabel', "Comments in {0}, full path {1}", basename(element.resource), element.resource.fsPath);
}
if (element instanceof CommentNode) {
return this.getAriaForNode(element);
return this.getScreenReaderInfoForNode(element, 'ariaLabel');
}
return '';
},
Expand Down