Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/common/editor/textEditorModel.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 { ITextModel, ITextBufferFactory, ITextSnapshot, ModelConstants } from '../../../editor/common/model.js';
7
import { EditorModel } from './editorModel.js';
8
import { ILanguageSupport } from '../../services/textfile/common/textfiles.js';
9
import { URI } from '../../../base/common/uri.js';
10
import { ITextEditorModel, IResolvedTextEditorModel } from '../../../editor/common/services/resolverService.js';
11
import { ILanguageService, ILanguageSelection } from '../../../editor/common/languages/language.js';
12
import { IModelService } from '../../../editor/common/services/model.js';
13
import { MutableDisposable } from '../../../base/common/lifecycle.js';
14
import { PLAINTEXT_LANGUAGE_ID } from '../../../editor/common/languages/modesRegistry.js';
15
import { ILanguageDetectionService, LanguageDetectionLanguageEventSource } from '../../services/languageDetection/common/languageDetectionWorkerService.js';
16
import { ThrottledDelayer } from '../../../base/common/async.js';
17
import { IAccessibilityService } from '../../../platform/accessibility/common/accessibility.js';
18
import { localize } from '../../../nls.js';
19
import { IMarkdownString } from '../../../base/common/htmlContent.js';
20
import { TextModelEditSource } from '../../../editor/common/textModelEditSource.js';
21
22
/**
23
* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
24
*/
25
export class BaseTextEditorModel extends EditorModel implements ITextEditorModel, ILanguageSupport {
26
27
private static readonly AUTO_DETECT_LANGUAGE_THROTTLE_DELAY = 600;
28
29
protected textEditorModelHandle: URI | undefined = undefined;
30
31
private createdEditorModel: boolean | undefined;
32
33
private readonly modelDisposeListener = this._register(new MutableDisposable());
34
private readonly autoDetectLanguageThrottler = this._register(new ThrottledDelayer<void>(BaseTextEditorModel.AUTO_DETECT_LANGUAGE_THROTTLE_DELAY));
35
36
constructor(
37
@IModelService protected modelService: IModelService,
38
@ILanguageService protected languageService: ILanguageService,
39
@ILanguageDetectionService private readonly languageDetectionService: ILanguageDetectionService,
40
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
41
textEditorModelHandle?: URI
42
) {
43
super();
44
45
if (textEditorModelHandle) {
46
this.handleExistingModel(textEditorModelHandle);
47
}
48
}
49
50
private handleExistingModel(textEditorModelHandle: URI): void {
51
52
// We need the resource to point to an existing model
53
const model = this.modelService.getModel(textEditorModelHandle);
54
if (!model) {
55
throw new Error(`Document with resource ${textEditorModelHandle.toString(true)} does not exist`);
56
}
57
58
this.textEditorModelHandle = textEditorModelHandle;
59
60
// Make sure we clean up when this model gets disposed
61
this.registerModelDisposeListener(model);
62
}
63
64
private registerModelDisposeListener(model: ITextModel): void {
65
this.modelDisposeListener.value = model.onWillDispose(() => {
66
this.textEditorModelHandle = undefined; // make sure we do not dispose code editor model again
67
this.dispose();
68
});
69
}
70
71
get textEditorModel(): ITextModel | null {
72
return this.textEditorModelHandle ? this.modelService.getModel(this.textEditorModelHandle) : null;
73
}
74
75
isReadonly(): boolean | IMarkdownString {
76
return true;
77
}
78
79
private _blockLanguageChangeListener = false;
80
private _languageChangeSource: 'user' | 'api' | undefined = undefined;
81
get languageChangeSource() { return this._languageChangeSource; }
82
get hasLanguageSetExplicitly() {
83
// This is technically not 100% correct, because 'api' can also be
84
// set as source if a model is resolved as text first and then
85
// transitions into the resolved language. But to preserve the current
86
// behaviour, we do not change this property. Rather, `languageChangeSource`
87
// can be used to get more fine grained information.
88
return typeof this._languageChangeSource === 'string';
89
}
90
91
setLanguageId(languageId: string, source?: string): void {
92
93
// Remember that an explicit language was set
94
this._languageChangeSource = 'user';
95
96
this.setLanguageIdInternal(languageId, source);
97
}
98
99
private setLanguageIdInternal(languageId: string, source?: string): void {
100
if (!this.isResolved()) {
101
return;
102
}
103
104
if (!languageId || languageId === this.textEditorModel.getLanguageId()) {
105
return;
106
}
107
108
this._blockLanguageChangeListener = true;
109
try {
110
this.textEditorModel.setLanguage(this.languageService.createById(languageId), source);
111
} finally {
112
this._blockLanguageChangeListener = false;
113
}
114
}
115
116
protected installModelListeners(model: ITextModel): void {
117
118
// Setup listener for lower level language changes
119
const disposable = this._register(model.onDidChangeLanguage(e => {
120
if (
121
e.source === LanguageDetectionLanguageEventSource ||
122
this._blockLanguageChangeListener
123
) {
124
return;
125
}
126
127
this._languageChangeSource = 'api';
128
disposable.dispose();
129
}));
130
}
131
132
getLanguageId(): string | undefined {
133
return this.textEditorModel?.getLanguageId();
134
}
135
136
protected autoDetectLanguage(): Promise<void> {
137
return this.autoDetectLanguageThrottler.trigger(() => this.doAutoDetectLanguage());
138
}
139
140
private async doAutoDetectLanguage(): Promise<void> {
141
if (
142
this.hasLanguageSetExplicitly || // skip detection when the user has made an explicit choice on the language
143
!this.textEditorModelHandle || // require a URI to run the detection for
144
!this.languageDetectionService.isEnabledForLanguage(this.getLanguageId() ?? PLAINTEXT_LANGUAGE_ID) // require a valid language that is enlisted for detection
145
) {
146
return;
147
}
148
149
const lang = await this.languageDetectionService.detectLanguage(this.textEditorModelHandle);
150
const prevLang = this.getLanguageId();
151
if (lang && lang !== prevLang && !this.isDisposed()) {
152
this.setLanguageIdInternal(lang, LanguageDetectionLanguageEventSource);
153
const languageName = this.languageService.getLanguageName(lang);
154
this.accessibilityService.alert(localize('languageAutoDetected', "Language {0} was automatically detected and set as the language mode.", languageName ?? lang));
155
}
156
}
157
158
/**
159
* Creates the text editor model with the provided value, optional preferred language
160
* (can be comma separated for multiple values) and optional resource URL.
161
*/
162
protected createTextEditorModel(value: ITextBufferFactory, resource: URI | undefined, preferredLanguageId?: string): ITextModel {
163
const firstLineText = this.getFirstLineText(value);
164
const languageSelection = this.getOrCreateLanguage(resource, this.languageService, preferredLanguageId, firstLineText);
165
166
return this.doCreateTextEditorModel(value, languageSelection, resource);
167
}
168
169
private doCreateTextEditorModel(value: ITextBufferFactory, languageSelection: ILanguageSelection, resource: URI | undefined): ITextModel {
170
let model = resource && this.modelService.getModel(resource);
171
if (!model) {
172
model = this.modelService.createModel(value, languageSelection, resource);
173
this.createdEditorModel = true;
174
175
// Make sure we clean up when this model gets disposed
176
this.registerModelDisposeListener(model);
177
} else {
178
this.updateTextEditorModel(value, languageSelection.languageId);
179
}
180
181
this.textEditorModelHandle = model.uri;
182
183
return model;
184
}
185
186
protected getFirstLineText(value: ITextBufferFactory | ITextModel): string {
187
188
// text buffer factory
189
const textBufferFactory = value as ITextBufferFactory;
190
if (typeof textBufferFactory.getFirstLineText === 'function') {
191
return textBufferFactory.getFirstLineText(ModelConstants.FIRST_LINE_DETECTION_LENGTH_LIMIT);
192
}
193
194
// text model
195
const textSnapshot = value as ITextModel;
196
return textSnapshot.getLineContent(1).substr(0, ModelConstants.FIRST_LINE_DETECTION_LENGTH_LIMIT);
197
}
198
199
/**
200
* Gets the language for the given identifier. Subclasses can override to provide their own implementation of this lookup.
201
*
202
* @param firstLineText optional first line of the text buffer to set the language on. This can be used to guess a language from content.
203
*/
204
protected getOrCreateLanguage(resource: URI | undefined, languageService: ILanguageService, preferredLanguage: string | undefined, firstLineText?: string): ILanguageSelection {
205
206
// lookup language via resource path if the provided language is unspecific
207
if (!preferredLanguage || preferredLanguage === PLAINTEXT_LANGUAGE_ID) {
208
return languageService.createByFilepathOrFirstLine(resource ?? null, firstLineText);
209
}
210
211
// otherwise take the preferred language for granted
212
return languageService.createById(preferredLanguage);
213
}
214
215
/**
216
* Updates the text editor model with the provided value. If the value is the same as the model has, this is a no-op.
217
*/
218
updateTextEditorModel(newValue?: ITextBufferFactory, preferredLanguageId?: string, reason?: TextModelEditSource): void {
219
if (!this.isResolved()) {
220
return;
221
}
222
223
// contents
224
if (newValue) {
225
this.modelService.updateModel(this.textEditorModel, newValue, reason);
226
}
227
228
// language (only if specific and changed)
229
if (preferredLanguageId && preferredLanguageId !== PLAINTEXT_LANGUAGE_ID && this.textEditorModel.getLanguageId() !== preferredLanguageId) {
230
this.textEditorModel.setLanguage(this.languageService.createById(preferredLanguageId));
231
}
232
}
233
234
createSnapshot(this: IResolvedTextEditorModel): ITextSnapshot;
235
createSnapshot(this: ITextEditorModel): ITextSnapshot | null;
236
createSnapshot(): ITextSnapshot | null {
237
if (!this.textEditorModel) {
238
return null;
239
}
240
241
return this.textEditorModel.createSnapshot(true /* preserve BOM */);
242
}
243
244
override isResolved(): this is IResolvedTextEditorModel {
245
return !!this.textEditorModelHandle;
246
}
247
248
override dispose(): void {
249
this.modelDisposeListener.dispose(); // dispose this first because it will trigger another dispose() otherwise
250
251
if (this.textEditorModelHandle && this.createdEditorModel) {
252
this.modelService.destroyModel(this.textEditorModelHandle);
253
}
254
255
this.textEditorModelHandle = undefined;
256
this.createdEditorModel = false;
257
258
super.dispose();
259
}
260
}
261
262