Skip to content

Commit 7a82e67

Browse files
Another approach to stop language detection from stomping over changes (microsoft#159782)
* Another approach to stop language detection from stomping over changes This change does two important things: 1. Has the higher level `EditorModel` listen for when the lower level model changes languages (this is important because many things change the language at that level instead of this higher level) 2. adds a `reason` to this event so that the language detection can ignore language changes events that it caused. * use a better pattern * 💄 * 💄 * also register the disposable * change to source and have a couple places set the source to the command id * add a couple tests Co-authored-by: Benjamin Pasero <[email protected]>
1 parent 4230c22 commit 7a82e67

File tree

23 files changed

+131
-47
lines changed

23 files changed

+131
-47
lines changed

src/vs/editor/common/model.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -849,9 +849,11 @@ export interface ITextModel {
849849

850850
/**
851851
* Set the current language mode associated with the model.
852+
* @param languageId The new language.
853+
* @param source The source of the call that set the language.
852854
* @internal
853855
*/
854-
setMode(languageId: string): void;
856+
setMode(languageId: string, source?: string): void;
855857

856858
/**
857859
* Returns the real (inner-most) language mode at a given position.

src/vs/editor/common/model/textModel.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1907,8 +1907,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
19071907
return this.tokenization.getLanguageId();
19081908
}
19091909

1910-
public setMode(languageId: string): void {
1911-
this.tokenization.setLanguageId(languageId);
1910+
public setMode(languageId: string, source?: string): void {
1911+
this.tokenization.setLanguageId(languageId, source);
19121912
}
19131913

19141914
public getLanguageIdAtPosition(lineNumber: number, column: number): string {

src/vs/editor/common/model/tokenizationTextModelPart.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -485,15 +485,16 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz
485485
return lineTokens.getLanguageId(lineTokens.findTokenIndexAtOffset(position.column - 1));
486486
}
487487

488-
public setLanguageId(languageId: string): void {
488+
public setLanguageId(languageId: string, source: string = 'api'): void {
489489
if (this._languageId === languageId) {
490490
// There's nothing to do
491491
return;
492492
}
493493

494494
const e: IModelLanguageChangedEvent = {
495495
oldLanguage: this._languageId,
496-
newLanguage: languageId
496+
newLanguage: languageId,
497+
source
497498
};
498499

499500
this._languageId = languageId;

src/vs/editor/common/services/model.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface IModelService {
2222

2323
updateModel(model: ITextModel, value: string | ITextBufferFactory): void;
2424

25-
setMode(model: ITextModel, languageSelection: ILanguageSelection): void;
25+
setMode(model: ITextModel, languageSelection: ILanguageSelection, source?: string): void;
2626

2727
destroyModel(resource: URI): void;
2828

src/vs/editor/common/services/modelService.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ class ModelData implements IDisposable {
9191
this._disposeLanguageSelection();
9292
}
9393

94-
public setLanguage(languageSelection: ILanguageSelection): void {
94+
public setLanguage(languageSelection: ILanguageSelection, source?: string): void {
9595
this._disposeLanguageSelection();
9696
this._languageSelection = languageSelection;
97-
this._languageSelectionListener = this._languageSelection.onDidChange(() => this.model.setMode(languageSelection.languageId));
98-
this.model.setMode(languageSelection.languageId);
97+
this._languageSelectionListener = this._languageSelection.onDidChange(() => this.model.setMode(languageSelection.languageId, source));
98+
this.model.setMode(languageSelection.languageId, source);
9999
}
100100
}
101101

@@ -516,15 +516,15 @@ export class ModelService extends Disposable implements IModelService {
516516
return modelData.model;
517517
}
518518

519-
public setMode(model: ITextModel, languageSelection: ILanguageSelection): void {
519+
public setMode(model: ITextModel, languageSelection: ILanguageSelection, source?: string): void {
520520
if (!languageSelection) {
521521
return;
522522
}
523523
const modelData = this._models[MODEL_ID(model.uri)];
524524
if (!modelData) {
525525
return;
526526
}
527-
modelData.setLanguage(languageSelection);
527+
modelData.setLanguage(languageSelection, source);
528528
}
529529

530530
public destroyModel(resource: URI): void {

src/vs/editor/common/textModelEvents.ts

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export interface IModelLanguageChangedEvent {
1919
* New language
2020
*/
2121
readonly newLanguage: string;
22+
23+
/**
24+
* Source of the call that caused the event.
25+
*/
26+
readonly source: string;
2227
}
2328

2429
/**

src/vs/editor/common/tokenizationTextModelPart.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export interface ITokenizationTextModelPart {
9595
getLanguageId(): string;
9696
getLanguageIdAtPosition(lineNumber: number, column: number): string;
9797

98-
setLanguageId(languageId: string): void;
98+
setLanguageId(languageId: string, source?: string): void;
9999

100100
readonly backgroundTokenizationState: BackgroundTokenizationState;
101101
readonly onBackgroundTokenizationStateChanged: Event<void>;

src/vs/monaco.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -2634,6 +2634,10 @@ declare namespace monaco.editor {
26342634
* New language
26352635
*/
26362636
readonly newLanguage: string;
2637+
/**
2638+
* Source of the call that caused the event.
2639+
*/
2640+
readonly source: string;
26372641
}
26382642

