Path: blob/main/src/vs/editor/browser/controller/editContext/screenReaderUtils.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { EndOfLinePreference } from '../../../common/model.js';6import { Position } from '../../../common/core/position.js';7import { Range } from '../../../common/core/range.js';8import { Selection, SelectionDirection } from '../../../common/core/selection.js';9import { EditorOption, IComputedEditorOptions } from '../../../common/config/editorOptions.js';10import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';11import { AccessibilitySupport } from '../../../../platform/accessibility/common/accessibility.js';12import * as nls from '../../../../nls.js';13import { ISimpleModel } from '../../../common/viewModel/screenReaderSimpleModel.js';1415export interface IPagedScreenReaderStrategy<T> {16fromEditorSelection(model: ISimpleModel, selection: Selection, linesPerPage: number, trimLongText: boolean): T;17}1819export interface ISimpleScreenReaderContentState {20value: string;2122/** the offset where selection starts inside `value` */23selectionStart: number;2425/** the offset where selection ends inside `value` */26selectionEnd: number;2728/** the editor range in the view coordinate system that matches the selection inside `value` */29selection: Range;3031/** the position of the start of the `value` in the editor */32startPositionWithinEditor: Position;3334/** the visible line count (wrapped, not necessarily matching \n characters) for the text in `value` before `selectionStart` */35newlineCountBeforeSelection: number;36}3738export class SimplePagedScreenReaderStrategy implements IPagedScreenReaderStrategy<ISimpleScreenReaderContentState> {39private _getPageOfLine(lineNumber: number, linesPerPage: number): number {40return Math.floor((lineNumber - 1) / linesPerPage);41}4243private _getRangeForPage(page: number, linesPerPage: number): Range {44const offset = page * linesPerPage;45const startLineNumber = offset + 1;46const endLineNumber = offset + linesPerPage;47return new Range(startLineNumber, 1, endLineNumber + 1, 1);48}4950public fromEditorSelection(model: ISimpleModel, selection: Selection, linesPerPage: number, trimLongText: boolean): ISimpleScreenReaderContentState {51// Chromium handles very poorly text even of a few thousand chars52// Cut text to avoid stalling the entire UI53const LIMIT_CHARS = 500;5455const selectionStartPage = this._getPageOfLine(selection.startLineNumber, linesPerPage);56const selectionStartPageRange = this._getRangeForPage(selectionStartPage, linesPerPage);5758const selectionEndPage = this._getPageOfLine(selection.endLineNumber, linesPerPage);59const selectionEndPageRange = this._getRangeForPage(selectionEndPage, linesPerPage);6061let pretextRange = selectionStartPageRange.intersectRanges(new Range(1, 1, selection.startLineNumber, selection.startColumn))!;62if (trimLongText && model.getValueLengthInRange(pretextRange, EndOfLinePreference.LF) > LIMIT_CHARS) {63const pretextStart = model.modifyPosition(pretextRange.getEndPosition(), -LIMIT_CHARS);64pretextRange = Range.fromPositions(pretextStart, pretextRange.getEndPosition());65}66const pretext = model.getValueInRange(pretextRange, EndOfLinePreference.LF);6768const lastLine = model.getLineCount();69const lastLineMaxColumn = model.getLineMaxColumn(lastLine);70let posttextRange = selectionEndPageRange.intersectRanges(new Range(selection.endLineNumber, selection.endColumn, lastLine, lastLineMaxColumn))!;71if (trimLongText && model.getValueLengthInRange(posttextRange, EndOfLinePreference.LF) > LIMIT_CHARS) {72const posttextEnd = model.modifyPosition(posttextRange.getStartPosition(), LIMIT_CHARS);73posttextRange = Range.fromPositions(posttextRange.getStartPosition(), posttextEnd);74}75const posttext = model.getValueInRange(posttextRange, EndOfLinePreference.LF);767778let text: string;79if (selectionStartPage === selectionEndPage || selectionStartPage + 1 === selectionEndPage) {80// take full selection81text = model.getValueInRange(selection, EndOfLinePreference.LF);82} else {83const selectionRange1 = selectionStartPageRange.intersectRanges(selection)!;84const selectionRange2 = selectionEndPageRange.intersectRanges(selection)!;85text = (86model.getValueInRange(selectionRange1, EndOfLinePreference.LF)87+ String.fromCharCode(8230)88+ model.getValueInRange(selectionRange2, EndOfLinePreference.LF)89);90}91if (trimLongText && text.length > 2 * LIMIT_CHARS) {92text = text.substring(0, LIMIT_CHARS) + String.fromCharCode(8230) + text.substring(text.length - LIMIT_CHARS, text.length);93}9495let selectionStart: number;96let selectionEnd: number;97if (selection.getDirection() === SelectionDirection.LTR) {98selectionStart = pretext.length;99selectionEnd = pretext.length + text.length;100} else {101selectionEnd = pretext.length;102selectionStart = pretext.length + text.length;103}104return {105value: pretext + text + posttext,106selection: selection,107selectionStart,108selectionEnd,109startPositionWithinEditor: pretextRange.getStartPosition(),110newlineCountBeforeSelection: pretextRange.endLineNumber - pretextRange.startLineNumber,111};112}113}114115export function ariaLabelForScreenReaderContent(options: IComputedEditorOptions, keybindingService: IKeybindingService) {116const accessibilitySupport = options.get(EditorOption.accessibilitySupport);117if (accessibilitySupport === AccessibilitySupport.Disabled) {118119const toggleKeybindingLabel = keybindingService.lookupKeybinding('editor.action.toggleScreenReaderAccessibilityMode')?.getAriaLabel();120const runCommandKeybindingLabel = keybindingService.lookupKeybinding('workbench.action.showCommands')?.getAriaLabel();121const keybindingEditorKeybindingLabel = keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings')?.getAriaLabel();122const editorNotAccessibleMessage = nls.localize('accessibilityModeOff', "The editor is not accessible at this time.");123if (toggleKeybindingLabel) {124return nls.localize('accessibilityOffAriaLabel', "{0} To enable screen reader optimized mode, use {1}", editorNotAccessibleMessage, toggleKeybindingLabel);125} else if (runCommandKeybindingLabel) {126return nls.localize('accessibilityOffAriaLabelNoKb', "{0} To enable screen reader optimized mode, open the quick pick with {1} and run the command Toggle Screen Reader Accessibility Mode, which is currently not triggerable via keyboard.", editorNotAccessibleMessage, runCommandKeybindingLabel);127} else if (keybindingEditorKeybindingLabel) {128return nls.localize('accessibilityOffAriaLabelNoKbs', "{0} Please assign a keybinding for the command Toggle Screen Reader Accessibility Mode by accessing the keybindings editor with {1} and run it.", editorNotAccessibleMessage, keybindingEditorKeybindingLabel);129} else {130// SOS131return editorNotAccessibleMessage;132}133}134return options.get(EditorOption.ariaLabel);135}136137export function newlinecount(text: string): number {138let result = 0;139let startIndex = -1;140do {141startIndex = text.indexOf('\n', startIndex + 1);142if (startIndex === -1) {143break;144}145result++;146} while (true);147return result;148}149150151