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

wrapped lines w screen reader #163229

Merged
merged 33 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
a674f74
wip
meganrogge Oct 3, 2022
f260c55
Merge branch 'main' into merogge/acc
meganrogge Oct 10, 2022
581ef04
sync widths
meganrogge Oct 10, 2022
e6edba8
clean up
meganrogge Oct 10, 2022
d443fe5
remove line
meganrogge Oct 10, 2022
b4c3702
get it to work
meganrogge Oct 10, 2022
c03711b
add wrapping strategy
meganrogge Oct 10, 2022
324dbb1
fix issue
meganrogge Oct 10, 2022
a3853d9
always use the width
meganrogge Oct 10, 2022
e3c40f7
Reduce diffs
alexdima Oct 11, 2022
e13884c
Fix JSON schema for the WrappingStrategy option
alexdima Oct 11, 2022
69b2989
Only turn on advanced wrapping strategy when we know that a screen re…
alexdima Oct 11, 2022
b358b58
Make the textarea's width match the wrapping width when wrapping is e…
alexdima Oct 11, 2022
08dc27a
Force wrappingIndent to be none when we know a screen reader is attac…
alexdima Oct 11, 2022
c77d651
remove part of notification message
meganrogge Oct 11, 2022
8c31976
Merge branch 'main' into merogge/acc
meganrogge Oct 12, 2022
397965b
Merge branch 'main' into merogge/acc
meganrogge Oct 14, 2022
a75a26f
adjust z-indices, use content left when wrapped
meganrogge Oct 17, 2022
3c88fc3
use view model rendering data as content when wrapping is enabled
meganrogge Oct 17, 2022
1634cbb
selection is not working correctly
meganrogge Oct 17, 2022
a062f30
mostly fix selection problem
meganrogge Oct 17, 2022
bb9ad1c
broke normal wrapping handling
meganrogge Oct 17, 2022
dfe51e8
Merge remote-tracking branch 'origin/main' into merogge/acc
alexdima Oct 19, 2022
3332b55
Revert to using the text model instead of the view model and to using…
alexdima Oct 19, 2022
669dc20
Record also the start position for the text in the textarea
alexdima Oct 19, 2022
2e89793
Expose EndOfLinePreference to `getValueLengthInRange` and fix its imp…
alexdima Oct 19, 2022
9de6238
Fix `getValueLengthInRange` implementation to convert its range from …
alexdima Oct 19, 2022
33f8e8c
Record the visible line count for the text in `value` before `selecti…
alexdima Oct 19, 2022
a471dde
Merge remote-tracking branch 'origin/alexd/instant-tarantula' into me…
alexdima Oct 19, 2022
2b32be7
Fix up line count such that the VoiceOver thick black box lines up co…
alexdima Oct 19, 2022
afb7931
Merge remote-tracking branch 'origin/main' into merogge/acc
alexdima Oct 19, 2022
c23f1b2
Make tab characters inside the textarea match the editor's tab width
alexdima Oct 21, 2022
200754d
Turn off wrapping when doing IME and be sure to measure IME text usin…
alexdima Oct 21, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/vs/editor/browser/controller/textAreaHandler.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
overflow: hidden;
color: transparent;
background-color: transparent;
z-index: -10;
}
/*.monaco-editor .inputarea {
position: fixed !important;
Expand All @@ -28,6 +29,7 @@
background: white !important;
line-height: 15px !important;
font-size: 14px !important;
z-index: 10 !important;
}*/
.monaco-editor .inputarea.ime-input {
z-index: 10;
Expand Down
55 changes: 43 additions & 12 deletions src/vs/editor/browser/controller/textAreaHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/v
import { LineNumbersOverlay } from 'vs/editor/browser/viewParts/lineNumbers/lineNumbers';
import { Margin } from 'vs/editor/browser/viewParts/margin/margin';
import { RenderLineNumbersType, EditorOption, IComputedEditorOptions, EditorOptions } from 'vs/editor/common/config/editorOptions';
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/core/wordCharacterClassifier';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
Expand Down Expand Up @@ -114,10 +114,12 @@ export class TextAreaHandler extends ViewPart {
private _accessibilitySupport!: AccessibilitySupport;
private _accessibilityPageSize!: number;
private _accessibilityWriteTimer: TimeoutTimer;
private _textAreaWrapping!: boolean;
private _textAreaWidth!: number;
private _contentLeft: number;
private _contentWidth: number;
private _contentHeight: number;
private _fontInfo: BareFontInfo;
private _fontInfo: FontInfo;
private _lineHeight: number;
private _emptySelectionClipboard: boolean;
private _copyWithSyntaxHighlighting: boolean;
Expand Down Expand Up @@ -169,7 +171,9 @@ export class TextAreaHandler extends ViewPart {
this.textArea = createFastDomNode(document.createElement('textarea'));
PartFingerprints.write(this.textArea, PartFingerprint.TextArea);
this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
this.textArea.setAttribute('wrap', 'off');
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
const { tabSize } = this._context.viewModel.model.getOptions();
this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;
this.textArea.setAttribute('autocorrect', 'off');
this.textArea.setAttribute('autocapitalize', 'off');
this.textArea.setAttribute('autocomplete', 'off');
Expand Down Expand Up @@ -379,7 +383,8 @@ export class TextAreaHandler extends ViewPart {
const visibleBeforeCharCount = Math.min(startModelPosition.column - 1, desiredVisibleBeforeCharCount);
const distanceToModelLineStart = startModelPosition.column - 1 - visibleBeforeCharCount;
const hiddenLineTextBefore = lineTextBeforeSelection.substring(0, lineTextBeforeSelection.length - visibleBeforeCharCount);
const widthOfHiddenTextBefore = measureText(hiddenLineTextBefore, this._fontInfo);
const { tabSize } = this._context.viewModel.model.getOptions();
const widthOfHiddenTextBefore = measureText(hiddenLineTextBefore, this._fontInfo, tabSize);

return { distanceToModelLineStart, widthOfHiddenTextBefore };
})();
Expand Down Expand Up @@ -416,6 +421,9 @@ export class TextAreaHandler extends ViewPart {
distanceToModelLineEnd,
);

// We turn off wrapping if the <textarea> becomes visible for composition
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');

this._visibleTextArea.prepareRender(this._visibleRangeProvider);
this._render();

Expand All @@ -438,6 +446,10 @@ export class TextAreaHandler extends ViewPart {
this._register(this._textAreaInput.onCompositionEnd(() => {

this._visibleTextArea = null;

// We turn on wrapping as necessary if the <textarea> hides after composition
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');

this._render();

this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
Expand Down Expand Up @@ -545,6 +557,21 @@ export class TextAreaHandler extends ViewPart {
} else {
this._accessibilityPageSize = accessibilityPageSize;
}

// When wrapping is enabled and a screen reader might be attached,
// we will size the textarea to match the width used for wrapping points computation (see `domLineBreaksComputer.ts`).
// This is because screen readers will read the text in the textarea and we'd like that the
// wrapping points in the textarea match the wrapping points in the editor.
const layoutInfo = options.get(EditorOption.layoutInfo);
const wrappingColumn = layoutInfo.wrappingColumn;
if (wrappingColumn !== -1 && this._accessibilitySupport !== AccessibilitySupport.Disabled) {
const fontInfo = options.get(EditorOption.fontInfo);
this._textAreaWrapping = true;
this._textAreaWidth = Math.round(wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);
} else {
this._textAreaWrapping = false;
this._textAreaWidth = (canUseZeroSizeTextarea ? 0 : 1);
}
}

// --- begin event handlers
Expand All @@ -561,6 +588,9 @@ export class TextAreaHandler extends ViewPart {
this._lineHeight = options.get(EditorOption.lineHeight);
this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard);
this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting);
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
const { tabSize } = this._context.viewModel.model.getOptions();
this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;
this.textArea.setAttribute('aria-label', this._getAriaLabel(options));
this.textArea.setAttribute('tabindex', String(options.get(EditorOption.tabIndex)));

Expand Down Expand Up @@ -757,25 +787,25 @@ export class TextAreaHandler extends ViewPart {
// We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers
this._doRender({
lastRenderPosition: this._primaryCursorPosition,
top: top,
left: left,
width: (canUseZeroSizeTextarea ? 0 : 1),
top,
left: this._textAreaWrapping ? this._contentLeft : left,
width: this._textAreaWidth,
height: this._lineHeight,
useCover: false
});
// In case the textarea contains a word, we're going to try to align the textarea's cursor
// with our cursor by scrolling the textarea as much as possible
this.textArea.domNode.scrollLeft = this._primaryCursorVisibleRange.left;
const lineCount = this._newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));
const lineCount = this._textAreaInput.textAreaState.newlineCountBeforeSelection ?? this._newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));
this.textArea.domNode.scrollTop = lineCount * this._lineHeight;
return;
}

this._doRender({
lastRenderPosition: this._primaryCursorPosition,
top: top,
left: left,
width: (canUseZeroSizeTextarea ? 0 : 1),
left: this._textAreaWrapping ? this._contentLeft : left,
width: this._textAreaWidth,
height: (canUseZeroSizeTextarea ? 0 : 1),
useCover: false
});
Expand All @@ -801,7 +831,7 @@ export class TextAreaHandler extends ViewPart {
lastRenderPosition: null,
top: 0,
left: 0,
width: (canUseZeroSizeTextarea ? 0 : 1),
width: this._textAreaWidth,
height: (canUseZeroSizeTextarea ? 0 : 1),
useCover: true
});
Expand Down Expand Up @@ -861,7 +891,7 @@ interface IRenderData {
strikethrough?: boolean;
}

function measureText(text: string, fontInfo: BareFontInfo): number {
function measureText(text: string, fontInfo: FontInfo, tabSize: number): number {
if (text.length === 0) {
return 0;
}
Expand All @@ -874,6 +904,7 @@ function measureText(text: string, fontInfo: BareFontInfo): number {
const regularDomNode = document.createElement('span');
applyFontInfo(regularDomNode, fontInfo);
regularDomNode.style.whiteSpace = 'pre'; // just like the textarea
regularDomNode.style.tabSize = `${tabSize * fontInfo.spaceWidth}px`; // just like the textarea
regularDomNode.append(text);
container.appendChild(regularDomNode);

Expand Down
5 changes: 5 additions & 0 deletions src/vs/editor/browser/controller/textAreaInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ export class TextAreaInput extends Disposable {
private readonly _asyncFocusGainWriteScreenReaderContent: RunOnceScheduler;

private _textAreaState: TextAreaState;

public get textAreaState(): TextAreaState {
return this._textAreaState;
}

private _selectionChangeListener: IDisposable | null;

private _hasFocus: boolean;
Expand Down
132 changes: 83 additions & 49 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2326,7 +2326,6 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption<EditorOption.
const wordWrap = (wordWrapOverride1 === 'inherit' ? options.get(EditorOption.wordWrap) : wordWrapOverride1);

const wordWrapColumn = options.get(EditorOption.wordWrapColumn);
const accessibilitySupport = options.get(EditorOption.accessibilitySupport);
const isDominatedByLongLines = env.isDominatedByLongLines;

const showGlyphMargin = options.get(EditorOption.glyphMargin);
Expand Down Expand Up @@ -2378,20 +2377,14 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption<EditorOption.
let isViewportWrapping = false;
let wrappingColumn = -1;

if (accessibilitySupport !== AccessibilitySupport.Enabled) {
// See https://github.com/microsoft/vscode/issues/27766
// Never enable wrapping when a screen reader is attached
// because arrow down etc. will not move the cursor in the way
// a screen reader expects.
if (wordWrapOverride1 === 'inherit' && isDominatedByLongLines) {
// Force viewport width wrapping if model is dominated by long lines
isWordWrapMinified = true;
isViewportWrapping = true;
} else if (wordWrap === 'on' || wordWrap === 'bounded') {
isViewportWrapping = true;
} else if (wordWrap === 'wordWrapColumn') {
wrappingColumn = wordWrapColumn;
}
if (wordWrapOverride1 === 'inherit' && isDominatedByLongLines) {
// Force viewport width wrapping if model is dominated by long lines
isWordWrapMinified = true;
isViewportWrapping = true;
} else if (wordWrap === 'on' || wordWrap === 'bounded') {
isViewportWrapping = true;
} else if (wordWrap === 'wordWrapColumn') {
wrappingColumn = wordWrapColumn;
}

const minimapLayout = EditorLayoutInfoComputer._computeMinimapLayout({
Expand Down Expand Up @@ -2469,6 +2462,42 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption<EditorOption.

//#endregion

//#region WrappingStrategy
class WrappingStrategy extends BaseEditorOption<EditorOption.wrappingStrategy, 'simple' | 'advanced', 'simple' | 'advanced'> {

constructor() {
super(EditorOption.wrappingStrategy, 'wrappingStrategy', 'simple',
{
'editor.wrappingStrategy': {
enumDescriptions: [
nls.localize('wrappingStrategy.simple', "Assumes that all characters are of the same width. This is a fast algorithm that works correctly for monospace fonts and certain scripts (like Latin characters) where glyphs are of equal width."),
nls.localize('wrappingStrategy.advanced', "Delegates wrapping points computation to the browser. This is a slow algorithm, that might cause freezes for large files, but it works correctly in all cases.")
],
type: 'string',
enum: ['simple', 'advanced'],
default: 'simple',
description: nls.localize('wrappingStrategy', "Controls the algorithm that computes wrapping points. Note that when in accessibility mode, advanced will be used for the best experience.")
}
}
);
}

public validate(input: any): 'simple' | 'advanced' {
return stringSet<'simple' | 'advanced'>(input, 'simple', ['simple', 'advanced']);
}

public override compute(env: IEnvironmentalOptions, options: IComputedEditorOptions, value: 'simple' | 'advanced'): 'simple' | 'advanced' {
const accessibilitySupport = options.get(EditorOption.accessibilitySupport);
if (accessibilitySupport === AccessibilitySupport.Enabled) {
// if we know for a fact that a screen reader is attached, we switch our strategy to advanced to
// help that the editor's wrapping points match the textarea's wrapping points
return 'advanced';
}
return value;
}
}
//#endregion

//#region lightbulb

/**
Expand Down Expand Up @@ -4426,12 +4455,42 @@ export const enum WrappingIndent {
DeepIndent = 3
}

function _wrappingIndentFromString(wrappingIndent: 'none' | 'same' | 'indent' | 'deepIndent'): WrappingIndent {
switch (wrappingIndent) {
case 'none': return WrappingIndent.None;
case 'same': return WrappingIndent.Same;
case 'indent': return WrappingIndent.Indent;
case 'deepIndent': return WrappingIndent.DeepIndent;
class WrappingIndentOption extends BaseEditorOption<EditorOption.wrappingIndent, 'none' | 'same' | 'indent' | 'deepIndent', WrappingIndent> {

constructor() {
super(EditorOption.wrappingIndent, 'wrappingIndent', WrappingIndent.Same,
{
'editor.wrappingIndent': {
enumDescriptions: [
nls.localize('wrappingIndent.none', "No indentation. Wrapped lines begin at column 1."),
nls.localize('wrappingIndent.same', "Wrapped lines get the same indentation as the parent."),
nls.localize('wrappingIndent.indent', "Wrapped lines get +1 indentation toward the parent."),
nls.localize('wrappingIndent.deepIndent', "Wrapped lines get +2 indentation toward the parent."),
],
description: nls.localize('wrappingIndent', "Controls the indentation of wrapped lines."),
}
}
);
}

public validate(input: any): WrappingIndent {
switch (input) {
case 'none': return WrappingIndent.None;
case 'same': return WrappingIndent.Same;
case 'indent': return WrappingIndent.Indent;
case 'deepIndent': return WrappingIndent.DeepIndent;
}
return WrappingIndent.Same;
}

public override compute(env: IEnvironmentalOptions, options: IComputedEditorOptions, value: WrappingIndent): WrappingIndent {
const accessibilitySupport = options.get(EditorOption.accessibilitySupport);
if (accessibilitySupport === AccessibilitySupport.Enabled) {
// if we know for a fact that a screen reader is attached, we use no indent wrapping to
// help that the editor's wrapping points match the textarea's wrapping points
return WrappingIndent.None;
}
return value;
}
}

Expand Down Expand Up @@ -5345,40 +5404,15 @@ export const EditorOptions = {
'inherit' as 'off' | 'on' | 'inherit',
['off', 'on', 'inherit'] as const
)),
wrappingIndent: register(new EditorEnumOption(
EditorOption.wrappingIndent, 'wrappingIndent',
WrappingIndent.Same, 'same',
['none', 'same', 'indent', 'deepIndent'],
_wrappingIndentFromString,
{
enumDescriptions: [
nls.localize('wrappingIndent.none', "No indentation. Wrapped lines begin at column 1."),
nls.localize('wrappingIndent.same', "Wrapped lines get the same indentation as the parent."),
nls.localize('wrappingIndent.indent', "Wrapped lines get +1 indentation toward the parent."),
nls.localize('wrappingIndent.deepIndent', "Wrapped lines get +2 indentation toward the parent."),
],
description: nls.localize('wrappingIndent', "Controls the indentation of wrapped lines."),
}
)),
wrappingStrategy: register(new EditorStringEnumOption(
EditorOption.wrappingStrategy, 'wrappingStrategy',
'simple' as 'simple' | 'advanced',
['simple', 'advanced'] as const,
{
enumDescriptions: [
nls.localize('wrappingStrategy.simple', "Assumes that all characters are of the same width. This is a fast algorithm that works correctly for monospace fonts and certain scripts (like Latin characters) where glyphs are of equal width."),
nls.localize('wrappingStrategy.advanced', "Delegates wrapping points computation to the browser. This is a slow algorithm, that might cause freezes for large files, but it works correctly in all cases.")
],
description: nls.localize('wrappingStrategy', "Controls the algorithm that computes wrapping points.")
}
)),

// Leave these at the end (because they have dependencies!)
editorClassName: register(new EditorClassName()),
pixelRatio: register(new EditorPixelRatio()),
tabFocusMode: register(new EditorTabFocusMode()),
layoutInfo: register(new EditorLayoutInfoComputer()),
wrappingInfo: register(new EditorWrappingInfoComputer())
wrappingInfo: register(new EditorWrappingInfoComputer()),
wrappingIndent: register(new WrappingIndentOption()),
wrappingStrategy: register(new WrappingStrategy())
};

type EditorOptionsType = typeof EditorOptions;
Expand Down
4 changes: 2 additions & 2 deletions src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4681,13 +4681,13 @@ declare namespace monaco.editor {
wordWrapColumn: IEditorOption<EditorOption.wordWrapColumn, number>;
wordWrapOverride1: IEditorOption<EditorOption.wordWrapOverride1, 'on' | 'off' | 'inherit'>;
wordWrapOverride2: IEditorOption<EditorOption.wordWrapOverride2, 'on' | 'off' | 'inherit'>;
wrappingIndent: IEditorOption<EditorOption.wrappingIndent, WrappingIndent>;
wrappingStrategy: IEditorOption<EditorOption.wrappingStrategy, 'simple' | 'advanced'>;
editorClassName: IEditorOption<EditorOption.editorClassName, string>;
pixelRatio: IEditorOption<EditorOption.pixelRatio, number>;
tabFocusMode: IEditorOption<EditorOption.tabFocusMode, boolean>;
layoutInfo: IEditorOption<EditorOption.layoutInfo, EditorLayoutInfo>;
wrappingInfo: IEditorOption<EditorOption.wrappingInfo, EditorWrappingInfo>;
wrappingIndent: IEditorOption<EditorOption.wrappingIndent, WrappingIndent>;
wrappingStrategy: IEditorOption<EditorOption.wrappingStrategy, 'simple' | 'advanced'>;
};

type EditorOptionsType = typeof EditorOptions;
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/browser/parts/editor/editorStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
if (!this.screenReaderNotification) {
this.screenReaderNotification = this.notificationService.prompt(
Severity.Info,
localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"),
localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code?"),
[{
label: localize('screenReaderDetectedExplanation.answerYes', "Yes"),
run: () => {
Expand Down