26392643
/**

src/vs/workbench/browser/parts/editor/editorStatus.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ class SideBySideEditorLanguageSupport implements ILanguageSupport {
7171

7272
constructor(private primary: ILanguageSupport, private secondary: ILanguageSupport) { }
7373

74-
setLanguageId(languageId: string): void {
75-
[this.primary, this.secondary].forEach(editor => editor.setLanguageId(languageId));
74+
setLanguageId(languageId: string, source?: string): void {
75+
[this.primary, this.secondary].forEach(editor => editor.setLanguageId(languageId, source));
7676
}
7777
}
7878

@@ -1237,7 +1237,7 @@ export class ChangeLanguageAction extends Action {
12371237

12381238
// Change language
12391239
if (typeof languageSelection !== 'undefined') {
1240-
languageSupport.setLanguageId(languageSelection.languageId);
1240+
languageSupport.setLanguageId(languageSelection.languageId, ChangeLanguageAction.ID);
12411241

12421242
if (resource?.scheme === Schemas.untitled) {
12431243
type SetUntitledDocumentLanguageEvent = { to: string; from: string; modelPreference: string };

src/vs/workbench/common/editor/textEditorModel.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IModelService } from 'vs/editor/common/services/model';
1313
import { MutableDisposable } from 'vs/base/common/lifecycle';
1414
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
1515
import { withUndefinedAsNull } from 'vs/base/common/types';
16-
import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
16+
import { ILanguageDetectionService, LanguageDetectionLanguageEventSource } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
1717
import { ThrottledDelayer } from 'vs/base/common/async';
1818
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
1919
import { localize } from 'vs/nls';
@@ -78,15 +78,15 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
7878
private _hasLanguageSetExplicitly: boolean = false;
7979
get hasLanguageSetExplicitly(): boolean { return this._hasLanguageSetExplicitly; }
8080

81-
setLanguageId(languageId: string): void {
81+
setLanguageId(languageId: string, source?: string): void {
8282

8383
// Remember that an explicit language was set
8484
this._hasLanguageSetExplicitly = true;
8585

86-
this.setLanguageIdInternal(languageId);
86+
this.setLanguageIdInternal(languageId, source);
8787
}
8888

89-
private setLanguageIdInternal(languageId: string): void {
89+
private setLanguageIdInternal(languageId: string, source?: string): void {
9090
if (!this.isResolved()) {
9191
return;
9292
}
@@ -95,7 +95,20 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
9595
return;
9696
}
9797

98-
this.modelService.setMode(this.textEditorModel, this.languageService.createById(languageId));
98+
this.modelService.setMode(this.textEditorModel, this.languageService.createById(languageId), source);
99+
}
100+
101+
protected installModelListeners(model: ITextModel): void {
102+
103+
// Setup listener for lower level language changes
104+
const disposable = this._register(model.onDidChangeLanguage((e) => {
105+
if (e.source === LanguageDetectionLanguageEventSource) {
106+
return;
107+
}
108+
109+
this._hasLanguageSetExplicitly = true;
110+
disposable.dispose();
111+
}));
99112
}
100113

101114
getLanguageId(): string | undefined {
@@ -117,7 +130,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
117130

118131
const lang = await this.languageDetectionService.detectLanguage(this.textEditorModelHandle);
119132
if (lang && !this.isDisposed()) {
120-
this.setLanguageIdInternal(lang);
133+
this.setLanguageIdInternal(lang, LanguageDetectionLanguageEventSource);
121134

122135
const languageName = this.languageService.getLanguageName(lang);
123136
if (languageName) {

src/vs/workbench/common/editor/textResourceEditorInput.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ export class TextResourceEditorInput extends AbstractTextResourceEditorInput imp
130130
}
131131
}
132132

133-
setLanguageId(languageId: string): void {
133+
setLanguageId(languageId: string, source?: string): void {
134134
this.setPreferredLanguageId(languageId);
135135

136-
this.cachedModel?.setLanguageId(languageId);
136+
this.cachedModel?.setLanguageId(languageId, source);
137137
}
138138

139139
setPreferredLanguageId(languageId: string): void {

src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements
246246
return this.preferredLanguageId;
247247
}
248248

249-
setLanguageId(languageId: string): void {
249+
setLanguageId(languageId: string, source?: string): void {
250250
this.setPreferredLanguageId(languageId);
251251

252-
this.model?.setLanguageId(languageId);
252+
this.model?.setLanguageId(languageId, source);
253253
}
254254

255255
setPreferredLanguageId(languageId: string): void {

src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo
1111
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1212
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
1313
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
14-
import { ILanguageDetectionService, LanguageDetectionHintConfig } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
14+
import { ILanguageDetectionService, LanguageDetectionHintConfig, LanguageDetectionLanguageEventSource } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService';
1515
import { ThrottledDelayer } from 'vs/base/common/async';
1616
import { ILanguageService } from 'vs/editor/common/languages/language';
1717
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@@ -139,7 +139,7 @@ registerAction2(class extends Action2 {
139139
if (editorUri) {
140140
const lang = await languageDetectionService.detectLanguage(editorUri);
141141
if (lang) {
142-
editor.getModel()?.setMode(lang);
142+
editor.getModel()?.setMode(lang, LanguageDetectionLanguageEventSource);
143143
} else {
144144
notificationService.warn(localize('noDetection', "Unable to detect editor language"));
145145
}

src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInput.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@ export class MergeEditorInput extends AbstractTextResourceEditorInput implements
194194
return Boolean(this._outTextModel?.isDirty());
195195
}
196196

197-
setLanguageId(languageId: string, _setExplicitly?: boolean): void {
198-
this._model?.setLanguageId(languageId);
197+
setLanguageId(languageId: string, source?: string): void {
198+
this._model?.setLanguageId(languageId, source);
199199
}
200200

201201
// implement get/set languageId

src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,12 @@ export class MergeEditorModel extends EditorModel {
400400

401401
public readonly hasUnhandledConflicts = this.unhandledConflictsCount.map(value => /** @description hasUnhandledConflicts */ value > 0);
402402

