Repository: microsoft/vscode (TypeScript)
Date: 2021-08-31 00:33:58+00:00
Task type: Feature

Issue:
Announce once a language is automatically detected
1. Untitled file
2. Start typing
3. Once a language is automatically selected we should announce that, such that screen readers are aware that we have automatically selected a language

The message should be concise, something like "Python Language Mode is Set"

Also when "Auto Detect" is triggered from the Select Language Mode. But we should announce only if the mode changed.


Issue discussion:
Was "Auto Detect" done by @bpasero?
Probably.
Ok, I'll wait for him to come back. I'd like to do this generically for all things that change the mode. I'm even thinking about Notebooks influencing the editor language based on the kernel.
Yeah, having a general solution makes good sense.
@isidorn do you have any thoughts on where the best place for this announcement might live?
I would probably do it as soon as you change the `mode` of the `Editor`.
It should not live in your service, since the service is just guessing, it know nothing about editors and modes. So I think clients of the service should take care of it.
Yeah sorry I guess I was wondering if there was a particular service for announcing arbitrary text. Then maybe I can put that in whatever service handles rendering of an editor?
@TylerLeonhardt there is no service, there are just helper methods [`alert`](https://github.com/microsoft/vscode/blob/main/src/vs/base/browser/ui/aria/aria.ts#L49). 

I do not think this should be in a service that renders the editor. Since this should only be announced if the language is automatically set (since that is surprising). We should not announce in all other cases.
@isidorn, I was going to add the `alert` here:
https://github.com/microsoft/vscode/blob/c830429cb1da0b4e20d15a031f24c8f9f6e1460a/src/vs/workbench/common/editor/textEditorModel.ts#L121

but it looks like it violates imports:
![image](https://user-images.githubusercontent.com/2644648/130526659-f797f0ec-7bf1-4e12-942a-aa1b1846d28e.png)

I'm surprised the `alert` is under browser honestly. Could that be moved?
@TylerLeonhardt `aria` is using browser specific approach to make a screen reader announce something. It is using `HTMLElements` and `aria-live` flags. Also `aria` stands for `Accessible Rich Internet Applications`, so it is very browser specific. I would not move it.

So I suggest to call it one layer above if possible.
Alternatively we need to expose this from a service in `common` that is then implemented in `browser`?
@bpasero yeah we could move it to a service, but I like the simplicity of just importing a helper method.

Solution patch:
diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts
--- a/src/vs/editor/standalone/browser/standaloneServices.ts
+++ b/src/vs/editor/standalone/browser/standaloneServices.ts
@@ -47,7 +47,7 @@ import { MarkerDecorationsService } from 'vs/editor/common/services/markerDecora
 import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
 import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
 import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions';
-import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService';
+import { AccessibilityService } from 'vs/platform/accessibility/browser/accessibilityService';
 import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
 import { BrowserClipboardService } from 'vs/platform/clipboard/browser/clipboardService';
 import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
diff --git a/src/vs/platform/accessibility/common/accessibilityService.ts b/src/vs/platform/accessibility/browser/accessibilityService.ts
similarity index 96%
rename from src/vs/platform/accessibility/common/accessibilityService.ts
rename to src/vs/platform/accessibility/browser/accessibilityService.ts
--- a/src/vs/platform/accessibility/common/accessibilityService.ts
+++ b/src/vs/platform/accessibility/browser/accessibilityService.ts
@@ -3,6 +3,7 @@
  *  Licensed under the MIT License. See License.txt in the project root for license information.
  *--------------------------------------------------------------------------------------------*/
 
+import { alert } from 'vs/base/browser/ui/aria/aria';
 import { Emitter, Event } from 'vs/base/common/event';
 import { Disposable } from 'vs/base/common/lifecycle';
 import { AccessibilitySupport, CONTEXT_ACCESSIBILITY_MODE_ENABLED, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
@@ -58,4 +59,8 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe
 		this._accessibilitySupport = accessibilitySupport;
 		this._onDidChangeScreenReaderOptimized.fire();
 	}
+
+	alert(message: string): void {
+		alert(message);
+	}
 }
diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts
--- a/src/vs/platform/accessibility/common/accessibility.ts
+++ b/src/vs/platform/accessibility/common/accessibility.ts
@@ -18,6 +18,7 @@ export interface IAccessibilityService {
 	isScreenReaderOptimized(): boolean;
 	getAccessibilitySupport(): AccessibilitySupport;
 	setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void;
+	alert(message: string): void;
 }
 
 export const enum AccessibilitySupport {
diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts
--- a/src/vs/workbench/common/editor/textEditorModel.ts
+++ b/src/vs/workbench/common/editor/textEditorModel.ts
@@ -15,6 +15,8 @@ import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
 import { withUndefinedAsNull } from 'vs/base/common/types';
 import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
 import { ThrottledDelayer } from 'vs/base/common/async';
+import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
+import { localize } from 'vs/nls';
 
 /**
  * The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
@@ -34,6 +36,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
 		@IModelService protected modelService: IModelService,
 		@IModeService protected modeService: IModeService,
 		@ILanguageDetectionService private readonly languageDetectionService: ILanguageDetectionService,
+		@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
 		textEditorModelHandle?: URI
 	) {
 		super();
@@ -114,6 +117,10 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
 		const lang = await this.languageDetectionService.detectLanguage(this.textEditorModelHandle);
 		if (lang && !this.isDisposed()) {
 			this.setModeInternal(lang);
+			const languageName = this.modeService.getLanguageName(lang);
+			if (languageName) {
+				this.accessibilityService.alert(localize('languageAutoDetected', "Language {0} was automatically detected and set as the language mode.", languageName));
+			}
 		}
 	}
 
diff --git a/src/vs/workbench/common/editor/textResourceEditorModel.ts b/src/vs/workbench/common/editor/textResourceEditorModel.ts
--- a/src/vs/workbench/common/editor/textResourceEditorModel.ts
+++ b/src/vs/workbench/common/editor/textResourceEditorModel.ts
@@ -8,6 +8,7 @@ import { URI } from 'vs/base/common/uri';
 import { IModeService } from 'vs/editor/common/services/modeService';
 import { IModelService } from 'vs/editor/common/services/modelService';
 import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
+import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
 
 /**
  * An editor model for in-memory, readonly text content that
@@ -19,9 +20,10 @@ export class TextResourceEditorModel extends BaseTextEditorModel {
 		resource: URI,
 		@IModeService modeService: IModeService,
 		@IModelService modelService: IModelService,
-		@ILanguageDetectionService languageDetectionService: ILanguageDetectionService
+		@ILanguageDetectionService languageDetectionService: ILanguageDetectionService,
+		@IAccessibilityService accessibilityService: IAccessibilityService,
 	) {
-		super(modelService, modeService, languageDetectionService, resource);
+		super(modelService, modeService, languageDetectionService, accessibilityService, resource);
 	}
 
 	override dispose(): void {
diff --git a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts
--- a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts
+++ b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts
@@ -9,7 +9,7 @@ import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/enviro
 import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
 import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
 import { Registry } from 'vs/platform/registry/common/platform';
-import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService';
+import { AccessibilityService } from 'vs/platform/accessibility/browser/accessibilityService';
 import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
 import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
 import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
--- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
+++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
@@ -27,6 +27,7 @@ import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textMo
 import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
 import { IPathService } from 'vs/workbench/services/path/common/pathService';
 import { extUri } from 'vs/base/common/resources';
+import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
 
 interface IBackupMetaData extends IWorkingCopyBackupMeta {
 	mtime: number;
@@ -111,9 +112,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
 		@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService,
 		@ILabelService private readonly labelService: ILabelService,
 		@ILanguageDetectionService languageDetectionService: ILanguageDetectionService,
+		@IAccessibilityService accessibilityService: IAccessibilityService,
 		@IPathService private readonly pathService: IPathService
 	) {
-		super(modelService, modeService, languageDetectionService);
+		super(modelService, modeService, languageDetectionService, accessibilityService);
 
 		// Make known to working copy service
 		this._register(this.workingCopyService.registerWorkingCopy(this));
diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts
--- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts
+++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts
@@ -27,6 +27,7 @@ import { getCharContainingOffset } from 'vs/base/common/strings';
 import { UTF8 } from 'vs/workbench/services/textfile/common/encoding';
 import { bufferToStream, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
 import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
+import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
 
 export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy {
 
@@ -136,12 +137,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
 		@ITextFileService private readonly textFileService: ITextFileService,
 		@ILabelService private readonly labelService: ILabelService,
 		@IEditorService private readonly editorService: IEditorService,
-		@ILanguageDetectionService languageDetectionService: ILanguageDetectionService
+		@ILanguageDetectionService languageDetectionService: ILanguageDetectionService,
+		@IAccessibilityService accessibilityService: IAccessibilityService,
 	) {
-		super(
-			modelService,
-			modeService,
-			languageDetectionService);
+		super(modelService, modeService, languageDetectionService, accessibilityService);
 
 		// Make known to working copy service
 		this._register(this.workingCopyService.registerWorkingCopy(this));
diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts
--- a/src/vs/workbench/workbench.web.main.ts
+++ b/src/vs/workbench/workbench.web.main.ts
@@ -79,7 +79,7 @@ import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/
 import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
 import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
 import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
-import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService';
+import { AccessibilityService } from 'vs/platform/accessibility/browser/accessibilityService';
 import { ICustomEndpointTelemetryService } from 'vs/platform/telemetry/common/telemetry';
 import { NullEndpointTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
 import { ITitleService } from 'vs/workbench/services/title/common/titleService';


Test patch:
diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts
--- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts
+++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts
@@ -29,7 +29,7 @@ import { EditorModel } from 'vs/workbench/common/editor/editorModel';
 import { Mimes } from 'vs/base/common/mime';
 import { LanguageDetectionService } from 'vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl';
 import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
-import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices';
+import { TestAccessibilityService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices';
 
 suite('EditorModel', () => {
 
@@ -88,7 +88,7 @@ suite('EditorModel', () => {
 	test('BaseTextEditorModel', async () => {
 		let modelService = stubModelService(instantiationService);
 
-		const model = new MyTextEditorModel(modelService, modeService, instantiationService.createInstance(LanguageDetectionService));
+		const model = new MyTextEditorModel(modelService, modeService, instantiationService.createInstance(LanguageDetectionService), instantiationService.createInstance(TestAccessibilityService));
 		await model.resolve();
 
 		model.createTextEditorModel(createTextBufferFactory('foo'), null!, Mimes.text);
diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts
--- a/src/vs/workbench/test/browser/workbenchTestServices.ts
+++ b/src/vs/workbench/test/browser/workbenchTestServices.ts
@@ -449,6 +449,7 @@ export class TestAccessibilityService implements IAccessibilityService {
 	alwaysUnderlineAccessKeys(): Promise<boolean> { return Promise.resolve(false); }
 	setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { }
 	getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; }
+	alert(message: string): void { }
 }
 
 export class TestDecorationsService implements IDecorationsService {