Skip to content

Commit b2498a1

Browse files
authored
Merge pull request #118846 from Mai-Lapyst/language-default-icon
Adding default fileicon support to language contributions
2 parents 4636c22 + f88c6bd commit b2498a1

File tree

12 files changed

+309
-170
lines changed

12 files changed

+309
-170
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ export interface ILanguageExtensionPoint {
1919
aliases?: string[];
2020
mimetypes?: string[];
2121
configuration?: URI;
22+
/**
23+
* @internal
24+
*/
25+
icon?: ILanguageIcon;
2226
}
2327

2428
export interface ILanguageSelection {
@@ -31,6 +35,11 @@ export interface ILanguageNameIdPair {
3135
readonly languageId: string;
3236
}
3337

38+
export interface ILanguageIcon {
39+
readonly light: URI;
40+
readonly dark: URI;
41+
}
42+
3443
export interface ILanguageService {
3544
readonly _serviceBrand: undefined;
3645

@@ -76,6 +85,11 @@ export interface ILanguageService {
7685
*/
7786
getMimeType(languageId: string): string | null;
7887

88+
/**
89+
* Get the default icon for the language.
90+
*/
91+
getIcon(languageId: string): ILanguageIcon | null;
92+
7993
/**
8094
* Get all file extensions for a language.
8195
*/

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event';
77
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
88
import { URI } from 'vs/base/common/uri';
99
import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry';
10-
import { ILanguageNameIdPair, ILanguageSelection, ILanguageService } from 'vs/editor/common/services/language';
10+
import { ILanguageNameIdPair, ILanguageSelection, ILanguageService, ILanguageIcon } from 'vs/editor/common/services/language';
1111
import { firstOrDefault } from 'vs/base/common/arrays';
1212
import { ILanguageIdCodec, TokenizationRegistry } from 'vs/editor/common/languages';
1313
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
@@ -61,6 +61,10 @@ export class LanguageService extends Disposable implements ILanguageService {
6161
return this._registry.getMimeType(languageId);
6262
}
6363

64+
public getIcon(languageId: string): ILanguageIcon | null {
65+
return this._registry.getIcon(languageId);
66+
}
67+
6468
public getExtensions(languageId: string): ReadonlyArray<string> {
6569
return this._registry.getExtensions(languageId);
6670
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { clearLanguageAssociations, getMimeTypes, registerLanguageAssociation }
1212
import { URI } from 'vs/base/common/uri';
1313
import { ILanguageIdCodec, LanguageId } from 'vs/editor/common/languages';
1414
import { ModesRegistry, PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
15-
import { ILanguageExtensionPoint, ILanguageNameIdPair } from 'vs/editor/common/services/language';
15+
import { ILanguageExtensionPoint, ILanguageNameIdPair, ILanguageIcon } from 'vs/editor/common/services/language';
1616
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
1717
import { Registry } from 'vs/platform/registry/common/platform';
1818

@@ -27,6 +27,7 @@ export interface IResolvedLanguage {
2727
extensions: string[];
2828
filenames: string[];
2929
configurationFiles: URI[];
30+
icons: ILanguageIcon[];
3031
}
3132

3233
export class LanguageIdCodec implements ILanguageIdCodec {
@@ -162,7 +163,8 @@ export class LanguagesRegistry extends Disposable {
162163
aliases: [],
163164
extensions: [],
164165
filenames: [],
165-
configurationFiles: []
166+
configurationFiles: [],
167+
icons: []
166168
};
167169
this._languages[langId] = resolvedLanguage;
168170
}
@@ -260,6 +262,10 @@ export class LanguagesRegistry extends Disposable {
260262
if (lang.configuration) {
261263
resolvedLanguage.configurationFiles.push(lang.configuration);
262264
}
265+
266+
if (lang.icon) {
267+
resolvedLanguage.icons.push(lang.icon);
268+
}
263269
}
264270

265271
public isRegisteredLanguageId(languageId: string | null | undefined): boolean {
@@ -316,6 +322,14 @@ export class LanguagesRegistry extends Disposable {
316322
return this._languages[languageId].filenames;
317323
}
318324

325+
public getIcon(languageId: string): ILanguageIcon | null {
326+
if (!hasOwnProperty.call(this._languages, languageId)) {
327+
return null;
328+
}
329+
const language = this._languages[languageId];
330+
return (language.icons[0] || null);
331+
}
332+
319333
public getConfigurationFiles(languageId: string): ReadonlyArray<URI> {
320334
if (!hasOwnProperty.call(this._languages, languageId)) {
321335
return [];

src/vs/platform/theme/browser/iconsStyleSheet.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Emitter, Event } from 'vs/base/common/event';
88
import { getIconRegistry, IconContribution, IconFontDefinition } from 'vs/platform/theme/common/iconRegistry';
99
import { IProductIconTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService';
1010

11-
1211
export interface IIconsStyleSheet {
1312
getCSS(): string;
1413
readonly onDidChange: Event<void>;

src/vs/platform/theme/common/iconRegistry.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ export interface IconContribution {
3737
readonly defaults: IconDefaults;
3838
}
3939

40+
export namespace IconContribution {
41+
export function getDefinition(contribution: IconContribution, registry: IIconRegistry): IconDefinition | undefined {
42+
let definition = contribution.defaults;
43+
while (ThemeIcon.isThemeIcon(definition)) {
44+
const c = iconRegistry.getIcon(definition.id);
45+
if (!c) {
46+
return undefined;
47+
}
48+
definition = c.defaults;
49+
}
50+
return definition;
51+
}
52+
}
53+
4054
export interface IconFontContribution {
4155
readonly id: string;
4256
getDefinition(): IconFontDefinition | undefined;

src/vs/workbench/services/extensions/common/extensionsApiProposals.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const allApiProposals = Object.freeze({
2525
fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts',
2626
inlayHints: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlayHints.d.ts',
2727
inlineCompletions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts',
28+
languageIcon: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageIcon.d.ts',
2829
languageStatus: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.languageStatus.d.ts',
2930
notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts',
3031
notebookConcatTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts',

src/vs/workbench/services/language/common/languageService.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import { LanguageService } from 'vs/editor/common/services/languageService';
1212
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
1313
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
1414
import { FILES_ASSOCIATIONS_CONFIG, IFilesConfiguration } from 'vs/platform/files/common/files';
15-
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
15+
import { IExtensionService, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
1616
import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
1717
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
18+
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
1819

1920
export interface IRawLanguageExtensionPoint {
2021
id: string;
@@ -25,6 +26,7 @@ export interface IRawLanguageExtensionPoint {
2526
aliases: string[];
2627
mimetypes: string[];
2728
configuration: string;
29+
icon: { light: string; dark: string };
2830
}
2931

3032
export const languagesExtPoint: IExtensionPoint<IRawLanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<IRawLanguageExtensionPoint[]>({
@@ -84,6 +86,20 @@ export const languagesExtPoint: IExtensionPoint<IRawLanguageExtensionPoint[]> =
8486
description: localize('vscode.extension.contributes.languages.configuration', 'A relative path to a file containing configuration options for the language.'),
8587
type: 'string',
8688
default: './language-configuration.json'
89+
},
90+
icon: {
91+
type: 'object',
92+
description: localize('vscode.extension.contributes.languages.icon', 'A icon to use as file icon, if no icon theme provides one for the language.'),
93+
properties: {
94+
light: {
95+
description: localize('vscode.extension.contributes.languages.icon.light', 'Icon path when a light theme is used'),
96+
type: 'string'
97+
},
98+
dark: {
99+
description: localize('vscode.extension.contributes.languages.icon.dark', 'Icon path when a dark theme is used'),
100+
type: 'string'
101+
}
102+
}
87103
}
88104
}
89105
}
@@ -116,7 +132,7 @@ export class WorkbenchLanguageService extends LanguageService {
116132

117133
for (let j = 0, lenJ = extension.value.length; j < lenJ; j++) {
118134
let ext = extension.value[j];
119-
if (isValidLanguageExtensionPoint(ext, extension.collector)) {
135+
if (isValidLanguageExtensionPoint(ext, extension.description, extension.collector)) {
120136
let configuration: URI | undefined = undefined;
121137
if (ext.configuration) {
122138
configuration = joinPath(extension.description.extensionLocation, ext.configuration);
@@ -129,7 +145,11 @@ export class WorkbenchLanguageService extends LanguageService {
129145
firstLine: ext.firstLine,
130146
aliases: ext.aliases,
131147
mimetypes: ext.mimetypes,
132-
configuration: configuration
148+
configuration: configuration,
149+
icon: ext.icon && {
150+
light: joinPath(extension.description.extensionLocation, ext.icon.light),
151+
dark: joinPath(extension.description.extensionLocation, ext.icon.dark)
152+
}
133153
});
134154
}
135155
}
@@ -184,7 +204,7 @@ function isUndefinedOrStringArray(value: string[]): boolean {
184204
return value.every(item => typeof item === 'string');
185205
}
186206

187-
function isValidLanguageExtensionPoint(value: IRawLanguageExtensionPoint, collector: ExtensionMessageCollector): boolean {
207+
function isValidLanguageExtensionPoint(value: IRawLanguageExtensionPoint, extension: IExtensionDescription, collector: ExtensionMessageCollector): boolean {
188208
if (!value) {
189209
collector.error(localize('invalid.empty', "Empty value for `contributes.{0}`", languagesExtPoint.name));
190210
return false;
@@ -217,6 +237,17 @@ function isValidLanguageExtensionPoint(value: IRawLanguageExtensionPoint, collec
217237
collector.error(localize('opt.mimetypes', "property `{0}` can be omitted and must be of type `string[]`", 'mimetypes'));
218238
return false;
219239
}
240+
if (typeof value.icon !== 'undefined') {
241+
const proposal = 'languageIcon';
242+
if (!isProposedApiEnabled(extension, proposal)) {
243+
collector.error(`Extension '${extension.identifier.value}' CANNOT use API proposal: ${proposal}.\nIts package.json#enabledApiProposals-property declares: ${extension.enabledApiProposals?.join(', ') ?? '[]'} but NOT ${proposal}.\n The missing proposal MUST be added and you must start in extension development mode or use the following command line switch: --enable-proposed-api ${extension.identifier.value}`);
244+
return false;
245+
}
246+
if (typeof value.icon !== 'object' || typeof value.icon.light !== 'string' || typeof value.icon.dark !== 'string') {
247+
collector.error(localize('opt.icon', "property `{0}` can be omitted and must be of type `object` with properties `{1}` and `{2}` of type `string`", 'icon', 'light', 'dark'));
248+
return false;
249+
}
250+
}
220251
return true;
221252
}
222253

0 commit comments

Comments
 (0)