403-
public setLanguageId(languageId: string): void {
403+
public setLanguageId(languageId: string, source?: string): void {
404404
const language = this.languageService.createById(languageId);
405-
this.modelService.setMode(this.base, language);
406-
this.modelService.setMode(this.input1.textModel, language);
407-
this.modelService.setMode(this.input2.textModel, language);
408-
this.modelService.setMode(this.resultTextModel, language);
405+
this.modelService.setMode(this.base, language, source);
406+
this.modelService.setMode(this.input1.textModel, language, source);
407+
this.modelService.setMode(this.input2.textModel, language, source);
408+
this.modelService.setMode(this.resultTextModel, language, source);
409409
}
410410

411411
public getInitialResultValue(): string {

src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class ApplyFileSnippetAction extends SnippetsAction {
6262
}]);
6363

6464
// set language if possible
65-
modelService.setMode(editor.getModel(), langService.createById(selection.langId));
65+
modelService.setMode(editor.getModel(), langService.createById(selection.langId), ApplyFileSnippetAction.Id);
6666
}
6767
}
6868

src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
88

99
export const ILanguageDetectionService = createDecorator<ILanguageDetectionService>('ILanguageDetectionService');
1010

11+
export const LanguageDetectionLanguageEventSource = 'languageDetection';
12+
1113
export interface ILanguageDetectionService {
1214
readonly _serviceBrand: undefined;
1315

src/vs/workbench/services/textfile/common/textFileEditorModel.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
197197
this.modelService.setMode(this.textEditorModel, languageSelection);
198198
}
199199

