Add support for ILanguageSelection directly to the text model (#175344)

Add support for `ILanguageSelection` directly to the text model (fixes #155609)
This commit is contained in:
Alexandru Dima 2023-02-24 17:11:18 +01:00 committed by GitHub
parent 2f5ef1e59e
commit e7dbed0625
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 64 additions and 82 deletions

View file

@ -22,6 +22,7 @@ import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides';
import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart';
import { ThemeColor } from 'vs/base/common/themables';
import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
import { ILanguageSelection } from 'vs/editor/common/languages/language';
/**
* Vertical Lane in the overview ruler of the editor.
@ -871,7 +872,15 @@ export interface ITextModel {
* @param source The source of the call that set the language.
* @internal
*/
setMode(languageId: string, source?: string): void;
setLanguage(languageId: string, source?: string): void;
/**
* Set the current language mode associated with the model.
* @param languageSelection The new language selection.
* @param source The source of the call that set the language.
* @internal
*/
setLanguage(languageSelection: ILanguageSelection, source?: string): void;
/**
* Returns the real (inner-most) language mode at a given position.

View file

@ -9,7 +9,7 @@ import { Color } from 'vs/base/common/color';
import { illegalArgument, onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { combinedDisposable, Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { combinedDisposable, Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { listenStream } from 'vs/base/common/stream';
import * as strings from 'vs/base/common/strings';
import { Constants } from 'vs/base/common/uint';
@ -24,7 +24,7 @@ import { TextChange } from 'vs/editor/common/core/textChange';
import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults';
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
import { FormattingOptions } from 'vs/editor/common/languages';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import * as model from 'vs/editor/common/model';
import { BracketPairsTextModelPart } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl';
@ -243,6 +243,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
private _buffer: model.ITextBuffer;
private _bufferDisposable: IDisposable;
private _options: model.TextModelResolvedOptions;
private _languageSelectionListener = this._register(new MutableDisposable<IDisposable>());
private _isDisposed: boolean;
private __isDisposing: boolean;
@ -287,7 +288,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
constructor(
source: string | model.ITextBufferFactory,
languageId: string,
languageIdOrSelection: string | ILanguageSelection,
creationOptions: model.ITextModelCreationOptions,
associatedResource: URI | null = null,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
@ -313,6 +314,11 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
this._options = TextModel.resolveOptions(this._buffer, creationOptions);
const languageId = (typeof languageIdOrSelection === 'string' ? languageIdOrSelection : languageIdOrSelection.languageId);
if (typeof languageIdOrSelection !== 'string') {
this._languageSelectionListener.value = languageIdOrSelection.onDidChange(() => this._setLanguage(languageIdOrSelection.languageId));
}
this._bracketPairs = this._register(new BracketPairsTextModelPart(this, this._languageConfigurationService));
this._guidesTextModelPart = this._register(new GuidesTextModelPart(this, this._languageConfigurationService));
this._decorationProvider = this._register(new ColorizedBracketPairsDecorationProvider(this));
@ -1907,7 +1913,17 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
return this.tokenization.getLanguageId();
}
public setMode(languageId: string, source?: string): void {
public setLanguage(languageIdOrSelection: string | ILanguageSelection, source?: string): void {
if (typeof languageIdOrSelection === 'string') {
this._languageSelectionListener.clear();
this._setLanguage(languageIdOrSelection, source);
} else {
this._languageSelectionListener.value = languageIdOrSelection.onDidChange(() => this._setLanguage(languageIdOrSelection.languageId, source));
this._setLanguage(languageIdOrSelection.languageId, source);
}
}
private _setLanguage(languageId: string, source?: string): void {
this.tokenization.setLanguageId(languageId, source);
this._languageService.requestRichLanguageFeatures(languageId);
}

View file

@ -21,8 +21,6 @@ export interface IModelService {
updateModel(model: ITextModel, value: string | ITextBufferFactory): void;
setMode(model: ITextModel, languageSelection: ILanguageSelection, source?: string): void;
destroyModel(resource: URI): void;
getModels(): ITextModel[];

View file

@ -40,46 +40,22 @@ function computeModelSha1(model: ITextModel): string {
return shaComputer.digest();
}
class ModelData implements IDisposable {
public readonly model: TextModel;
private _languageSelection: ILanguageSelection | null;
private _languageSelectionListener: IDisposable | null;
private readonly _modelEventListeners = new DisposableStore();
constructor(
model: TextModel,
public readonly model: TextModel,
onWillDispose: (model: ITextModel) => void,
onDidChangeLanguage: (model: ITextModel, e: IModelLanguageChangedEvent) => void
) {
this.model = model;
this._languageSelection = null;
this._languageSelectionListener = null;
this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model)));
this._modelEventListeners.add(model.onDidChangeLanguage((e) => onDidChangeLanguage(model, e)));
}
private _disposeLanguageSelection(): void {
if (this._languageSelectionListener) {
this._languageSelectionListener.dispose();
this._languageSelectionListener = null;
}
}
public dispose(): void {
this._modelEventListeners.dispose();
this._disposeLanguageSelection();
}
public setLanguage(languageSelection: ILanguageSelection, source?: string): void {
this._disposeLanguageSelection();
this._languageSelection = languageSelection;
this._languageSelectionListener = this._languageSelection.onDidChange(() => this.model.setMode(languageSelection.languageId, source));
this.model.setMode(languageSelection.languageId, source);
}
}
@ -242,7 +218,8 @@ export class ModelService extends Disposable implements IModelService {
return true;
}
public getCreationOptions(language: string, resource: URI | undefined, isForSimpleWidget: boolean): ITextModelCreationOptions {
public getCreationOptions(languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ITextModelCreationOptions {
const language = (typeof languageIdOrSelection === 'string' ? languageIdOrSelection : languageIdOrSelection.languageId);
let creationOptions = this._modelCreationOptionsByLanguageAndResource[language + resource];
if (!creationOptions) {
const editor = this._configurationService.getValue<IRawEditorConfig>('editor', { overrideIdentifier: language, resource });
@ -345,12 +322,12 @@ export class ModelService extends Disposable implements IModelService {
}
}
private _createModelData(value: string | ITextBufferFactory, languageId: string, resource: URI | undefined, isForSimpleWidget: boolean): ModelData {
private _createModelData(value: string | ITextBufferFactory, languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ModelData {
// create & save the model
const options = this.getCreationOptions(languageId, resource, isForSimpleWidget);
const options = this.getCreationOptions(languageIdOrSelection, resource, isForSimpleWidget);
const model: TextModel = new TextModel(
value,
languageId,
languageIdOrSelection,
options,
resource,
this._undoRedoService,
@ -478,8 +455,7 @@ export class ModelService extends Disposable implements IModelService {
let modelData: ModelData;
if (languageSelection) {
modelData = this._createModelData(value, languageSelection.languageId, resource, isForSimpleWidget);
this.setMode(modelData.model, languageSelection);
modelData = this._createModelData(value, languageSelection, resource, isForSimpleWidget);
} else {
modelData = this._createModelData(value, PLAINTEXT_LANGUAGE_ID, resource, isForSimpleWidget);
}
@ -489,17 +465,6 @@ export class ModelService extends Disposable implements IModelService {
return modelData.model;
}
public setMode(model: ITextModel, languageSelection: ILanguageSelection, source?: string): void {
if (!languageSelection) {
return;
}
const modelData = this._models[MODEL_ID(model.uri)];
if (!modelData) {
return;
}
modelData.setLanguage(languageSelection, source);
}
public destroyModel(resource: URI): void {
// We need to support that not all models get disposed through this service (i.e. model.dispose() should work!)
const modelData = this._models[MODEL_ID(resource)];

View file

@ -240,9 +240,8 @@ export function createModel(value: string, language?: string, uri?: URI): ITextM
*/
export function setModelLanguage(model: ITextModel, mimeTypeOrLanguageId: string): void {
const languageService = StandaloneServices.get(ILanguageService);
const modelService = StandaloneServices.get(IModelService);
const languageId = languageService.getLanguageIdByMimeType(mimeTypeOrLanguageId) || mimeTypeOrLanguageId || PLAINTEXT_LANGUAGE_ID;
modelService.setMode(model, languageService.createById(languageId));
model.setLanguage(languageService.createById(languageId));
}
/**

View file

@ -41,7 +41,7 @@ suite('CodeEditorWidget', () => {
invoked = true;
}));
viewModel.model.setMode('testMode');
viewModel.model.setLanguage('testMode');
assert.deepStrictEqual(invoked, true);
@ -55,7 +55,7 @@ suite('CodeEditorWidget', () => {
const languageService = instantiationService.get(ILanguageService);
const disposables = new DisposableStore();
disposables.add(languageService.registerLanguage({ id: 'testMode' }));
viewModel.model.setMode('testMode');
viewModel.model.setLanguage('testMode');
let invoked = false;
disposables.add(editor.onDidChangeModelLanguageConfiguration((e) => {

View file

@ -586,12 +586,12 @@ suite('TextModelWithTokens regression tests', () => {
assertViewLineTokens(model, 1, true, [createViewLineToken(12, 1)]);
assertViewLineTokens(model, 2, true, [createViewLineToken(9, 1)]);
model.setMode(LANG_ID1);
model.setLanguage(LANG_ID1);
assertViewLineTokens(model, 1, true, [createViewLineToken(12, 11)]);
assertViewLineTokens(model, 2, true, [createViewLineToken(9, 12)]);
model.setMode(LANG_ID2);
model.setLanguage(LANG_ID2);
assertViewLineTokens(model, 1, false, [createViewLineToken(12, 1)]);
assertViewLineTokens(model, 2, false, [createViewLineToken(9, 1)]);

View file

@ -52,7 +52,7 @@ export class MainThreadLanguages implements MainThreadLanguagesShape {
const uri = URI.revive(resource);
const ref = await this._resolverService.createModelReference(uri);
try {
this._modelService.setMode(ref.object.textEditorModel, this._languageService.createById(languageId));
ref.object.textEditorModel.setLanguage(this._languageService.createById(languageId));
} finally {
ref.dispose();
}

View file

@ -207,7 +207,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor {
// High confidence, set language id at TextEditorModel level to block future auto-detection
this.input.model.setLanguageId(candidateLanguage.id);
} else {
this.modelService.setMode(textModel, this.languageService.createById(candidateLanguage.id));
textModel.setLanguage(this.languageService.createById(candidateLanguage.id));
}
const opts = this.modelService.getCreationOptions(textModel.getLanguageId(), textModel.uri, textModel.isForSimpleWidget);

View file

@ -95,7 +95,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
return;
}
this.modelService.setMode(this.textEditorModel, this.languageService.createById(languageId), source);
this.textEditorModel.setLanguage(this.languageService.createById(languageId), source);
}
protected installModelListeners(model: ITextModel): void {
@ -211,7 +211,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
// language (only if specific and changed)
if (preferredLanguageId && preferredLanguageId !== PLAINTEXT_LANGUAGE_ID && this.textEditorModel.getLanguageId() !== preferredLanguageId) {
this.modelService.setMode(this.textEditorModel, this.languageService.createById(preferredLanguageId));
this.textEditorModel.setLanguage(this.languageService.createById(preferredLanguageId));
}
}

View file

@ -169,7 +169,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi
if (this.editor.hasModel()) {
// Use plaintext language for log messages, otherwise respect underlying editor language #125619
const languageId = this.context === Context.LOG_MESSAGE ? PLAINTEXT_LANGUAGE_ID : this.editor.getModel().getLanguageId();
this.input.getModel().setMode(languageId);
this.input.getModel().setLanguage(languageId);
}
}
@ -229,7 +229,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi
CONTEXT_IN_BREAKPOINT_WIDGET.bindTo(scopedContextKeyService).set(true);
const model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:${this.editor.getId()}:breakpointinput`), true);
if (this.editor.hasModel()) {
model.setMode(this.editor.getModel().getLanguageId());
model.setLanguage(this.editor.getModel().getLanguageId());
}
this.input.setModel(model);
this.setInputMode();

View file

@ -342,7 +342,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget {
this.modelChangeListener.dispose();
this.modelChangeListener = activeEditorControl.onDidChangeModelLanguage(() => this.setMode());
if (this.model && activeEditorControl.hasModel()) {
this.model.setMode(activeEditorControl.getModel().getLanguageId());
this.model.setLanguage(activeEditorControl.getModel().getLanguageId());
}
}
}

View file

@ -39,7 +39,7 @@ suite('Emmet', () => {
assert.fail('Editor model not found');
}
model.setMode(mode);
model.setLanguage(mode);
const langOutput = EmmetEditorAction.getLanguage(editor, new MockGrammarContributions(scopeName));
if (!langOutput) {
assert.fail('langOutput not found');

View file

@ -554,7 +554,7 @@ export class InteractiveEditor extends EditorPane {
if (selectedOrSuggested) {
const language = selectedOrSuggested.supportedLanguages[0];
const newMode = language ? this.#languageService.createById(language).languageId : PLAINTEXT_LANGUAGE_ID;
textModel.setMode(newMode);
textModel.setLanguage(newMode);
NOTEBOOK_KERNEL.bindTo(this.#contextKeyService).set(selectedOrSuggested.id);
}

View file

@ -139,7 +139,7 @@ registerAction2(class extends Action2 {
if (editorUri) {
const lang = await languageDetectionService.detectLanguage(editorUri);
if (lang) {
editor.getModel()?.setMode(lang, LanguageDetectionLanguageEventSource);
editor.getModel()?.setLanguage(lang, LanguageDetectionLanguageEventSource);
} else {
notificationService.warn(localize('noDetection', "Unable to detect editor language"));
}

View file

@ -10,7 +10,6 @@ import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { ITextModel } from 'vs/editor/common/model';
import { IModelService } from 'vs/editor/common/services/model';
import { localize } from 'vs/nls';
import { IResourceUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
@ -59,7 +58,6 @@ export class MergeEditorModel extends EditorModel {
private readonly diffComputer: IMergeDiffComputer,
private readonly options: { resetResult: boolean },
public readonly telemetry: MergeEditorTelemetry,
@IModelService private readonly modelService: IModelService,
@ILanguageService private readonly languageService: ILanguageService,
@IUndoRedoService private readonly undoRedoService: IUndoRedoService,
) {
@ -558,10 +556,10 @@ export class MergeEditorModel extends EditorModel {
public setLanguageId(languageId: string, source?: string): void {
const language = this.languageService.createById(languageId);
this.modelService.setMode(this.base, language, source);
this.modelService.setMode(this.input1.textModel, language, source);
this.modelService.setMode(this.input2.textModel, language, source);
this.modelService.setMode(this.resultTextModel, language, source);
this.base.setLanguage(language, source);
this.input1.textModel.setLanguage(language, source);
this.input2.textModel.setLanguage(language, source);
this.resultTextModel.setLanguage(language, source);
}
public getInitialResultValue(): string {

View file

@ -506,7 +506,7 @@ class CellInfoContentProvider {
}
model.setValue(newResult.content);
model.setMode(newResult.mode.languageId);
model.setLanguage(newResult.mode.languageId);
});
const once = model.onWillDispose(() => {

View file

@ -93,7 +93,7 @@ export class NotebookCellTextModel extends Disposable implements ICell {
if (this._textModel) {
const languageId = this._languageService.createById(newLanguageId);
this._textModel.setMode(languageId.languageId);
this._textModel.setLanguage(languageId.languageId);
}
if (this._language === newLanguage) {

View file

@ -95,7 +95,7 @@ class PerfModelContentProvider implements ITextModelContentProvider {
this._model = this._modelService.getModel(resource) || this._modelService.createModel('Loading...', langId, resource);
this._modelDisposables.push(langId.onDidChange(e => {
this._model?.setMode(e);
this._model?.setLanguage(e);
}));
this._modelDisposables.push(this._extensionService.onDidChangeExtensionsStatus(this._updateModel, this));

View file

@ -7,7 +7,6 @@ import { groupBy, isFalsyOrEmpty } from 'vs/base/common/arrays';
import { compare } from 'vs/base/common/strings';
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
import { ILanguageService } from 'vs/editor/common/languages/language';
import { IModelService } from 'vs/editor/common/services/model';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { localize } from 'vs/nls';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@ -37,7 +36,6 @@ export class ApplyFileSnippetAction extends SnippetsAction {
const quickInputService = accessor.get(IQuickInputService);
const editorService = accessor.get(IEditorService);
const langService = accessor.get(ILanguageService);
const modelService = accessor.get(IModelService);
const editor = getCodeEditor(editorService.activeTextEditorControl);
if (!editor || !editor.hasModel()) {
@ -62,7 +60,7 @@ export class ApplyFileSnippetAction extends SnippetsAction {
}]);
// set language if possible
modelService.setMode(editor.getModel(), langService.createById(selection.langId), ApplyFileSnippetAction.Id);
editor.getModel().setLanguage(langService.createById(selection.langId), ApplyFileSnippetAction.Id);
editor.focus();
}

View file

@ -531,7 +531,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
const sourceLanguageId = sourceTextModel.getLanguageId();
const targetLanguageId = targetTextModel.getLanguageId();
if (sourceLanguageId !== PLAINTEXT_LANGUAGE_ID && targetLanguageId === PLAINTEXT_LANGUAGE_ID) {
targetTextModel.setMode(sourceLanguageId); // only use if more specific than plain/text
targetTextModel.setLanguage(sourceLanguageId); // only use if more specific than plain/text
}
// transient properties

View file

@ -196,7 +196,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
const firstLineText = this.getFirstLineText(this.textEditorModel);
const languageSelection = this.getOrCreateLanguage(this.resource, this.languageService, this.preferredLanguageId, firstLineText);
this.modelService.setMode(this.textEditorModel, languageSelection);
this.textEditorModel.setLanguage(languageSelection);
}
override setLanguageId(languageId: string, source?: string): void {

View file

@ -351,7 +351,7 @@ suite('Untitled text editors', () => {
await input.resolve();
assert.ok(!input.model.hasLanguageSetExplicitly);
accessor.modelService.setMode(model.textEditorModel!, accessor.languageService.createById(language));
model.textEditorModel!.setLanguage(accessor.languageService.createById(language));
assert.ok(input.model.hasLanguageSetExplicitly);
assert.strictEqual(model.getLanguageId(), language);
@ -373,8 +373,7 @@ suite('Untitled text editors', () => {
await input.resolve();
assert.ok(!input.model.hasLanguageSetExplicitly);
accessor.modelService.setMode(
model.textEditorModel!,
model.textEditorModel!.setLanguage(
accessor.languageService.createById(language),
// This is really what this is testing
LanguageDetectionLanguageEventSource);