Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/controller/editContext/screenReaderUtils.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { EndOfLinePreference } from '../../../common/model.js';
7
import { Position } from '../../../common/core/position.js';
8
import { Range } from '../../../common/core/range.js';
9
import { Selection, SelectionDirection } from '../../../common/core/selection.js';
10
import { EditorOption, IComputedEditorOptions } from '../../../common/config/editorOptions.js';
11
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
12
import { AccessibilitySupport } from '../../../../platform/accessibility/common/accessibility.js';
13
import * as nls from '../../../../nls.js';
14
import { ISimpleModel } from '../../../common/viewModel/screenReaderSimpleModel.js';
15
16
export interface IPagedScreenReaderStrategy<T> {
17
fromEditorSelection(model: ISimpleModel, selection: Selection, linesPerPage: number, trimLongText: boolean): T;
18
}
19
20
export interface ISimpleScreenReaderContentState {
21
value: string;
22
23
/** the offset where selection starts inside `value` */
24
selectionStart: number;
25
26
/** the offset where selection ends inside `value` */
27
selectionEnd: number;
28
29
/** the editor range in the view coordinate system that matches the selection inside `value` */
30
selection: Range;
31
32
/** the position of the start of the `value` in the editor */
33
startPositionWithinEditor: Position;
34
35
/** the visible line count (wrapped, not necessarily matching \n characters) for the text in `value` before `selectionStart` */
36
newlineCountBeforeSelection: number;
37
}
38
39
export class SimplePagedScreenReaderStrategy implements IPagedScreenReaderStrategy<ISimpleScreenReaderContentState> {
40
private _getPageOfLine(lineNumber: number, linesPerPage: number): number {
41
return Math.floor((lineNumber - 1) / linesPerPage);
42
}
43
44
private _getRangeForPage(page: number, linesPerPage: number): Range {
45
const offset = page * linesPerPage;
46
const startLineNumber = offset + 1;
47
const endLineNumber = offset + linesPerPage;
48
return new Range(startLineNumber, 1, endLineNumber + 1, 1);
49
}
50
51
public fromEditorSelection(model: ISimpleModel, selection: Selection, linesPerPage: number, trimLongText: boolean): ISimpleScreenReaderContentState {
52
// Chromium handles very poorly text even of a few thousand chars
53
// Cut text to avoid stalling the entire UI
54
const LIMIT_CHARS = 500;
55
56
const selectionStartPage = this._getPageOfLine(selection.startLineNumber, linesPerPage);
57
const selectionStartPageRange = this._getRangeForPage(selectionStartPage, linesPerPage);
58
59
const selectionEndPage = this._getPageOfLine(selection.endLineNumber, linesPerPage);
60
const selectionEndPageRange = this._getRangeForPage(selectionEndPage, linesPerPage);
61
62
let pretextRange = selectionStartPageRange.intersectRanges(new Range(1, 1, selection.startLineNumber, selection.startColumn))!;
63
if (trimLongText && model.getValueLengthInRange(pretextRange, EndOfLinePreference.LF) > LIMIT_CHARS) {
64
const pretextStart = model.modifyPosition(pretextRange.getEndPosition(), -LIMIT_CHARS);
65
pretextRange = Range.fromPositions(pretextStart, pretextRange.getEndPosition());
66
}
67
const pretext = model.getValueInRange(pretextRange, EndOfLinePreference.LF);
68
69
const lastLine = model.getLineCount();
70
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
71
let posttextRange = selectionEndPageRange.intersectRanges(new Range(selection.endLineNumber, selection.endColumn, lastLine, lastLineMaxColumn))!;
72
if (trimLongText && model.getValueLengthInRange(posttextRange, EndOfLinePreference.LF) > LIMIT_CHARS) {
73
const posttextEnd = model.modifyPosition(posttextRange.getStartPosition(), LIMIT_CHARS);
74
posttextRange = Range.fromPositions(posttextRange.getStartPosition(), posttextEnd);
75
}
76
const posttext = model.getValueInRange(posttextRange, EndOfLinePreference.LF);
77
78
79
let text: string;
80
if (selectionStartPage === selectionEndPage || selectionStartPage + 1 === selectionEndPage) {
81
// take full selection
82
text = model.getValueInRange(selection, EndOfLinePreference.LF);
83
} else {
84
const selectionRange1 = selectionStartPageRange.intersectRanges(selection)!;
85
const selectionRange2 = selectionEndPageRange.intersectRanges(selection)!;
86
text = (
87
model.getValueInRange(selectionRange1, EndOfLinePreference.LF)
88
+ String.fromCharCode(8230)
89
+ model.getValueInRange(selectionRange2, EndOfLinePreference.LF)
90
);
91
}
92
if (trimLongText && text.length > 2 * LIMIT_CHARS) {
93
text = text.substring(0, LIMIT_CHARS) + String.fromCharCode(8230) + text.substring(text.length - LIMIT_CHARS, text.length);
94
}
95
96
let selectionStart: number;
97
let selectionEnd: number;
98
if (selection.getDirection() === SelectionDirection.LTR) {
99
selectionStart = pretext.length;
100
selectionEnd = pretext.length + text.length;
101
} else {
102
selectionEnd = pretext.length;
103
selectionStart = pretext.length + text.length;
104
}
105
return {
106
value: pretext + text + posttext,
107
selection: selection,
108
selectionStart,
109
selectionEnd,
110
startPositionWithinEditor: pretextRange.getStartPosition(),
111
newlineCountBeforeSelection: pretextRange.endLineNumber - pretextRange.startLineNumber,
112
};
113
}
114
}
115
116
export function ariaLabelForScreenReaderContent(options: IComputedEditorOptions, keybindingService: IKeybindingService) {
117
const accessibilitySupport = options.get(EditorOption.accessibilitySupport);
118
if (accessibilitySupport === AccessibilitySupport.Disabled) {
119
120
const toggleKeybindingLabel = keybindingService.lookupKeybinding('editor.action.toggleScreenReaderAccessibilityMode')?.getAriaLabel();
121
const runCommandKeybindingLabel = keybindingService.lookupKeybinding('workbench.action.showCommands')?.getAriaLabel();
122
const keybindingEditorKeybindingLabel = keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings')?.getAriaLabel();
123
const editorNotAccessibleMessage = nls.localize('accessibilityModeOff', "The editor is not accessible at this time.");
124
if (toggleKeybindingLabel) {
125
return nls.localize('accessibilityOffAriaLabel', "{0} To enable screen reader optimized mode, use {1}", editorNotAccessibleMessage, toggleKeybindingLabel);
126
} else if (runCommandKeybindingLabel) {
127
return 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);
128
} else if (keybindingEditorKeybindingLabel) {
129
return 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);
130
} else {
131
// SOS
132
return editorNotAccessibleMessage;
133
}
134
}
135
return options.get(EditorOption.ariaLabel);
136
}
137
138
export function newlinecount(text: string): number {
139
let result = 0;
140
let startIndex = -1;
141
do {
142
startIndex = text.indexOf('\n', startIndex + 1);
143
if (startIndex === -1) {
144
break;
145
}
146
result++;
147
} while (true);
148
return result;
149
}
150
151