Skip to content

Commit

Permalink
Use ILanguageService to broker basic or rich language features requ…
Browse files Browse the repository at this point in the history
  • Loading branch information
alexdima committed Feb 24, 2023
1 parent 07d120e commit dfd5353
Show file tree
Hide file tree
Showing 11 changed files with 111 additions and 39 deletions.
29 changes: 27 additions & 2 deletions src/vs/editor/common/languages/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,23 @@ export interface ILanguageService {
readonly languageIdCodec: ILanguageIdCodec;

/**
* An event emitted when a language is needed for the first time.
* An event emitted when basic language features are requested for the first time.
* This event is emitted when embedded languages are encountered (e.g. JS code block inside Markdown)
* or when a language is associated to a text model.
*
* **Note**: Basic language features refers to language configuration related features.
* **Note**: This event is a superset of `onDidRequestRichLanguageFeatures`
*/
onDidEncounterLanguage: Event<string>;
onDidRequestBasicLanguageFeatures: Event<string>;

/**
* An event emitted when rich language features are requested for the first time.
* This event is emitted when a language is associated to a text model.
*
* **Note**: Rich language features refers to tokenizers, language features based on providers, etc.
* **Note**: This event is a subset of `onDidRequestRichLanguageFeatures`
*/
onDidRequestRichLanguageFeatures: Event<string>;

/**
* An event emitted when languages have changed.
Expand Down Expand Up @@ -140,4 +154,15 @@ export interface ILanguageService {
* Will fall back to 'plaintext' if the `languageId` cannot be determined.
*/
createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection;

/**
* Request basic language features for a language.
*/
requestBasicLanguageFeatures(languageId: string): void;

/**
* Request rich language features for a language.
*/
requestRichLanguageFeatures(languageId: string): void;

}
3 changes: 3 additions & 0 deletions src/vs/editor/common/model/textModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
this._onDidChangeDecorations.fire();
this._onDidChangeDecorations.endDeferredEmit();
}));

this._languageService.requestRichLanguageFeatures(languageId);
}

