dc02116fea3055e4ac08034ea60c1ed5dc932814cdf2565e0c92e3744c163370759abb4cc077dab815973a33b04a930a590c597e20c8330810e030c4cb6fe3 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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. import * as dom from '../../../base/browser/dom.js';
  6. import { addMatchMediaChangeListener } from '../../../base/browser/browser.js';
  7. import { Color } from '../../../base/common/color.js';
  8. import { Emitter } from '../../../base/common/event.js';
  9. import { TokenizationRegistry } from '../../common/languages.js';
  10. import { TokenMetadata } from '../../common/encodedTokenAttributes.js';
  11. import { TokenTheme, generateTokensCSSForColorMap } from '../../common/languages/supports/tokenization.js';
  12. import { hc_black, hc_light, vs, vs_dark } from '../common/themes.js';
  13. import { Registry } from '../../../platform/registry/common/platform.js';
  14. import { asCssVariableName, Extensions } from '../../../platform/theme/common/colorRegistry.js';
  15. import { Extensions as ThemingExtensions } from '../../../platform/theme/common/themeService.js';
  16. import { Disposable } from '../../../base/common/lifecycle.js';
  17. import { ColorScheme, isDark, isHighContrast } from '../../../platform/theme/common/theme.js';
  18. import { getIconsStyleSheet, UnthemedProductIconTheme } from '../../../platform/theme/browser/iconsStyleSheet.js';
  19. export const VS_LIGHT_THEME_NAME = 'vs';
  20. export const VS_DARK_THEME_NAME = 'vs-dark';
  21. export const HC_BLACK_THEME_NAME = 'hc-black';
  22. export const HC_LIGHT_THEME_NAME = 'hc-light';
  23. const colorRegistry = Registry.as(Extensions.ColorContribution);
  24. const themingRegistry = Registry.as(ThemingExtensions.ThemingContribution);
  25. class StandaloneTheme {
  26. constructor(name, standaloneThemeData) {
  27. this.semanticHighlighting = false;
  28. this.themeData = standaloneThemeData;
  29. const base = standaloneThemeData.base;
  30. if (name.length > 0) {
  31. if (isBuiltinTheme(name)) {
  32. this.id = name;
  33. }
  34. else {
  35. this.id = base + ' ' + name;
  36. }
  37. this.themeName = name;
  38. }
  39. else {
  40. this.id = base;
  41. this.themeName = base;
  42. }
  43. this.colors = null;
  44. this.defaultColors = Object.create(null);
  45. this._tokenTheme = null;
  46. }
  47. get base() {
  48. return this.themeData.base;
  49. }
  50. notifyBaseUpdated() {
  51. if (this.themeData.inherit) {
  52. this.colors = null;
  53. this._tokenTheme = null;
  54. }
  55. }
  56. getColors() {
  57. if (!this.colors) {
  58. const colors = new Map();
  59. for (const id in this.themeData.colors) {
  60. colors.set(id, Color.fromHex(this.themeData.colors[id]));
  61. }
  62. if (this.themeData.inherit) {
  63. const baseData = getBuiltinRules(this.themeData.base);
  64. for (const id in baseData.colors) {
  65. if (!colors.has(id)) {
  66. colors.set(id, Color.fromHex(baseData.colors[id]));
  67. }
  68. }
  69. }
  70. this.colors = colors;
  71. }
  72. return this.colors;
  73. }
  74. getColor(colorId, useDefault) {
  75. const color = this.getColors().get(colorId);
  76. if (color) {
  77. return color;
  78. }
  79. if (useDefault !== false) {
  80. return this.getDefault(colorId);
  81. }
  82. return undefined;
  83. }
  84. getDefault(colorId) {
  85. let color = this.defaultColors[colorId];
  86. if (color) {
  87. return color;
  88. }
  89. color = colorRegistry.resolveDefaultColor(colorId, this);
  90. this.defaultColors[colorId] = color;
  91. return color;
  92. }
  93. defines(colorId) {
  94. return Object.prototype.hasOwnProperty.call(this.getColors(), colorId);
  95. }
  96. get type() {
  97. switch (this.base) {
  98. case VS_LIGHT_THEME_NAME: return ColorScheme.LIGHT;
  99. case HC_BLACK_THEME_NAME: return ColorScheme.HIGH_CONTRAST_DARK;
  100. case HC_LIGHT_THEME_NAME: return ColorScheme.HIGH_CONTRAST_LIGHT;
  101. default: return ColorScheme.DARK;
  102. }
  103. }
  104. get tokenTheme() {
  105. if (!this._tokenTheme) {
  106. let rules = [];
  107. let encodedTokensColors = [];
  108. if (this.themeData.inherit) {
  109. const baseData = getBuiltinRules(this.themeData.base);
  110. rules = baseData.rules;
  111. if (baseData.encodedTokensColors) {
  112. encodedTokensColors = baseData.encodedTokensColors;
  113. }
  114. }
  115. // Pick up default colors from `editor.foreground` and `editor.background` if available
  116. const editorForeground = this.themeData.colors['editor.foreground'];
  117. const editorBackground = this.themeData.colors['editor.background'];
  118. if (editorForeground || editorBackground) {
  119. const rule = { token: '' };
  120. if (editorForeground) {
  121. rule.foreground = editorForeground;
  122. }
  123. if (editorBackground) {
  124. rule.background = editorBackground;
  125. }
  126. rules.push(rule);
  127. }
  128. rules = rules.concat(this.themeData.rules);
  129. if (this.themeData.encodedTokensColors) {
  130. encodedTokensColors = this.themeData.encodedTokensColors;
  131. }
  132. this._tokenTheme = TokenTheme.createFromRawTokenTheme(rules, encodedTokensColors);
  133. }
  134. return this._tokenTheme;
  135. }
  136. getTokenStyleMetadata(type, modifiers, modelLanguage) {
  137. // use theme rules match
  138. const style = this.tokenTheme._match([type].concat(modifiers).join('.'));
  139. const metadata = style.metadata;
  140. const foreground = TokenMetadata.getForeground(metadata);
  141. const fontStyle = TokenMetadata.getFontStyle(metadata);
  142. return {
  143. foreground: foreground,
  144. italic: Boolean(fontStyle & 1 /* FontStyle.Italic */),
  145. bold: Boolean(fontStyle & 2 /* FontStyle.Bold */),
  146. underline: Boolean(fontStyle & 4 /* FontStyle.Underline */),
  147. strikethrough: Boolean(fontStyle & 8 /* FontStyle.Strikethrough */)
  148. };
  149. }
  150. }
  151. function isBuiltinTheme(themeName) {
  152. return (themeName === VS_LIGHT_THEME_NAME
  153. || themeName === VS_DARK_THEME_NAME
  154. || themeName === HC_BLACK_THEME_NAME
  155. || themeName === HC_LIGHT_THEME_NAME);
  156. }
  157. function getBuiltinRules(builtinTheme) {
  158. switch (builtinTheme) {
  159. case VS_LIGHT_THEME_NAME:
  160. return vs;
  161. case VS_DARK_THEME_NAME:
  162. return vs_dark;
  163. case HC_BLACK_THEME_NAME:
  164. return hc_black;
  165. case HC_LIGHT_THEME_NAME:
  166. return hc_light;
  167. }
  168. }
  169. function newBuiltInTheme(builtinTheme) {
  170. const themeData = getBuiltinRules(builtinTheme);
  171. return new StandaloneTheme(builtinTheme, themeData);
  172. }
  173. export class StandaloneThemeService extends Disposable {
  174. constructor() {
  175. super();
  176. this._onColorThemeChange = this._register(new Emitter());
  177. this.onDidColorThemeChange = this._onColorThemeChange.event;
  178. this._onProductIconThemeChange = this._register(new Emitter());
  179. this.onDidProductIconThemeChange = this._onProductIconThemeChange.event;
  180. this._environment = Object.create(null);
  181. this._builtInProductIconTheme = new UnthemedProductIconTheme();
  182. this._autoDetectHighContrast = true;
  183. this._knownThemes = new Map();
  184. this._knownThemes.set(VS_LIGHT_THEME_NAME, newBuiltInTheme(VS_LIGHT_THEME_NAME));
  185. this._knownThemes.set(VS_DARK_THEME_NAME, newBuiltInTheme(VS_DARK_THEME_NAME));
  186. this._knownThemes.set(HC_BLACK_THEME_NAME, newBuiltInTheme(HC_BLACK_THEME_NAME));
  187. this._knownThemes.set(HC_LIGHT_THEME_NAME, newBuiltInTheme(HC_LIGHT_THEME_NAME));
  188. const iconsStyleSheet = getIconsStyleSheet(this);
  189. this._codiconCSS = iconsStyleSheet.getCSS();
  190. this._themeCSS = '';
  191. this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
  192. this._globalStyleElement = null;
  193. this._styleElements = [];
  194. this._colorMapOverride = null;
  195. this.setTheme(VS_LIGHT_THEME_NAME);
  196. this._onOSSchemeChanged();
  197. iconsStyleSheet.onDidChange(() => {
  198. this._codiconCSS = iconsStyleSheet.getCSS();
  199. this._updateCSS();
  200. });
  201. addMatchMediaChangeListener('(forced-colors: active)', () => {
  202. this._onOSSchemeChanged();
  203. });
  204. }
  205. registerEditorContainer(domNode) {
  206. if (dom.isInShadowDOM(domNode)) {
  207. return this._registerShadowDomContainer(domNode);
  208. }
  209. return this._registerRegularEditorContainer();
  210. }
  211. _registerRegularEditorContainer() {
  212. if (!this._globalStyleElement) {
  213. this._globalStyleElement = dom.createStyleSheet();
  214. this._globalStyleElement.className = 'monaco-colors';
  215. this._globalStyleElement.textContent = this._allCSS;
  216. this._styleElements.push(this._globalStyleElement);
  217. }
  218. return Disposable.None;
  219. }
  220. _registerShadowDomContainer(domNode) {
  221. const styleElement = dom.createStyleSheet(domNode);
  222. styleElement.className = 'monaco-colors';
  223. styleElement.textContent = this._allCSS;
  224. this._styleElements.push(styleElement);
  225. return {
  226. dispose: () => {
  227. for (let i = 0; i < this._styleElements.length; i++) {
  228. if (this._styleElements[i] === styleElement) {
  229. this._styleElements.splice(i, 1);
  230. return;
  231. }
  232. }
  233. }
  234. };
  235. }
  236. defineTheme(themeName, themeData) {
  237. if (!/^[a-z0-9\-]+$/i.test(themeName)) {
  238. throw new Error('Illegal theme name!');
  239. }
  240. if (!isBuiltinTheme(themeData.base) && !isBuiltinTheme(themeName)) {
  241. throw new Error('Illegal theme base!');
  242. }
  243. // set or replace theme
  244. this._knownThemes.set(themeName, new StandaloneTheme(themeName, themeData));
  245. if (isBuiltinTheme(themeName)) {
  246. this._knownThemes.forEach(theme => {
  247. if (theme.base === themeName) {
  248. theme.notifyBaseUpdated();
  249. }
  250. });
  251. }
  252. if (this._theme.themeName === themeName) {
  253. this.setTheme(themeName); // refresh theme
  254. }
  255. }
  256. getColorTheme() {
  257. return this._theme;
  258. }
  259. setColorMapOverride(colorMapOverride) {
  260. this._colorMapOverride = colorMapOverride;
  261. this._updateThemeOrColorMap();
  262. }
  263. setTheme(themeName) {
  264. let theme;
  265. if (this._knownThemes.has(themeName)) {
  266. theme = this._knownThemes.get(themeName);
  267. }
  268. else {
  269. theme = this._knownThemes.get(VS_LIGHT_THEME_NAME);
  270. }
  271. this._updateActualTheme(theme);
  272. }
  273. _updateActualTheme(desiredTheme) {
  274. if (!desiredTheme || this._theme === desiredTheme) {
  275. // Nothing to do
  276. return;
  277. }
  278. this._theme = desiredTheme;
  279. this._updateThemeOrColorMap();
  280. }
  281. _onOSSchemeChanged() {
  282. if (this._autoDetectHighContrast) {
  283. const wantsHighContrast = window.matchMedia(`(forced-colors: active)`).matches;
  284. if (wantsHighContrast !== isHighContrast(this._theme.type)) {
  285. // switch to high contrast or non-high contrast but stick to dark or light
  286. let newThemeName;
  287. if (isDark(this._theme.type)) {
  288. newThemeName = wantsHighContrast ? HC_BLACK_THEME_NAME : VS_DARK_THEME_NAME;
  289. }
  290. else {
  291. newThemeName = wantsHighContrast ? HC_LIGHT_THEME_NAME : VS_LIGHT_THEME_NAME;
  292. }
  293. this._updateActualTheme(this._knownThemes.get(newThemeName));
  294. }
  295. }
  296. }
  297. setAutoDetectHighContrast(autoDetectHighContrast) {
  298. this._autoDetectHighContrast = autoDetectHighContrast;
  299. this._onOSSchemeChanged();
  300. }
  301. _updateThemeOrColorMap() {
  302. const cssRules = [];
  303. const hasRule = {};
  304. const ruleCollector = {
  305. addRule: (rule) => {
  306. if (!hasRule[rule]) {
  307. cssRules.push(rule);
  308. hasRule[rule] = true;
  309. }
  310. }
  311. };
  312. themingRegistry.getThemingParticipants().forEach(p => p(this._theme, ruleCollector, this._environment));
  313. const colorVariables = [];
  314. for (const item of colorRegistry.getColors()) {
  315. const color = this._theme.getColor(item.id, true);
  316. if (color) {
  317. colorVariables.push(`${asCssVariableName(item.id)}: ${color.toString()};`);
  318. }
  319. }
  320. ruleCollector.addRule(`.monaco-editor { ${colorVariables.join('\n')} }`);
  321. const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap();
  322. ruleCollector.addRule(generateTokensCSSForColorMap(colorMap));
  323. this._themeCSS = cssRules.join('\n');
  324. this._updateCSS();
  325. TokenizationRegistry.setColorMap(colorMap);
  326. this._onColorThemeChange.fire(this._theme);
  327. }
  328. _updateCSS() {
  329. this._allCSS = `${this._codiconCSS}\n${this._themeCSS}`;
  330. this._styleElements.forEach(styleElement => styleElement.textContent = this._allCSS);
  331. }
  332. getFileIconTheme() {
  333. return {
  334. hasFileIcons: false,
  335. hasFolderIcons: false,
  336. hidesExplorerArrows: false
  337. };
  338. }
  339. getProductIconTheme() {
  340. return this._builtInProductIconTheme;
  341. }
  342. }