Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/accessibility/browser/accessibleView.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 { EventType, addDisposableListener, getActiveWindow, isActiveElement } from '../../../../base/browser/dom.js';
7
import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js';
9
import { alert } from '../../../../base/browser/ui/aria/aria.js';
10
import { IAction } from '../../../../base/common/actions.js';
11
import { Codicon } from '../../../../base/common/codicons.js';
12
import { KeyCode } from '../../../../base/common/keyCodes.js';
13
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
14
import * as marked from '../../../../base/common/marked/marked.js';
15
import { Schemas } from '../../../../base/common/network.js';
16
import { isMacintosh, isWindows } from '../../../../base/common/platform.js';
17
import { ThemeIcon } from '../../../../base/common/themables.js';
18
import { URI } from '../../../../base/common/uri.js';
19
import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';
20
import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';
21
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
22
import { IPosition, Position } from '../../../../editor/common/core/position.js';
23
import { ITextModel } from '../../../../editor/common/model.js';
24
import { IModelService } from '../../../../editor/common/services/model.js';
25
import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js';
26
import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js';
27
import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js';
28
import { localize } from '../../../../nls.js';
29
import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js';
30
import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
31
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
32
import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
33
import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
34
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
35
import { ICommandService } from '../../../../platform/commands/common/commands.js';
36
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
37
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
38
import { IContextViewDelegate, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
39
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
40
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
41
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
42
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
43
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
44
import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
45
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
46
import { FloatingEditorClickMenu } from '../../../browser/codeeditor.js';
47
import { IChatCodeBlockContextProviderService } from '../../chat/browser/chat.js';
48
import { ICodeBlockActionContext } from '../../chat/browser/codeBlockPart.js';
49
import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';
50
import { AccessibilityCommandId } from '../common/accessibilityCommands.js';
51
import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from './accessibilityConfiguration.js';
52
import { resolveContentAndKeybindingItems } from './accessibleViewKeybindingResolver.js';
53
54
const enum DIMENSIONS {
55
MAX_WIDTH = 600
56
}
57
58
export type AccesibleViewContentProvider = AccessibleContentProvider | ExtensionContentProvider;
59
60
interface ICodeBlock {
61
startLine: number;
62
endLine: number;
63
code: string;
64
languageId?: string;
65
chatSessionId: string | undefined;
66
}
67
68
export class AccessibleView extends Disposable implements ITextModelContentProvider {
69
private _editorWidget: CodeEditorWidget;
70
71
private _accessiblityHelpIsShown: IContextKey<boolean>;
72
private _onLastLine: IContextKey<boolean>;
73
private _accessibleViewIsShown: IContextKey<boolean>;
74
private _accessibleViewSupportsNavigation: IContextKey<boolean>;
75
private _accessibleViewVerbosityEnabled: IContextKey<boolean>;
76
private _accessibleViewGoToSymbolSupported: IContextKey<boolean>;
77
private _accessibleViewCurrentProviderId: IContextKey<string>;
78
private _accessibleViewInCodeBlock: IContextKey<boolean>;
79
private _accessibleViewContainsCodeBlocks: IContextKey<boolean>;
80
private _hasUnassignedKeybindings: IContextKey<boolean>;
81
private _hasAssignedKeybindings: IContextKey<boolean>;
82
83
private _codeBlocks?: ICodeBlock[];
84
private _isInQuickPick: boolean = false;
85
86
get editorWidget() { return this._editorWidget; }
87
private _container: HTMLElement;
88
private _title: HTMLElement;
89
private readonly _toolbar: WorkbenchToolBar;
90
91
private _currentProvider: AccesibleViewContentProvider | undefined;
92
private _currentContent: string | undefined;
93
94
private _lastProvider: AccesibleViewContentProvider | undefined;
95
96
private _viewContainer: HTMLElement | undefined;
97
98
99
constructor(
100
@IOpenerService private readonly _openerService: IOpenerService,
101
@IInstantiationService private readonly _instantiationService: IInstantiationService,
102
@IConfigurationService private readonly _configurationService: IConfigurationService,
103
@IModelService private readonly _modelService: IModelService,
104
@IContextViewService private readonly _contextViewService: IContextViewService,
105
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
106
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
107
@IKeybindingService private readonly _keybindingService: IKeybindingService,
108
@ILayoutService private readonly _layoutService: ILayoutService,
109
@IMenuService private readonly _menuService: IMenuService,
110
@ICommandService private readonly _commandService: ICommandService,
111
@IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService,
112
@IStorageService private readonly _storageService: IStorageService,
113
@ITextModelService private readonly textModelResolverService: ITextModelService,
114
@IQuickInputService private readonly _quickInputService: IQuickInputService,
115
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,
116
) {
117
super();
118
119
this._accessiblityHelpIsShown = accessibilityHelpIsShown.bindTo(this._contextKeyService);
120
this._accessibleViewIsShown = accessibleViewIsShown.bindTo(this._contextKeyService);
121
this._accessibleViewSupportsNavigation = accessibleViewSupportsNavigation.bindTo(this._contextKeyService);
122
this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService);
123
this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService);
124
this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService);
125
this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService);
126
this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService);
127
this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService);
128
this._hasUnassignedKeybindings = accessibleViewHasUnassignedKeybindings.bindTo(this._contextKeyService);
129
this._hasAssignedKeybindings = accessibleViewHasAssignedKeybindings.bindTo(this._contextKeyService);
130
131
this._container = document.createElement('div');
132
this._container.classList.add('accessible-view');
133
if (this._configurationService.getValue(AccessibilityWorkbenchSettingId.HideAccessibleView)) {
134
this._container.classList.add('hide');
135
}
136
const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {
137
contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID)
138
};
139
const titleBar = document.createElement('div');
140
titleBar.classList.add('accessible-view-title-bar');
141
this._title = document.createElement('div');
142
this._title.classList.add('accessible-view-title');
143
titleBar.appendChild(this._title);
144
const actionBar = document.createElement('div');
145
actionBar.classList.add('accessible-view-action-bar');
146
titleBar.appendChild(actionBar);
147
this._container.appendChild(titleBar);
148
this._toolbar = this._register(_instantiationService.createInstance(WorkbenchToolBar, actionBar, { orientation: ActionsOrientation.HORIZONTAL }));
149
this._toolbar.context = { viewId: 'accessibleView' };
150
const toolbarElt = this._toolbar.getElement();
151
toolbarElt.tabIndex = 0;
152
153
const editorOptions: IEditorConstructionOptions = {
154
...getSimpleEditorOptions(this._configurationService),
155
lineDecorationsWidth: 6,
156
dragAndDrop: false,
157
cursorWidth: 1,
158
wordWrap: 'off',
159
wrappingStrategy: 'advanced',
160
wrappingIndent: 'none',
161
padding: { top: 2, bottom: 2 },
162
quickSuggestions: false,
163
renderWhitespace: 'none',
164
dropIntoEditor: { enabled: false },
165
readOnly: true,
166
fontFamily: 'var(--monaco-monospace-font)'
167
};
168
this.textModelResolverService.registerTextModelContentProvider(Schemas.accessibleView, this);
169
170
this._editorWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._container, editorOptions, codeEditorWidgetOptions));
171
this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {
172
if (this._currentProvider && this._accessiblityHelpIsShown.get()) {
173
this.show(this._currentProvider);
174
}
175
}));
176
this._register(this._configurationService.onDidChangeConfiguration(e => {
177
if (isIAccessibleViewContentProvider(this._currentProvider) && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) {
178
if (this._accessiblityHelpIsShown.get()) {
179
this.show(this._currentProvider);
180
}
181
this._accessibleViewVerbosityEnabled.set(this._configurationService.getValue(this._currentProvider.verbositySettingKey));
182
this._updateToolbar(this._currentProvider.actions, this._currentProvider.options.type);
183
}
184
if (e.affectsConfiguration(AccessibilityWorkbenchSettingId.HideAccessibleView)) {
185
this._container.classList.toggle('hide', this._configurationService.getValue(AccessibilityWorkbenchSettingId.HideAccessibleView));
186
}
187
}));
188
this._register(this._editorWidget.onDidDispose(() => this._resetContextKeys()));
189
this._register(this._editorWidget.onDidChangeCursorPosition(() => {
190
this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount());
191
const cursorPosition = this._editorWidget.getPosition()?.lineNumber;
192
if (this._codeBlocks && cursorPosition !== undefined) {
193
const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined;
194
this._accessibleViewInCodeBlock.set(inCodeBlock);
195
}
196
this._playDiffSignals();
197
}));
198
}
199
200
private _playDiffSignals(): void {
201
const position = this._editorWidget.getPosition();
202
const model = this._editorWidget.getModel();
203
if (!position || !model) {
204
return undefined;
205
}
206
const lineContent = model.getLineContent(position.lineNumber);
207
if (lineContent?.startsWith('+')) {
208
this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted);
209
} else if (lineContent?.startsWith('-')) {
210
this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted);
211
}
212
}
213
214
provideTextContent(resource: URI): Promise<ITextModel | null> | null {
215
return this._getTextModel(resource);
216
}
217
218
private _resetContextKeys(): void {
219
this._accessiblityHelpIsShown.reset();
220
this._accessibleViewIsShown.reset();
221
this._accessibleViewSupportsNavigation.reset();
222
this._accessibleViewVerbosityEnabled.reset();
223
this._accessibleViewGoToSymbolSupported.reset();
224
this._accessibleViewCurrentProviderId.reset();
225
this._hasAssignedKeybindings.reset();
226
this._hasUnassignedKeybindings.reset();
227
}
228
229
getPosition(id?: AccessibleViewProviderId): Position | undefined {
230
if (!id || !this._lastProvider || this._lastProvider.id !== id) {
231
return undefined;
232
}
233
return this._editorWidget.getPosition() || undefined;
234
}
235
236
setPosition(position: Position, reveal?: boolean, select?: boolean): void {
237
this._editorWidget.setPosition(position);
238
if (reveal) {
239
this._editorWidget.revealPosition(position);
240
}
241
if (select) {
242
const lineLength = this._editorWidget.getModel()?.getLineLength(position.lineNumber) ?? 0;
243
if (lineLength) {
244
this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: lineLength + 1 });
245
}
246
}
247
}
248
249
getCodeBlockContext(): ICodeBlockActionContext | undefined {
250
const position = this._editorWidget.getPosition();
251
if (!this._codeBlocks?.length || !position) {
252
return;
253
}
254
const codeBlockIndex = this._codeBlocks?.findIndex(c => c.startLine <= position?.lineNumber && c.endLine >= position?.lineNumber);
255
const codeBlock = codeBlockIndex !== undefined && codeBlockIndex > -1 ? this._codeBlocks[codeBlockIndex] : undefined;
256
if (!codeBlock || codeBlockIndex === undefined) {
257
return;
258
}
259
return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined, chatSessionId: codeBlock.chatSessionId };
260
}
261
262
navigateToCodeBlock(type: 'next' | 'previous'): void {
263
const position = this._editorWidget.getPosition();
264
if (!this._codeBlocks?.length || !position) {
265
return;
266
}
267
let codeBlock;
268
const codeBlocks = this._codeBlocks.slice();
269
if (type === 'previous') {
270
codeBlock = codeBlocks.reverse().find(c => c.endLine < position.lineNumber);
271
} else {
272
codeBlock = codeBlocks.find(c => c.startLine > position.lineNumber);
273
}
274
if (!codeBlock) {
275
return;
276
}
277
this.setPosition(new Position(codeBlock.startLine, 1), true);
278
}
279
280
showLastProvider(id: AccessibleViewProviderId): void {
281
if (!this._lastProvider || this._lastProvider.options.id !== id) {
282
return;
283
}
284
this.show(this._lastProvider);
285
}
286
287
show(provider?: AccesibleViewContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: IPosition): void {
288
provider = provider ?? this._currentProvider;
289
if (!provider) {
290
return;
291
}
292
provider.onOpen?.();
293
const delegate: IContextViewDelegate = {
294
getAnchor: () => { return { x: (getActiveWindow().innerWidth / 2) - ((Math.min(this._layoutService.activeContainerDimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.activeContainerOffset.quickPickTop }; },
295
render: (container) => {
296
this._viewContainer = container;
297
this._viewContainer.classList.add('accessible-view-container');
298
return this._render(provider, container, showAccessibleViewHelp);
299
},
300
onHide: () => {
301
if (!showAccessibleViewHelp) {
302
this._updateLastProvider();
303
this._currentProvider?.dispose();
304
this._currentProvider = undefined;
305
this._resetContextKeys();
306
}
307
}
308
};
309
this._contextViewService.showContextView(delegate);
310
311
if (position) {
312
// Context view takes time to show up, so we need to wait for it to show up before we can set the position
313
queueMicrotask(() => {
314
this._editorWidget.revealLine(position.lineNumber);
315
this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column });
316
});
317
}
318
319
if (symbol && this._currentProvider) {
320
this.showSymbol(this._currentProvider, symbol);
321
}
322
if (provider instanceof AccessibleContentProvider && provider.onDidRequestClearLastProvider) {
323
this._register(provider.onDidRequestClearLastProvider((id: string) => {
324
if (this._lastProvider?.options.id === id) {
325
this._lastProvider = undefined;
326
}
327
}));
328
}
329
if (provider.options.id) {
330
// only cache a provider with an ID so that it will eventually be cleared.
331
this._lastProvider = provider;
332
}
333
if (provider.id === AccessibleViewProviderId.PanelChat || provider.id === AccessibleViewProviderId.QuickChat) {
334
this._register(this._codeBlockContextProviderService.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView'));
335
}
336
if (provider instanceof ExtensionContentProvider) {
337
this._storageService.store(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${provider.id}`, true, StorageScope.APPLICATION, StorageTarget.USER);
338
}
339
if (provider.onDidChangeContent) {
340
this._register(provider.onDidChangeContent(() => {
341
if (this._viewContainer) { this._render(provider, this._viewContainer, showAccessibleViewHelp); }
342
}));
343
}
344
}
345
346
previous(): void {
347
const newContent = this._currentProvider?.providePreviousContent?.();
348
if (!this._currentProvider || !this._viewContainer || !newContent) {
349
return;
350
}
351
this._render(this._currentProvider, this._viewContainer, undefined, newContent);
352
}
353
354
next(): void {
355
const newContent = this._currentProvider?.provideNextContent?.();
356
if (!this._currentProvider || !this._viewContainer || !newContent) {
357
return;
358
}
359
this._render(this._currentProvider, this._viewContainer, undefined, newContent);
360
}
361
362
private _verbosityEnabled(): boolean {
363
if (!this._currentProvider) {
364
return false;
365
}
366
return isIAccessibleViewContentProvider(this._currentProvider) ? this._configurationService.getValue(this._currentProvider.verbositySettingKey) === true : this._storageService.getBoolean(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${this._currentProvider.id}`, StorageScope.APPLICATION, false);
367
}
368
369
goToSymbol(): void {
370
if (!this._currentProvider) {
371
return;
372
}
373
this._isInQuickPick = true;
374
this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider);
375
}
376
377
calculateCodeBlocks(markdown?: string): void {
378
if (!markdown) {
379
return;
380
}
381
if (this._currentProvider?.id !== AccessibleViewProviderId.PanelChat && this._currentProvider?.id !== AccessibleViewProviderId.QuickChat) {
382
return;
383
}
384
if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') {
385
// Symbols haven't been provided and we cannot parse this language
386
return;
387
}
388
const lines = markdown.split('\n');
389
this._codeBlocks = [];
390
let inBlock = false;
391
let startLine = 0;
392
393
let languageId: string | undefined;
394
lines.forEach((line, i) => {
395
if (!inBlock && line.startsWith('```')) {
396
inBlock = true;
397
startLine = i + 1;
398
languageId = line.substring(3).trim();
399
} else if (inBlock && line.endsWith('```')) {
400
inBlock = false;
401
const endLine = i;
402
const code = lines.slice(startLine, endLine).join('\n');
403
this._codeBlocks?.push({ startLine, endLine, code, languageId, chatSessionId: undefined });
404
}
405
});
406
this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0);
407
}
408
409
getSymbols(): IAccessibleViewSymbol[] | undefined {
410
const provider = this._currentProvider ? this._currentProvider : undefined;
411
if (!this._currentContent || !provider) {
412
return;
413
}
414
const symbols: IAccessibleViewSymbol[] = 'getSymbols' in provider ? provider.getSymbols?.() || [] : [];
415
if (symbols?.length) {
416
return symbols;
417
}
418
if (provider.options.language && provider.options.language !== 'markdown') {
419
// Symbols haven't been provided and we cannot parse this language
420
return;
421
}
422
const markdownTokens: marked.TokensList | undefined = marked.marked.lexer(this._currentContent);
423
if (!markdownTokens) {
424
return;
425
}
426
this._convertTokensToSymbols(markdownTokens, symbols);
427
return symbols.length ? symbols : undefined;
428
}
429
430
openHelpLink(): void {
431
if (!this._currentProvider?.options.readMoreUrl) {
432
return;
433
}
434
this._openerService.open(URI.parse(this._currentProvider.options.readMoreUrl));
435
}
436
437
configureKeybindings(unassigned: boolean): void {
438
this._isInQuickPick = true;
439
const provider = this._updateLastProvider();
440
const items = unassigned ? provider?.options?.configureKeybindingItems : provider?.options?.configuredKeybindingItems;
441
if (!items) {
442
return;
443
}
444
const disposables = this._register(new DisposableStore());
445
const quickPick: IQuickPick<IQuickPickItem> = disposables.add(this._quickInputService.createQuickPick());
446
quickPick.items = items;
447
quickPick.title = localize('keybindings', 'Configure keybindings');
448
quickPick.placeholder = localize('selectKeybinding', 'Select a command ID to configure a keybinding for it');
449
quickPick.show();
450
disposables.add(quickPick.onDidAccept(async () => {
451
const item = quickPick.selectedItems[0];
452
if (item) {
453
await this._commandService.executeCommand('workbench.action.openGlobalKeybindings', item.id);
454
}
455
quickPick.dispose();
456
}));
457
disposables.add(quickPick.onDidHide(() => {
458
if (!quickPick.selectedItems.length && provider) {
459
this.show(provider);
460
}
461
disposables.dispose();
462
this._isInQuickPick = false;
463
}));
464
}
465
466
private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void {
467
let firstListItem: string | undefined;
468
for (const token of tokens) {
469
let label: string | undefined = undefined;
470
if ('type' in token) {
471
switch (token.type) {
472
case 'heading':
473
case 'paragraph':
474
case 'code':
475
label = token.text;
476
break;
477
case 'list': {
478
const firstItem = (token as marked.Tokens.List).items[0];
479
if (!firstItem) {
480
break;
481
}
482
firstListItem = `- ${firstItem.text}`;
483
label = (token as marked.Tokens.List).items.map(i => i.text).join(', ');
484
break;
485
}
486
}
487
}
488
if (label) {
489
symbols.push({ markdownToParse: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label), firstListItem });
490
firstListItem = undefined;
491
}
492
}
493
}
494
495
showSymbol(provider: AccesibleViewContentProvider, symbol: IAccessibleViewSymbol): void {
496
if (!this._currentContent) {
497
return;
498
}
499
let lineNumber: number | undefined = symbol.lineNumber;
500
const markdownToParse = symbol.markdownToParse;
501
if (lineNumber === undefined && markdownToParse === undefined) {
502
// No symbols provided and we cannot parse this language
503
return;
504
}
505
506
if (lineNumber === undefined && markdownToParse) {
507
// Note that this scales poorly, thus isn't used for worst case scenarios like the terminal, for which a line number will always be provided.
508
// Parse the markdown to find the line number
509
const index = this._currentContent.split('\n').findIndex(line => line.includes(markdownToParse.split('\n')[0]) || (symbol.firstListItem && line.includes(symbol.firstListItem))) ?? -1;
510
if (index >= 0) {
511
lineNumber = index + 1;
512
}
513
}
514
if (lineNumber === undefined) {
515
return;
516
}
517
this._isInQuickPick = false;
518
this.show(provider, undefined, undefined, { lineNumber, column: 1 });
519
this._updateContextKeys(provider, true);
520
}
521
522
disableHint(): void {
523
if (!isIAccessibleViewContentProvider(this._currentProvider)) {
524
return;
525
}
526
this._configurationService.updateValue(this._currentProvider?.verbositySettingKey, false);
527
alert(localize('disableAccessibilityHelp', '{0} accessibility verbosity is now disabled', this._currentProvider.verbositySettingKey));
528
}
529
530
private _updateContextKeys(provider: AccesibleViewContentProvider, shown: boolean): void {
531
if (provider.options.type === AccessibleViewType.Help) {
532
this._accessiblityHelpIsShown.set(shown);
533
this._accessibleViewIsShown.reset();
534
} else {
535
this._accessibleViewIsShown.set(shown);
536
this._accessiblityHelpIsShown.reset();
537
}
538
this._accessibleViewSupportsNavigation.set(provider.provideNextContent !== undefined || provider.providePreviousContent !== undefined);
539
this._accessibleViewVerbosityEnabled.set(this._verbosityEnabled());
540
this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false);
541
}
542
543
private _updateContent(provider: AccesibleViewContentProvider, updatedContent?: string): void {
544
let content = updatedContent ?? provider.provideContent();
545
if (provider.options.type === AccessibleViewType.View) {
546
this._currentContent = content;
547
this._hasUnassignedKeybindings.reset();
548
this._hasAssignedKeybindings.reset();
549
return;
550
}
551
const readMoreLinkHint = this._readMoreHint(provider);
552
const disableHelpHint = this._disableVerbosityHint(provider);
553
const screenReaderModeHint = this._screenReaderModeHint(provider);
554
const exitThisDialogHint = this._exitDialogHint(provider);
555
let configureKbHint = '';
556
let configureAssignedKbHint = '';
557
const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, screenReaderModeHint + content + readMoreLinkHint + disableHelpHint + exitThisDialogHint);
558
if (resolvedContent) {
559
content = resolvedContent.content.value;
560
if (resolvedContent.configureKeybindingItems) {
561
provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems;
562
this._hasUnassignedKeybindings.set(true);
563
configureKbHint = this._configureUnassignedKbHint();
564
} else {
565
this._hasAssignedKeybindings.reset();
566
}
567
if (resolvedContent.configuredKeybindingItems) {
568
provider.options.configuredKeybindingItems = resolvedContent.configuredKeybindingItems;
569
this._hasAssignedKeybindings.set(true);
570
configureAssignedKbHint = this._configureAssignedKbHint();
571
} else {
572
this._hasAssignedKeybindings.reset();
573
}
574
}
575
this._currentContent = content + configureKbHint + configureAssignedKbHint;
576
}
577
578
private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean, updatedContent?: string): IDisposable {
579
this._currentProvider = provider;
580
this._accessibleViewCurrentProviderId.set(provider.id);
581
const verbose = this._verbosityEnabled();
582
this._updateContent(provider, updatedContent);
583
this.calculateCodeBlocks(this._currentContent);
584
this._updateContextKeys(provider, true);
585
const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus();
586
this._getTextModel(URI.from({ path: `accessible-view-${provider.id}`, scheme: Schemas.accessibleView, fragment: this._currentContent })).then((model) => {
587
if (!model) {
588
return;
589
}
590
this._editorWidget.setModel(model);
591
const domNode = this._editorWidget.getDomNode();
592
if (!domNode) {
593
return;
594
}
595
model.setLanguage(provider.options.language ?? 'markdown');
596
container.appendChild(this._container);
597
let actionsHint = '';
598
const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || provider.actions?.length;
599
if (verbose && !showAccessibleViewHelp && hasActions) {
600
actionsHint = provider.options.position ? localize('ariaAccessibleViewActionsBottom', 'Explore actions such as disabling this hint (Shift+Tab), use Escape to exit this dialog.') : localize('ariaAccessibleViewActions', 'Explore actions such as disabling this hint (Shift+Tab).');
601
}
602
let ariaLabel = provider.options.type === AccessibleViewType.Help ? localize('accessibility-help', "Accessibility Help") : localize('accessible-view', "Accessible View");
603
this._title.textContent = ariaLabel;
604
if (actionsHint && provider.options.type === AccessibleViewType.View) {
605
ariaLabel = localize('accessible-view-hint', "Accessible View, {0}", actionsHint);
606
} else if (actionsHint) {
607
ariaLabel = localize('accessibility-help-hint', "Accessibility Help, {0}", actionsHint);
608
}
609
if (isWindows && widgetIsFocused) {
610
// prevent the screen reader on windows from reading
611
// the aria label again when it's refocused
612
ariaLabel = '';
613
}
614
this._editorWidget.updateOptions({ ariaLabel });
615
this._editorWidget.focus();
616
if (this._currentProvider?.options.position) {
617
const position = this._editorWidget.getPosition();
618
const isDefaultPosition = position?.lineNumber === 1 && position.column === 1;
619
if (this._currentProvider.options.position === 'bottom' || this._currentProvider.options.position === 'initial-bottom' && isDefaultPosition) {
620
const lastLine = this.editorWidget.getModel()?.getLineCount();
621
const position = lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined;
622
if (position) {
623
this._editorWidget.setPosition(position);
624
this._editorWidget.revealLine(position.lineNumber);
625
}
626
}
627
}
628
});
629
this._updateToolbar(this._currentProvider.actions, provider.options.type);
630
631
const hide = (e?: KeyboardEvent | IKeyboardEvent): void => {
632
if (!this._isInQuickPick) {
633
provider.onClose();
634
}
635
e?.stopPropagation();
636
this._contextViewService.hideContextView();
637
if (this._isInQuickPick) {
638
return;
639
}
640
this._updateContextKeys(provider, false);
641
this._lastProvider = undefined;
642
this._currentContent = undefined;
643
this._currentProvider?.dispose();
644
this._currentProvider = undefined;
645
};
646
const disposableStore = new DisposableStore();
647
disposableStore.add(this._editorWidget.onKeyDown((e) => {
648
if (e.keyCode === KeyCode.Enter) {
649
this._commandService.executeCommand('editor.action.openLink');
650
} else if (e.keyCode === KeyCode.Escape || shouldHide(e.browserEvent, this._keybindingService, this._configurationService)) {
651
hide(e);
652
} else if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) {
653
const url: string = provider.options.readMoreUrl;
654
alert(AccessibilityHelpNLS.openingDocs);
655
this._openerService.open(URI.parse(url));
656
e.preventDefault();
657
e.stopPropagation();
658
}
659
if (provider instanceof AccessibleContentProvider) {
660
provider.onKeyDown?.(e);
661
}
662
}));
663
disposableStore.add(addDisposableListener(this._toolbar.getElement(), EventType.KEY_DOWN, (e: KeyboardEvent) => {
664
const keyboardEvent = new StandardKeyboardEvent(e);
665
if (keyboardEvent.equals(KeyCode.Escape)) {
666
hide(e);
667
}
668
}));
669
disposableStore.add(this._editorWidget.onDidBlurEditorWidget(() => {
670
if (!isActiveElement(this._toolbar.getElement())) {
671
hide();
672
}
673
}));
674
disposableStore.add(this._editorWidget.onDidContentSizeChange(() => this._layout()));
675
disposableStore.add(this._layoutService.onDidLayoutActiveContainer(() => this._layout()));
676
return disposableStore;
677
}
678
679
private _updateToolbar(providedActions?: IAction[], type?: AccessibleViewType): void {
680
this._toolbar.setAriaLabel(type === AccessibleViewType.Help ? localize('accessibleHelpToolbar', 'Accessibility Help') : localize('accessibleViewToolbar', "Accessible View"));
681
const toolbarMenu = this._register(this._menuService.createMenu(MenuId.AccessibleView, this._contextKeyService));
682
const menuActions = getFlatActionBarActions(toolbarMenu.getActions({}));
683
if (providedActions) {
684
for (const providedAction of providedActions) {
685
providedAction.class = providedAction.class || ThemeIcon.asClassName(Codicon.primitiveSquare);
686
providedAction.checked = undefined;
687
}
688
this._toolbar.setActions([...providedActions, ...menuActions]);
689
} else {
690
this._toolbar.setActions(menuActions);
691
}
692
}
693
694
private _layout(): void {
695
const dimension = this._layoutService.activeContainerDimension;
696
const maxHeight = dimension.height && dimension.height * .4;
697
const height = Math.min(maxHeight, this._editorWidget.getContentHeight());
698
const width = Math.min(dimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH);
699
this._editorWidget.layout({ width, height });
700
}
701
702
private async _getTextModel(resource: URI): Promise<ITextModel | null> {
703
const existing = this._modelService.getModel(resource);
704
if (existing && !existing.isDisposed()) {
705
return existing;
706
}
707
return this._modelService.createModel(resource.fragment, null, resource, false);
708
}
709
710
private _goToSymbolsSupported(): boolean {
711
if (!this._currentProvider) {
712
return false;
713
}
714
return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AccessibleContentProvider && !!this._currentProvider.getSymbols?.());
715
}
716
717
private _updateLastProvider(): AccesibleViewContentProvider | undefined {
718
const provider = this._currentProvider;
719
if (!provider) {
720
return;
721
}
722
const lastProvider = provider instanceof AccessibleContentProvider ? new AccessibleContentProvider(
723
provider.id,
724
provider.options,
725
provider.provideContent.bind(provider),
726
provider.onClose.bind(provider),
727
provider.verbositySettingKey,
728
provider.onOpen?.bind(provider),
729
provider.actions,
730
provider.provideNextContent?.bind(provider),
731
provider.providePreviousContent?.bind(provider),
732
provider.onDidChangeContent?.bind(provider),
733
provider.onKeyDown?.bind(provider),
734
provider.getSymbols?.bind(provider),
735
) : new ExtensionContentProvider(
736
provider.id,
737
provider.options,
738
provider.provideContent.bind(provider),
739
provider.onClose.bind(provider),
740
provider.onOpen?.bind(provider),
741
provider.provideNextContent?.bind(provider),
742
provider.providePreviousContent?.bind(provider),
743
provider.actions,
744
provider.onDidChangeContent?.bind(provider),
745
);
746
return lastProvider;
747
}
748
749
public showAccessibleViewHelp(): void {
750
const lastProvider = this._updateLastProvider();
751
if (!lastProvider) {
752
return;
753
}
754
let accessibleViewHelpProvider;
755
if (lastProvider instanceof AccessibleContentProvider) {
756
accessibleViewHelpProvider = new AccessibleContentProvider(
757
lastProvider.id,
758
{ type: AccessibleViewType.Help },
759
() => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()),
760
() => {
761
this._contextViewService.hideContextView();
762
// HACK: Delay to allow the context view to hide #207638
763
queueMicrotask(() => this.show(lastProvider));
764
},
765
lastProvider.verbositySettingKey
766
);
767
} else {
768
accessibleViewHelpProvider = new ExtensionContentProvider(
769
lastProvider.id,
770
{ type: AccessibleViewType.Help },
771
() => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()),
772
() => {
773
this._contextViewService.hideContextView();
774
// HACK: Delay to allow the context view to hide #207638
775
queueMicrotask(() => this.show(lastProvider));
776
},
777
);
778
}
779
this._contextViewService.hideContextView();
780
// HACK: Delay to allow the context view to hide #186514
781
if (accessibleViewHelpProvider) {
782
queueMicrotask(() => this.show(accessibleViewHelpProvider, undefined, true));
783
}
784
}
785
786
private _accessibleViewHelpDialogContent(providerHasSymbols?: boolean): string {
787
const navigationHint = this._navigationHint();
788
const goToSymbolHint = this._goToSymbolHint(providerHasSymbols);
789
const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab).");
790
const chatHints = this._getChatHints();
791
792
let hint = localize('intro', "In the accessible view, you can:\n");
793
if (navigationHint) {
794
hint += ' - ' + navigationHint + '\n';
795
}
796
if (goToSymbolHint) {
797
hint += ' - ' + goToSymbolHint + '\n';
798
}
799
if (toolbarHint) {
800
hint += ' - ' + toolbarHint + '\n';
801
}
802
if (chatHints) {
803
hint += chatHints;
804
}
805
return hint;
806
}
807
808
private _getChatHints(): string | undefined {
809
if (this._currentProvider?.id !== AccessibleViewProviderId.PanelChat && this._currentProvider?.id !== AccessibleViewProviderId.QuickChat) {
810
return;
811
}
812
return [localize('insertAtCursor', " - Insert the code block at the cursor{0}.", '<keybinding:workbench.action.chat.insertCodeBlock>'),
813
localize('insertIntoNewFile', " - Insert the code block into a new file{0}.", '<keybinding:workbench.action.chat.insertIntoNewFile>'),
814
localize('runInTerminal', " - Run the code block in the terminal{0}.\n", '<keybinding:workbench.action.chat.runInTerminal>')].join('\n');
815
}
816
817
private _navigationHint(): string {
818
return localize('accessibleViewNextPreviousHint', "Show the next item{0} or previous item{1}.", `<keybinding:${AccessibilityCommandId.ShowNext}`, `<keybinding:${AccessibilityCommandId.ShowPrevious}>`);
819
}
820
821
private _disableVerbosityHint(provider: AccesibleViewContentProvider): string {
822
if (provider.options.type === AccessibleViewType.Help && this._verbosityEnabled()) {
823
return localize('acessibleViewDisableHint', "\nDisable accessibility verbosity for this feature{0}.", `<keybinding:${AccessibilityCommandId.DisableVerbosityHint}>`);
824
}
825
return '';
826
}
827
828
private _goToSymbolHint(providerHasSymbols?: boolean): string | undefined {
829
if (!providerHasSymbols) {
830
return;
831
}
832
return localize('goToSymbolHint', 'Go to a symbol{0}.', `<keybinding:${AccessibilityCommandId.GoToSymbol}>`);
833
}
834
835
private _configureUnassignedKbHint(): string {
836
const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel();
837
const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Unassigned Keybindings.';
838
return localize('configureKb', '\nConfigure keybindings for commands that lack them {0}.', keybindingToConfigureQuickPick);
839
}
840
841
private _configureAssignedKbHint(): string {
842
const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings)?.getAriaLabel();
843
const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Assigned Keybindings.';
844
return localize('configureKbAssigned', '\nConfigure keybindings for commands that already have assignments {0}.', keybindingToConfigureQuickPick);
845
}
846
847
private _screenReaderModeHint(provider: AccesibleViewContentProvider): string {
848
const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized();
849
let screenReaderModeHint = '';
850
const turnOnMessage = (
851
isMacintosh
852
? AccessibilityHelpNLS.changeConfigToOnMac
853
: AccessibilityHelpNLS.changeConfigToOnWinLinux
854
);
855
if (accessibilitySupport && provider.id === AccessibleViewProviderId.Editor) {
856
screenReaderModeHint = AccessibilityHelpNLS.auto_on;
857
screenReaderModeHint += '\n';
858
} else if (!accessibilitySupport) {
859
screenReaderModeHint = AccessibilityHelpNLS.auto_off + '\n' + turnOnMessage;
860
screenReaderModeHint += '\n';
861
}
862
return screenReaderModeHint;
863
}
864
865
private _exitDialogHint(provider: AccesibleViewContentProvider): string {
866
return this._verbosityEnabled() && !provider.options.position ? localize('exit', '\nExit this dialog (Escape).') : '';
867
}
868
869
private _readMoreHint(provider: AccesibleViewContentProvider): string {
870
return provider.options.readMoreUrl ? localize("openDoc", "\nOpen a browser window with more information related to accessibility{0}.", `<keybinding:${AccessibilityCommandId.AccessibilityHelpOpenHelpLink}>`) : '';
871
}
872
}
873
874
export class AccessibleViewService extends Disposable implements IAccessibleViewService {
875
declare readonly _serviceBrand: undefined;
876
private _accessibleView: AccessibleView | undefined;
877
878
constructor(
879
@IInstantiationService private readonly _instantiationService: IInstantiationService,
880
@IConfigurationService private readonly _configurationService: IConfigurationService,
881
@IKeybindingService private readonly _keybindingService: IKeybindingService
882
) {
883
super();
884
}
885
886
show(provider: AccesibleViewContentProvider, position?: Position): void {
887
if (!this._accessibleView) {
888
this._accessibleView = this._register(this._instantiationService.createInstance(AccessibleView));
889
}
890
this._accessibleView.show(provider, undefined, undefined, position);
891
}
892
configureKeybindings(unassigned: boolean): void {
893
this._accessibleView?.configureKeybindings(unassigned);
894
}
895
openHelpLink(): void {
896
this._accessibleView?.openHelpLink();
897
}
898
showLastProvider(id: AccessibleViewProviderId): void {
899
this._accessibleView?.showLastProvider(id);
900
}
901
next(): void {
902
this._accessibleView?.next();
903
}
904
previous(): void {
905
this._accessibleView?.previous();
906
}
907
goToSymbol(): void {
908
this._accessibleView?.goToSymbol();
909
}
910
getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {
911
if (!this._configurationService.getValue(verbositySettingKey)) {
912
return null;
913
}
914
const keybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibleView)?.getAriaLabel();
915
let hint = null;
916
if (keybinding) {
917
hint = localize('acessibleViewHint', "Inspect this in the accessible view with {0}", keybinding);
918
} else {
919
hint = localize('acessibleViewHintNoKbEither', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.");
920
}
921
return hint;
922
}
923
disableHint(): void {
924
this._accessibleView?.disableHint();
925
}
926
showAccessibleViewHelp(): void {
927
this._accessibleView?.showAccessibleViewHelp();
928
}
929
getPosition(id: AccessibleViewProviderId): Position | undefined {
930
return this._accessibleView?.getPosition(id) ?? undefined;
931
}
932
getLastPosition(): Position | undefined {
933
const lastLine = this._accessibleView?.editorWidget.getModel()?.getLineCount();
934
return lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined;
935
}
936
setPosition(position: Position, reveal?: boolean, select?: boolean): void {
937
this._accessibleView?.setPosition(position, reveal, select);
938
}
939
getCodeBlockContext(): ICodeBlockActionContext | undefined {
940
return this._accessibleView?.getCodeBlockContext();
941
}
942
navigateToCodeBlock(type: 'next' | 'previous'): void {
943
this._accessibleView?.navigateToCodeBlock(type);
944
}
945
}
946
947
class AccessibleViewSymbolQuickPick {
948
constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) {
949
950
}
951
show(provider: AccesibleViewContentProvider): void {
952
const disposables = new DisposableStore();
953
const quickPick = disposables.add(this._quickInputService.createQuickPick<IAccessibleViewSymbol>());
954
quickPick.placeholder = localize('accessibleViewSymbolQuickPickPlaceholder', "Type to search symbols");
955
quickPick.title = localize('accessibleViewSymbolQuickPickTitle', "Go to Symbol Accessible View");
956
const picks = [];
957
const symbols = this._accessibleView.getSymbols();
958
if (!symbols) {
959
return;
960
}
961
for (const symbol of symbols) {
962
picks.push({
963
label: symbol.label,
964
ariaLabel: symbol.ariaLabel,
965
firstListItem: symbol.firstListItem,
966
lineNumber: symbol.lineNumber,
967
endLineNumber: symbol.endLineNumber,
968
markdownToParse: symbol.markdownToParse
969
});
970
}
971
quickPick.canSelectMany = false;
972
quickPick.items = picks;
973
quickPick.show();
974
disposables.add(quickPick.onDidAccept(() => {
975
this._accessibleView.showSymbol(provider, quickPick.selectedItems[0]);
976
quickPick.hide();
977
}));
978
disposables.add(quickPick.onDidHide(() => {
979
if (quickPick.selectedItems.length === 0) {
980
// this was escaped, so refocus the accessible view
981
this._accessibleView.show(provider);
982
}
983
disposables.dispose();
984
}));
985
}
986
}
987
988
989
function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean {
990
if (!configurationService.getValue(AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress)) {
991
return false;
992
}
993
const standardKeyboardEvent = new StandardKeyboardEvent(event);
994
const resolveResult = keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target);
995
996
const isValidChord = resolveResult.kind === ResultKind.MoreChordsNeeded;
997
if (keybindingService.inChordMode || isValidChord) {
998
return false;
999
}
1000
return shouldHandleKey(event) && !event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey;
1001
}
1002
1003
function shouldHandleKey(event: KeyboardEvent): boolean {
1004
return !!event.code.match(/^(Key[A-Z]|Digit[0-9]|Equal|Comma|Period|Slash|Quote|Backquote|Backslash|Minus|Semicolon|Space|Enter)$/);
1005
}
1006
1007