public override dispose(): void {
Expand Down Expand Up @@ -1907,6 +1909,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati

public setMode(languageId: string, source?: string): void {
this.tokenization.setLanguageId(languageId, source);
this._languageService.requestRichLanguageFeatures(languageId);
}

public getLanguageIdAtPosition(lineNumber: number, column: number): string {
Expand Down
45 changes: 30 additions & 15 deletions src/vs/editor/common/services/languageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,24 @@ export class LanguageService extends Disposable implements ILanguageService {

static instanceCount = 0;

private readonly _encounteredLanguages: Set<string>;
protected readonly _registry: LanguagesRegistry;
public readonly languageIdCodec: ILanguageIdCodec;
private readonly _onDidRequestBasicLanguageFeatures = this._register(new Emitter<string>());
public readonly onDidRequestBasicLanguageFeatures = this._onDidRequestBasicLanguageFeatures.event;

private readonly _onDidEncounterLanguage = this._register(new Emitter<string>());
public readonly onDidEncounterLanguage: Event<string> = this._onDidEncounterLanguage.event;
private readonly _onDidRequestRichLanguageFeatures = this._register(new Emitter<string>());
public readonly onDidRequestRichLanguageFeatures = this._onDidRequestRichLanguageFeatures.event;

protected readonly _onDidChange = this._register(new Emitter<void>({ leakWarningThreshold: 200 /* https://github.com/microsoft/vscode/issues/119968 */ }));
public readonly onDidChange: Event<void> = this._onDidChange.event;

private readonly _requestedBasicLanguages = new Set<string>();
private readonly _requestedRichLanguages = new Set<string>();

protected readonly _registry: LanguagesRegistry;
public readonly languageIdCodec: ILanguageIdCodec;

constructor(warnOnOverwrite = false) {
super();
LanguageService.instanceCount++;
this._encounteredLanguages = new Set<string>();
this._registry = this._register(new LanguagesRegistry(true, warnOnOverwrite));
this.languageIdCodec = this._registry.languageIdCodec;
this._register(this._registry.onDidChange(() => this._onDidChange.fire()));
Expand Down Expand Up @@ -94,20 +98,20 @@ export class LanguageService extends Disposable implements ILanguageService {
return firstOrDefault(languageIds, null);
}

public createById(languageId: string | null | undefined): ILanguageSelection {
public createById(languageId: string | null | undefined): ILanguageSelection {//
return new LanguageSelection(this.onDidChange, () => {
return this._createAndGetLanguageIdentifier(languageId);
});
}

public createByMimeType(mimeType: string | null | undefined): ILanguageSelection {
public createByMimeType(mimeType: string | null | undefined): ILanguageSelection {//
return new LanguageSelection(this.onDidChange, () => {
const languageId = this.getLanguageIdByMimeType(mimeType);
return this._createAndGetLanguageIdentifier(languageId);
});
}

public createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection {
public createByFilepathOrFirstLine(resource: URI | null, firstLine?: string): ILanguageSelection {//
return new LanguageSelection(this.onDidChange, () => {
const languageId = this.guessLanguageIdByFilepathOrFirstLine(resource, firstLine);
return this._createAndGetLanguageIdentifier(languageId);
Expand All @@ -120,17 +124,28 @@ export class LanguageService extends Disposable implements ILanguageService {
languageId = PLAINTEXT_LANGUAGE_ID;
}

if (!this._encounteredLanguages.has(languageId)) {
this._encounteredLanguages.add(languageId);
return languageId;
}

public requestBasicLanguageFeatures(languageId: string): void {
if (!this._requestedBasicLanguages.has(languageId)) {
this._requestedBasicLanguages.add(languageId);
this._onDidRequestBasicLanguageFeatures.fire(languageId);
}
}

public requestRichLanguageFeatures(languageId: string): void {
if (!this._requestedRichLanguages.has(languageId)) {
this._requestedRichLanguages.add(languageId);

// Ensure basic features are requested
this.requestBasicLanguageFeatures(languageId);

// Ensure tokenizers are created
TokenizationRegistry.getOrCreate(languageId);

// Fire event
this._onDidEncounterLanguage.fire(languageId);
this._onDidRequestRichLanguageFeatures.fire(languageId);
}

return languageId;
}
}

Expand Down
23 changes: 21 additions & 2 deletions src/vs/editor/standalone/browser/standaloneLanguages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,30 @@ export function getEncodedLanguageId(languageId: string): number {
}

/**
* An event emitted when a language is needed for the first time (e.g. a model has it set).
* An event emitted when a language is associated for the first time with a text model.
* @event
*/
export function onLanguage(languageId: string, callback: () => void): IDisposable {
const languageService = StandaloneServices.get(ILanguageService);
const disposable = languageService.onDidEncounterLanguage((encounteredLanguageId) => {
const disposable = languageService.onDidRequestRichLanguageFeatures((encounteredLanguageId) => {
if (encounteredLanguageId === languageId) {
// stop listening
disposable.dispose();
// invoke actual listener
callback();
}
});
return disposable;
}

/**
* An event emitted when a language is associated for the first time with a text model or
* whena language is encountered during the tokenization of another language.
* @event
*/
export function onLanguageEncountered(languageId: string, callback: () => void): IDisposable {
const languageService = StandaloneServices.get(ILanguageService);
const disposable = languageService.onDidRequestBasicLanguageFeatures((encounteredLanguageId) => {
if (encounteredLanguageId === languageId) {
// stop listening
disposable.dispose();
Expand Down Expand Up @@ -715,6 +733,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages {
register: <any>register,
getLanguages: <any>getLanguages,
onLanguage: <any>onLanguage,
onLanguageEncountered: <any>onLanguageEncountered,
getEncodedLanguageId: <any>getEncodedLanguageId,

// provider methods
Expand Down
9 changes: 8 additions & 1 deletion src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5872,11 +5872,18 @@ declare namespace monaco.languages {
export function getEncodedLanguageId(languageId: string): number;

/**
* An event emitted when a language is needed for the first time (e.g. a model has it set).
* An event emitted when a language is associated for the first time with a text model.
* @event
*/
export function onLanguage(languageId: string, callback: () => void): IDisposable;

/**
* An event emitted when a language is associated for the first time with a text model or
* whena language is encountered during the tokenization of another language.
* @event
*/
export function onLanguageEncountered(languageId: string, callback: () => void): IDisposable;

/**
* Set the editing configuration for a language.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
*--------------------------------------------------------------------------------------------*/

import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { getEditorFeatures } from 'vs/editor/common/editorFeatures';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IDisposable } from 'xterm';

class EditorFeaturesInstantiator extends Disposable implements IWorkbenchContribution {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { ILanguageService } from 'vs/editor/common/languages/language';
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ITextMateTokenizationService } from 'vs/workbench/services/textMate/browser/textMateTokenizationFeature';
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
import { IExtensionResourceLoaderService } from 'vs/platform/extensionResourceLoader/common/extensionResourceLoader';
import { hash } from 'vs/base/common/hash';
Expand Down Expand Up @@ -95,15 +94,14 @@ export class LanguageConfigurationFileHandler extends Disposable {
private readonly _done = new Map<string, number>();

constructor(
@ITextMateTokenizationService textMateService: ITextMateTokenizationService,
@ILanguageService private readonly _languageService: ILanguageService,
@IExtensionResourceLoaderService private readonly _extensionResourceLoaderService: IExtensionResourceLoaderService,
@IExtensionService private readonly _extensionService: IExtensionService,
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService,
) {
super();

this._register(this._languageService.onDidEncounterLanguage(async (languageIdentifier) => {
this._register(this._languageService.onDidRequestBasicLanguageFeatures(async (languageIdentifier) => {
// Modes can be instantiated before the extension points have finished registering
this._extensionService.whenInstalledExtensionsRegistered().then(() => {
this._loadConfigurationsForMode(languageIdentifier);
Expand All @@ -115,9 +113,6 @@ export class LanguageConfigurationFileHandler extends Disposable {
this._loadConfigurationsForMode(languageId);
}
}));
this._register(textMateService.onDidEncounterLanguage((languageId) => {
this._loadConfigurationsForMode(languageId);
}));
}

private async _loadConfigurationsForMode(languageId: string): Promise<void> {
Expand Down
5 changes: 3 additions & 2 deletions src/vs/workbench/services/language/common/languageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,11 @@ export class WorkbenchLanguageService extends LanguageService {
this.updateMime();
});

this.onDidEncounterLanguage((languageId) => {
this._register(this.onDidRequestRichLanguageFeatures((languageId) => {
// extension activation
this._extensionService.activateByEvent(`onLanguage:${languageId}`);
this._extensionService.activateByEvent(`onLanguage`);
});
}));
}

private updateMime(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,20 @@
import { registerSingleton, InstantiationType } from 'vs/platform/instantiation/common/extensions';
import { ITextMateTokenizationService } from 'vs/workbench/services/textMate/browser/textMateTokenizationFeature';
import { TextMateTokenizationFeature } from 'vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';

/**
* Makes sure the ITextMateTokenizationService is instantiated
*/
class TextMateTokenizationInstantiator implements IWorkbenchContribution {
constructor(
@ITextMateTokenizationService _textMateTokenizationService: ITextMateTokenizationService
) { }
}

registerSingleton(ITextMateTokenizationService, TextMateTokenizationFeature, InstantiationType.Eager);

const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(TextMateTokenizationInstantiator, LifecyclePhase.Ready);
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Event } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import type { IGrammar } from 'vscode-textmate';

Expand All @@ -12,8 +11,6 @@ export const ITextMateTokenizationService = createDecorator<ITextMateTokenizatio
export interface ITextMateTokenizationService {
readonly _serviceBrand: undefined;

onDidEncounterLanguage: Event<string>;

createGrammar(languageId: string): Promise<IGrammar | null>;

startDebugMode(printFn: (str: string) => void, onStop: () => void): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import * as dom from 'vs/base/browser/dom';
import { equals as equalArray } from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { FileAccess, nodeModulesAsarUnpackedPath, nodeModulesPath } from 'vs/base/common/network';
import { isWeb } from 'vs/base/common/platform';
Expand Down Expand Up @@ -40,9 +39,6 @@ import type { IGrammar, IOnigLib, IRawTheme } from 'vscode-textmate';
export class TextMateTokenizationFeature extends Disposable implements ITextMateTokenizationService {
public _serviceBrand: undefined;

private readonly _onDidEncounterLanguage: Emitter<string> = this._register(new Emitter<string>());
public readonly onDidEncounterLanguage: Event<string> = this._onDidEncounterLanguage.event;

private readonly _styleElement: HTMLStyleElement;
private readonly _createdModes: string[] = [];
private readonly _encounteredLanguages: boolean[] = [];
Expand Down Expand Up @@ -80,7 +76,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate
this._updateTheme(this._themeService.getColorTheme(), false);
}));

this._languageService.onDidEncounterLanguage((languageId) => {
this._languageService.onDidRequestRichLanguageFeatures((languageId) => {
this._createdModes.push(languageId);
});
}
Expand Down Expand Up @@ -284,7 +280,7 @@ export class TextMateTokenizationFeature extends Disposable implements ITextMate
if (!this._encounteredLanguages[encodedLanguageId]) {
const languageId = this._languageService.languageIdCodec.decodeLanguageId(encodedLanguageId);
this._encounteredLanguages[encodedLanguageId] = true;
this._onDidEncounterLanguage.fire(languageId);
this._languageService.requestBasicLanguageFeatures(languageId);
}
});

Expand Down

0 comments on commit dfd5353

Please sign in to comment.