200-
override setLanguageId(languageId: string): void {
201-
super.setLanguageId(languageId);
200+
override setLanguageId(languageId: string, source?: string): void {
201+
super.setLanguageId(languageId, source);
202202

203203
this.preferredLanguageId = languageId;
204204
}
@@ -556,15 +556,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
556556
}
557557
}
558558

559-
private installModelListeners(model: ITextModel): void {
559+
protected override installModelListeners(model: ITextModel): void {
560560

561561
// See https://github.com/microsoft/vscode/issues/30189
562562
// This code has been extracted to a different method because it caused a memory leak
563563
// where `value` was captured in the content change listener closure scope.
564564

565-
// Listen to text model events
566565
this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e.isUndoing || e.isRedoing)));
567566
this._register(model.onDidChangeLanguage(() => this.onMaybeShouldChangeEncoding())); // detect possible encoding change via language specific settings
567+
568+
super.installModelListeners(model);
568569
}
569570

570571
private onModelContentChanged(model: ITextModel, isUndoingOrRedoing: boolean): void {

src/vs/workbench/services/textfile/common/textfiles.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ export interface ILanguageSupport {
468468
/**
469469
* Sets the language id of the object.
470470
*/
471-
setLanguageId(languageId: string, setExplicitly?: boolean): void;
471+
setLanguageId(languageId: string, source?: string): void;
472472
}
473473

474474
export interface ITextFileEditorModelSaveEvent extends IWorkingCopySaveEvent {

src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ export class UntitledTextEditorInput extends AbstractTextResourceEditorInput imp
108108
return this.model.setEncoding(encoding);
109109
}
110110

111-
setLanguageId(languageId: string): void {
112-
this.model.setLanguageId(languageId);
111+
setLanguageId(languageId: string, source?: string): void {
112+
this.model.setLanguageId(languageId, source);
113113
}
114114

115115
getLanguageId(): string | undefined {

src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,14 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
190190

191191
//#region Language
192192

193-
override setLanguageId(languageId: string): void {
193+
override setLanguageId(languageId: string, source?: string): void {
194194
const actualLanguage: string | undefined = languageId === UntitledTextEditorModel.ACTIVE_EDITOR_LANGUAGE_ID
195195
? this.editorService.activeTextEditorLanguageId
196196
: languageId;
197197
this.preferredLanguageId = actualLanguage;
198198

199199
if (actualLanguage) {
200-
super.setLanguageId(actualLanguage);
200+
super.setLanguageId(actualLanguage, source);
201201
}
202202
}
203203

@@ -333,8 +333,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
333333

334334
// Listen to text model events
335335
const textEditorModel = assertIsDefined(this.textEditorModel);
336-
this._register(textEditorModel.onDidChangeContent(e => this.onModelContentChanged(textEditorModel, e)));
337-
this._register(textEditorModel.onDidChangeLanguage(() => this.onConfigurationChange(true))); // language change can have impact on config
336+
this.installModelListeners(textEditorModel);
338337

339338
// Only adjust name and dirty state etc. if we
340339
// actually created the untitled model
@@ -358,6 +357,13 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt
358357
return super.resolve();
359358
}
360359

360+
protected override installModelListeners(model: ITextModel): void {
361+
this._register(model.onDidChangeContent(e => this.onModelContentChanged(model, e)));
362+
this._register(model.onDidChangeLanguage(() => this.onConfigurationChange(true))); // language change can have impact on config
363+
364+
super.installModelListeners(model);
365+
}
366+
361367
private onModelContentChanged(textEditorModel: ITextModel, e: IModelContentChangedEvent): void {
362368

363369
// mark the untitled text editor as non-dirty once its content becomes empty and we do

0 commit comments

Comments
 (0)