Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Add custom themes for the Monaco editor #9463

Merged
merged 21 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Interface for the colors of the editor.
* See https://code.visualstudio.com/api/references/theme-color
* All colors must be in the format '#RRGGBB' or '#RRGGBBAA'.
*/
export interface EditorColors {
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
/**
* The background color of the editor.
*/
backgroundColor?: string;
/**
* The default color of all text in the editor, not including syntax highlighting.
*/
foregroundColor?: string;
/**
* Colors for line numbers in the editor.
*/
lineNumbers?: {
/**
* The color of the line numbers.
*/
foregroundColor?: string;
/**
* The color of the line number of the line that the cursor is on.
*/
activeForegroundColor?: string;
/**
* The color of the line numbers for dimmed lines. This is used for the final newline of the code.
*/
dimmedForegroundColor?: string;
};
/**
* Colors for the active line highlight in the editor.
*/
lineHighlight?: {
/**
* The color used as the background color for the cursor's current line.
*/
backgroundColor?: string;
/**
* The color used for the border of the cursor's current line.
*/
borderColor?: string;
};
/**
* Colors for the diff editor.
*/
diff?: {
/**
* The background color for inserted lines in the diff editor.
*/
insertedLineBackgroundColor?: string;
/**
* The background color for inserted text in the diff editor.
* This will overlap with the `insertedLineBackgroundColor`.
*/
insertedTextBackgroundColor?: string;
/**
* The background color for removed lines in the diff editor.
*/
removedTextBackgroundColor?: string;
/**
* The background color for removed text in the diff editor.
* This will overlap with the `removedLineBackgroundColor`.
*/
removedLineBackgroundColor?: string;
/**
* The color used for the diagonal fill in the diff editor.
* This is used when the diff editor pads the length of the files to align the lines of the original and modified files.
*/
diagonalFillColor?: string;
/**
* Colors for the diff editor gutter. This is the area to the left of the editor that shows the line numbers.
*/
gutter?: {
/**
* The background color for inserted lines in the diff editor gutter.
*/
insertedLineBackgroundColor?: string;
/**
* The background color for removed lines in the diff editor gutter.
*/
removedLineBackgroundColor?: string;
};
};
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
}
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Interface for the style of a token in a language.
* The editor applies these styles to the tokens in the specified language (or all languages), e.g. identifiers, keywords, etc.
*/
export interface LanguageTokenStyleDefinition {
/**
* The token to style, e.g. identifier
*/
token: string;
/**
* The language ID for which the token style should be applied.
* If not specified, the style is applied to all languages.
*/
languageId?: string;
/**
* The color of the text that should be applied to the token.
*/
foregroundColor?: string;
/**
* The background color that should be applied to the token.
*/
backgroundColor?: string;
/**
* The font style that should be applied to the token.
*/
fontStyle?: 'italic' | 'bold' | 'underline';
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { MonacoThemeDefinition } from 'app/shared/monaco-editor/model/themes/monaco-theme-definition.interface';

export const MONACO_DARK_THEME_DEFINITION: MonacoThemeDefinition = {
id: 'custom-dark',
baseTheme: 'vs-dark',
tokenStyles: [
{
token: 'keyword',
foregroundColor: '#ff7b72',
},
{
token: 'comment',
foregroundColor: '#9198a1',
},
{
token: 'string',
foregroundColor: '#a5d6ff',
},
{
token: 'number',
foregroundColor: '#79c0ff',
},
],
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
editorColors: {
backgroundColor: '#181a18',
lineHighlight: {
borderColor: '#00000000',
backgroundColor: '#282a2e',
},
lineNumbers: {
foregroundColor: '#ffffff',
activeForegroundColor: '#ffffff',
dimmedForegroundColor: '#ffffff',
},
diff: {
insertedLineBackgroundColor: '#2ea04326',
insertedTextBackgroundColor: '#2ea04326',
removedLineBackgroundColor: '#f8514926',
removedTextBackgroundColor: '#f8514946',
diagonalFillColor: '#00000000',
gutter: {
insertedLineBackgroundColor: '#3fb9504d',
removedLineBackgroundColor: '#f851494d',
},
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { MonacoThemeDefinition } from 'app/shared/monaco-editor/model/themes/monaco-theme-definition.interface';
import * as monaco from 'monaco-editor';

export class MonacoEditorTheme {
constructor(private readonly themeDefinition: MonacoThemeDefinition) {}

getId(): string {
return this.themeDefinition.id;
}

/**
* Creates a new record without any entries that have a value of `undefined`.
* @param record The record whose keys to filter.
* @returns The new record, only containing keys with defined values.
* @private
*/
private getRecordWithoutUndefinedEntries(record: Record<string, string | undefined>): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(record)) {
if (value !== undefined) {
result[key] = value;
}
}
return result;
}
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved

register(): void {
const colorDefinitions = this.themeDefinition.editorColors;
// The color keys are available here: https://code.visualstudio.com/api/references/theme-color
const colors = {
'editor.background': colorDefinitions.backgroundColor,
'editor.foreground': colorDefinitions.foregroundColor,
'editorLineNumber.foreground': colorDefinitions.lineNumbers?.foregroundColor,
'editorLineNumber.activeForeground': colorDefinitions.lineNumbers?.activeForegroundColor,
'editorLineNumber.dimmedForeground': colorDefinitions.lineNumbers?.dimmedForegroundColor,
'editor.lineHighlightBackground': colorDefinitions.lineHighlight?.backgroundColor,
'editor.lineHighlightBorder': colorDefinitions.lineHighlight?.borderColor,
'diffEditor.insertedLineBackground': colorDefinitions.diff?.insertedLineBackgroundColor,
'diffEditor.insertedTextBackground': colorDefinitions.diff?.insertedTextBackgroundColor,
'diffEditor.removedTextBackground': colorDefinitions.diff?.removedTextBackgroundColor,
'diffEditor.removedLineBackground': colorDefinitions.diff?.removedLineBackgroundColor,
'diffEditor.diagonalFill': colorDefinitions.diff?.diagonalFillColor,
'diffEditorGutter.insertedLineBackground': colorDefinitions.diff?.gutter?.insertedLineBackgroundColor,
'diffEditorGutter.removedLineBackground': colorDefinitions.diff?.gutter?.removedLineBackgroundColor,
};

const tokenStyleDefinitions = this.themeDefinition.tokenStyles;
const rules = tokenStyleDefinitions.map((tokenDefinition) => {
// Language-specific tokens have the key `token.languageId`, e.g. keyword.custom-md
return {
token: `${tokenDefinition.token}${tokenDefinition.languageId ? '.' + tokenDefinition.languageId : ''}`,
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
foreground: tokenDefinition.foregroundColor,
background: tokenDefinition.backgroundColor,
fontStyle: tokenDefinition.fontStyle,
};
});

// We cannot pass undefined colors to Monaco, so we filter them out to preserve the default values.
monaco.editor.defineTheme(this.getId(), {
base: this.themeDefinition.baseTheme,
inherit: true,
rules: rules,
colors: this.getRecordWithoutUndefinedEntries(colors),
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { MonacoThemeDefinition } from 'app/shared/monaco-editor/model/themes/monaco-theme-definition.interface';

export const MONACO_LIGHT_THEME_DEFINITION: MonacoThemeDefinition = {
id: 'custom-light',
baseTheme: 'vs',
tokenStyles: [
{
token: 'keyword',
foregroundColor: '#cf222e',
},
{
token: 'comment',
foregroundColor: '#59636e',
},
{
token: 'string',
foregroundColor: '#0a3069',
},
{
token: 'number',
foregroundColor: '#0550ae',
},
],
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
editorColors: {
lineHighlight: {
borderColor: '#00000000',
backgroundColor: '#e8e8e8',
},
lineNumbers: {
foregroundColor: '#000000',
activeForegroundColor: '#000000',
dimmedForegroundColor: '#000000',
},
diff: {
insertedLineBackgroundColor: '#dafbe1e6',
insertedTextBackgroundColor: '#aceebbe6',
removedLineBackgroundColor: '#ffebe9ef',
removedTextBackgroundColor: '#ff818250',
diagonalFillColor: '#00000000',
gutter: {
insertedLineBackgroundColor: '#d1f8d9',
removedLineBackgroundColor: '#ffcecb',
},
},
},
};
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LanguageTokenStyleDefinition } from 'app/shared/monaco-editor/model/themes/language-token-style-definition.interface';
import { EditorColors } from 'app/shared/monaco-editor/model/themes/editor-colors.interface';

export interface MonacoThemeDefinition {
id: string;
baseTheme: 'vs' | 'vs-dark' | 'hc-light' | 'hc-black';
tokenStyles: LanguageTokenStyleDefinition[];
editorColors: EditorColors;
}
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
.monaco-editor-container {
width: 100%;
height: 100%;

.monaco-editor {
// Disables the focus border around the editor.
outline: none;
}
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import * as monaco from 'monaco-editor';
import { CUSTOM_MARKDOWN_CONFIG, CUSTOM_MARKDOWN_LANGUAGE, CUSTOM_MARKDOWN_LANGUAGE_ID } from 'app/shared/monaco-editor/model/languages/monaco-custom-markdown.language';
import { Theme, ThemeService } from 'app/core/theme/theme.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { MONACO_LIGHT_THEME_DEFINITION } from 'app/shared/monaco-editor/model/themes/monaco-light.theme';
import { MonacoEditorTheme } from 'app/shared/monaco-editor/model/themes/monaco-editor-theme.model';
import { MONACO_DARK_THEME_DEFINITION } from 'app/shared/monaco-editor/model/themes/monaco-dark.theme';

/**
* Service providing shared functionality for the Monaco editor.
Expand All @@ -11,29 +14,41 @@ import { toSignal } from '@angular/core/rxjs-interop';
*/
@Injectable({ providedIn: 'root' })
export class MonacoEditorService {
static readonly LIGHT_THEME_ID = 'vs';
static readonly DARK_THEME_ID = 'vs-dark';

private readonly themeService: ThemeService = inject(ThemeService);
private readonly currentTheme = toSignal(this.themeService.getCurrentThemeObservable(), { requireSync: true });

private lightTheme: MonacoEditorTheme;
private darkTheme: MonacoEditorTheme;

constructor() {
monaco.languages.register({ id: CUSTOM_MARKDOWN_LANGUAGE_ID });
monaco.languages.setLanguageConfiguration(CUSTOM_MARKDOWN_LANGUAGE_ID, CUSTOM_MARKDOWN_CONFIG);
monaco.languages.setMonarchTokensProvider(CUSTOM_MARKDOWN_LANGUAGE_ID, CUSTOM_MARKDOWN_LANGUAGE);
this.registerCustomThemes();
this.registerCustomMarkdownLanguage();

effect(() => {
this.applyTheme(this.currentTheme());
});
}

private registerCustomThemes(): void {
this.lightTheme = new MonacoEditorTheme(MONACO_LIGHT_THEME_DEFINITION);
this.darkTheme = new MonacoEditorTheme(MONACO_DARK_THEME_DEFINITION);
this.lightTheme.register();
this.darkTheme.register();
}

private registerCustomMarkdownLanguage(): void {
monaco.languages.register({ id: CUSTOM_MARKDOWN_LANGUAGE_ID });
monaco.languages.setLanguageConfiguration(CUSTOM_MARKDOWN_LANGUAGE_ID, CUSTOM_MARKDOWN_CONFIG);
monaco.languages.setMonarchTokensProvider(CUSTOM_MARKDOWN_LANGUAGE_ID, CUSTOM_MARKDOWN_LANGUAGE);
}
pzdr7 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Applies the given theme to the Monaco editor.
* @param artemisTheme The theme to apply.
* @private
*/
private applyTheme(artemisTheme: Theme): void {
monaco.editor.setTheme(artemisTheme === Theme.LIGHT ? MonacoEditorService.LIGHT_THEME_ID : MonacoEditorService.DARK_THEME_ID);
monaco.editor.setTheme(artemisTheme === Theme.LIGHT ? this.lightTheme.getId() : this.darkTheme.getId());
}

/**
Expand Down Expand Up @@ -79,6 +94,10 @@ export class MonacoEditorService {
hideUnchangedRegions: {
enabled: true,
},
guides: {
indentation: false,
},
renderLineHighlight: 'none',
fontSize: 12,
});
}
Expand Down
Loading
Loading