├── .gitignore ├── .vscode └── launch.json ├── .vscodeignore ├── icon.png ├── inject ├── inject.css ├── inject.ts ├── ripple.ts ├── rounded-ui.ts └── utils │ ├── element.ts │ └── proxy.ts ├── package.json ├── readme.md ├── screenshots ├── editor.png ├── light-theme.png └── settings.png ├── scripts ├── config.ts ├── dev.ts ├── github.ts ├── npm │ ├── build.ts │ └── tsconfig.json └── release.ts ├── src ├── extension.ts ├── inject.ts ├── theme │ ├── create.ts │ ├── syntax.ts │ └── utils.ts └── utils │ ├── appdata.ts │ ├── config.ts │ ├── extension.ts │ ├── file.ts │ └── object.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bun.lockb 3 | build/ 4 | *.vsix -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Dev: Extension", 5 | "request": "launch", 6 | "type": "extensionHost", 7 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--disable-extensions"], 8 | "outFiles": ["${workspaceFolder}/build/**/*.js"] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | ** 2 | 3 | !build/ 4 | !icon.png 5 | !package.json 6 | !readme.md -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibdev/material-code/b59ad725df57ae2d9c5610ee21d0a7be6b420348/icon.png -------------------------------------------------------------------------------- /inject/inject.css: -------------------------------------------------------------------------------- 1 | body { 2 | --radius: 20px; 3 | } 4 | 5 | [role='tab'] { 6 | border-radius: 20px 20px 8px 8px; 7 | } 8 | 9 | [role=button], 10 | [role=tooltip], 11 | [role=dialog], 12 | .monaco-menu, 13 | .editor-widget, /* find & replace */ 14 | .menubar-menu-button, /* title bar menu buttons */ 15 | .notifications-center { 16 | border-radius: var(--radius) !important; 17 | } 18 | 19 | .monaco-menu-container, 20 | .monaco-editor .suggest-widget, /* autocomplete */ 21 | .quick-input-widget, /* command palette */ 22 | .notification-toast { 23 | border-radius: var(--radius) !important; 24 | overflow: hidden; 25 | } 26 | 27 | /* icon button */ 28 | .codicon { 29 | border-radius: var(--radius); 30 | } 31 | 32 | input, 33 | select, 34 | .monaco-inputbox, /* extensions, settings search input */ 35 | .suggest-input-container { 36 | border-radius: 10px; 37 | padding-left: 8px; 38 | padding-right: 8px; 39 | } 40 | 41 | /* .selected-text radius */ 42 | .monaco-editor .top-left-radius { 43 | border-top-left-radius: 5px; 44 | } 45 | .monaco-editor .bottom-left-radius { 46 | border-bottom-left-radius: 5px; 47 | } 48 | .monaco-editor .top-right-radius { 49 | border-top-right-radius: 5px; 50 | } 51 | .monaco-editor .bottom-right-radius { 52 | border-bottom-right-radius: 5px; 53 | } 54 | 55 | /* ripple */ 56 | .ripple-container { 57 | position: absolute; 58 | top: 0; 59 | left: 0; 60 | bottom: 0; 61 | right: 0; 62 | border-radius: inherit; 63 | overflow: hidden; 64 | pointer-events: none; 65 | } 66 | .ripple { 67 | background-color: currentColor; 68 | opacity: 0.19; 69 | border-radius: 50%; 70 | position: absolute; 71 | } 72 | -------------------------------------------------------------------------------- /inject/inject.ts: -------------------------------------------------------------------------------- 1 | import './inject.css' 2 | import './ripple' 3 | import './rounded-ui' 4 | -------------------------------------------------------------------------------- /inject/ripple.ts: -------------------------------------------------------------------------------- 1 | import { addListeners, findParent, removeListeners } from './utils/element' 2 | 3 | type TriggerEvent = PointerEvent | FocusEvent | KeyboardEvent 4 | 5 | const showRipple = (element: HTMLElement, event: TriggerEvent) => { 6 | const rect = element.getBoundingClientRect() 7 | const x = 'clientX' in event ? event.clientX - rect.left : rect.width / 2 8 | const y = 'clientY' in event ? event.clientY - rect.top : rect.height / 2 9 | const maxRadius = Math.hypot(Math.max(x, rect.width - x), Math.max(y, rect.height - y)) 10 | 11 | const ripple = document.createElement('div') 12 | ripple.className = 'ripple' 13 | ripple.style.width = maxRadius * 2 + 'px' 14 | ripple.style.height = ripple.style.width 15 | ripple.style.left = x - maxRadius + 'px' 16 | ripple.style.top = y - maxRadius + 'px' 17 | 18 | const container = document.createElement('div') 19 | container.className = 'ripple-container' 20 | container.appendChild(ripple) 21 | element.appendChild(container) 22 | 23 | const animation = ripple.animate({ transform: ['scale(0.1)', 'scale(1)'] }, 190) 24 | 25 | const releaseEvents: Record> = { 26 | pointerdown: ['pointerup', 'pointerleave'], 27 | keydown: ['keyup'], 28 | focusin: ['blur'] 29 | } 30 | 31 | const hideRipple = async () => { 32 | removeListeners(element, releaseEvents[event.type], hideRipple) 33 | 34 | await animation.finished 35 | const hidingAnimation = ripple.animate( 36 | { opacity: [getComputedStyle(ripple).opacity, '0'] }, 37 | { duration: 100, fill: 'forwards' } 38 | ) 39 | await hidingAnimation.finished 40 | ripple.remove() 41 | container.remove() 42 | } 43 | 44 | addListeners(element, releaseEvents[event.type], hideRipple) 45 | } 46 | 47 | document.body.addEventListener('pointerdown', (event: PointerEvent) => { 48 | const rippleElement = findParent(event.target as HTMLElement, element => 49 | element.matches( 50 | '[role=button], [role=tab], [role=listitem], [role=treeitem], [role=menuitem], [role=option], .scrollbar > .slider' 51 | ) 52 | ) 53 | if (rippleElement) { 54 | if (getComputedStyle(rippleElement).position == 'static') rippleElement.style.position = 'relative' 55 | showRipple(rippleElement, event) 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /inject/rounded-ui.ts: -------------------------------------------------------------------------------- 1 | import { proxy } from './utils/proxy' 2 | 3 | const contextMenuClass = 'context-view' 4 | const contextMenuCss = `.${contextMenuClass} { border-radius: var(--radius); overflow: hidden; }` 5 | 6 | const styleEditorContextMenu = (shadowRoot: ShadowRoot) => { 7 | const sheet = new CSSStyleSheet() 8 | sheet.replaceSync(contextMenuCss) 9 | shadowRoot.adoptedStyleSheets = [sheet] 10 | } 11 | 12 | proxy(Element.prototype, 'attachShadow', (shadowRoot: ShadowRoot) => { 13 | proxy(shadowRoot, 'appendChild', (child: HTMLElement) => { 14 | if (child.classList.contains(contextMenuClass)) styleEditorContextMenu(shadowRoot) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /inject/utils/element.ts: -------------------------------------------------------------------------------- 1 | export const addListeners = ( 2 | element: HTMLElement | Window, 3 | types: Type[], 4 | handler: EventListener 5 | ) => { 6 | for (const type of types) { 7 | element.addEventListener(type, handler) 8 | } 9 | } 10 | 11 | export const removeListeners = ( 12 | element: HTMLElement, 13 | types: Type[], 14 | handler: EventListener 15 | ) => { 16 | for (const type of types) { 17 | element.removeEventListener(type, handler) 18 | } 19 | } 20 | 21 | export const findParent = (start: HTMLElement, match: (element: HTMLElement) => boolean, maxDepth = 4) => { 22 | let count = 0 23 | let end: HTMLElement = start 24 | while (end) { 25 | if (end == (window as any) || end == document.body) return 26 | if (match(end)) return end 27 | if (end.parentElement) end = end.parentElement 28 | else return 29 | if (++count > maxDepth) return 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /inject/utils/proxy.ts: -------------------------------------------------------------------------------- 1 | type AnyFunction = (...args: any[]) => any 2 | 3 | export const proxy = ( 4 | source: T, 5 | method: K, 6 | callback: (this: T, returned: ReturnType) => void 7 | ) => { 8 | const original = source[method] as AnyFunction 9 | source[method] = function (this: T, ...args: Parameters) { 10 | const returned = original.apply(this, args) 11 | callback.call(this, returned) 12 | return returned 13 | } as T[K] 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-code", 3 | "displayName": "Material Code", 4 | "description": "Dynamic theme for Visual Studio Code.", 5 | "version": "3.0.2", 6 | "publisher": "rakib13332", 7 | "author": "https://github.com/rakibdev", 8 | "icon": "icon.png", 9 | "galleryBanner.color": "#ffffff", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/rakibdev/material-code" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/rakibdev/material-code/issues" 16 | }, 17 | "categories": [ 18 | "Themes" 19 | ], 20 | "keywords": [ 21 | "material", 22 | "theme" 23 | ], 24 | "main": "./build/extension.js", 25 | "engines": { 26 | "vscode": "^1.75.0" 27 | }, 28 | "activationEvents": [ 29 | "onStartupFinished" 30 | ], 31 | "contributes": { 32 | "themes": [ 33 | { 34 | "label": "Material Code", 35 | "uiTheme": "vs-dark", 36 | "path": "./build/dark.json", 37 | "_watch": true 38 | }, 39 | { 40 | "label": "Material Code Light", 41 | "uiTheme": "vs", 42 | "path": "./build/light.json", 43 | "_watch": true 44 | } 45 | ], 46 | "configuration": { 47 | "title": "Material Code", 48 | "properties": { 49 | "material-code.primaryColor": { 50 | "type": "string", 51 | "default": "#00adff", 52 | "pattern": "^#([A-Fa-f0-9]{6})$", 53 | "patternErrorMessage": "Invalid color." 54 | }, 55 | "material-code.inject": { 56 | "type": "array", 57 | "items": { 58 | "type": "string" 59 | }, 60 | "default": [ 61 | "${extensionDir}/build/inject.css", 62 | "${extensionDir}/build/inject.js" 63 | ], 64 | "markdownDescription": "Select **Command Palette > Material Code: Apply styles** after editing.\n\nExamples: \n* `~/Downloads/custom.css`\n* `C:/Users/rakib/Downloads/custom.css`\n* `https://raw.githubusercontent.com/.../custom.css`\n* `body { ... }`\n* ``\n* ``\n* `
...
`" 65 | }, 66 | "material-code.syntaxTheme": { 67 | "type": "string", 68 | "markdownDescription": "Use **Command Palette > Material Code: Select Syntax Theme**" 69 | } 70 | } 71 | }, 72 | "commands": [ 73 | { 74 | "command": "material-code.applyStyles", 75 | "title": "Material Code: Apply styles" 76 | }, 77 | { 78 | "command": "material-code.removeStyles", 79 | "title": "Material Code: Remove styles" 80 | }, 81 | { 82 | "command": "material-code.selectSyntaxTheme", 83 | "title": "Material Code: Select Syntax Theme" 84 | } 85 | ] 86 | }, 87 | "scripts": { 88 | "dev": "export DEV=true && bun ./scripts/dev.ts", 89 | "build": "bun ./scripts/dev.ts && yes | vsce package --no-dependencies", 90 | "build:npm": "bun ./scripts/npm/build.ts", 91 | "release": "bun ./scripts/release.ts && xdg-open https://marketplace.visualstudio.com/manage" 92 | }, 93 | "dependencies": { 94 | "@vscode/sudo-prompt": "^9.3.1", 95 | "material-colors": "https://github.com/rakibdev/material-colors/releases/download/1.0.0/npm.tgz" 96 | }, 97 | "devDependencies": { 98 | "@types/bun": "^1.2.15", 99 | "@types/vscode": "1.75.0", 100 | "@vscode/vsce": "^3.5.0", 101 | "esbuild": "^0.24.2", 102 | "typescript": "^5.8.3" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Material Code Editor](https://raw.githubusercontent.com/rakibdev/material-code/main/screenshots/editor.png) 2 | ![Material Code Settings](https://raw.githubusercontent.com/rakibdev/material-code/main/screenshots/settings.png) 3 | ![Material Code Settings](https://raw.githubusercontent.com/rakibdev/material-code/main/screenshots/light-theme.png) 4 | 5 | ## Features 6 | 7 | - Material You (Dark & Light). 8 | - Rounded corners. 9 | - Click ripple effect. 10 | - Modular syntax theme. 11 | - Inject custom code (CSS, JavaScript, HTML) via inline text, local file path, or URL. 12 | - Follow system theme e.g. [Mutagen](https://github.com/InioX/matugen), [Pywal](https://github.com/dylanaraps/pywal). 13 | 14 | Let me know your suggestions, issues on [Github](https://github.com/rakibdev/material-code/issues) 15 | 16 | ## Usage 17 | 18 | Material You theming works straight away. 19 | 20 | Other features requires running **Material Code: Apply styles** from command palette, which injects code into vscode installation file **workbench.html**. Therefore extension may ask for administrative privileges if needed. 21 | 22 | After applying vscode will warn "Installation corrupted". This can be safely ignored. Click notification gear icon > **Don't show again**. 23 | 24 | And to revert run **Material Code: Remove styles**. 25 | 26 | ### Follow system theme 27 | 28 | Material Code's installation directory (e.g. `~/.vscode-insiders/extensions/rakib13332.material-code.../build/theme.js`). The **theme.js** file exports functions to control theme programmatically outside VS Code. So call these whenever your system theme changes. I use shell scripts to apply system-wide GTK and VS Code theme at once, see [vscode.js](https://github.com/rakibdev/dotfiles/blob/main/home/rakib/Downloads/apps-script/theme/vscode.js). Edit according to your system before using. 29 | 30 | ## Help 31 | 32 | ### Revert original files manually 33 | 34 | In rare cases like [this](https://github.com/rakibdev/material-code/issues/2) where "Material Code: Remove styles" not working. Generally updating vscode version will revert itself including the styles but if you need fix urgent: 35 | 36 | - Open **workbench.html** file located in vscode installation folder. 37 | In my case (Linux) it's `/opt/visual-studio-code-insiders/resources/app/out/vs/code/electron-sandbox/workbench/workbench.html` 38 | - Remove all code inside `<--material-code-->` block and save. 39 | - To fix "Installation corrupted" warning, use [Fix VSCode Checksums](https://marketplace.visualstudio.com/items?itemName=lehni.vscode-fix-checksums) extension. 40 | 41 | ### Custom CSS 42 | 43 | **Change VS Code font**
44 | 45 | ```css 46 | .mac, 47 | .windows, 48 | .linux { 49 | font-family: Google Sans; 50 | } 51 | ``` 52 | 53 | **Change rounded corner radius**
54 | 55 | ```css 56 | body { 57 | --radius: 8px; 58 | } 59 | ``` 60 | 61 | ** Change background **
62 | // todo: 63 | 64 | ### Settings you might like 65 | 66 | ```json 67 | "editor.semanticHighlighting.enabled": true, 68 | "window.dialogStyle": "custom", 69 | "window.menuBarVisibility": "hidden" // I'm using command palette instead. 70 | ``` 71 | 72 | My [settings.json](https://github.com/rakibdev/dotfiles/blob/main/home/rakib/.config/Code%20-%20Insiders/User/settings.json) 73 | -------------------------------------------------------------------------------- /screenshots/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibdev/material-code/b59ad725df57ae2d9c5610ee21d0a7be6b420348/screenshots/editor.png -------------------------------------------------------------------------------- /screenshots/light-theme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibdev/material-code/b59ad725df57ae2d9c5610ee21d0a7be6b420348/screenshots/light-theme.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibdev/material-code/b59ad725df57ae2d9c5610ee21d0a7be6b420348/screenshots/settings.png -------------------------------------------------------------------------------- /scripts/config.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | green: '\x1b[32m', 3 | red: '\x1b[31m', 4 | reset: '\x1b[0m' 5 | } 6 | -------------------------------------------------------------------------------- /scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import { build, context, type BuildOptions, type Plugin } from 'esbuild' 2 | import * as materialColors from 'material-colors' 3 | import { colors } from './config' 4 | 5 | const options: BuildOptions = { 6 | outdir: 'build', 7 | minify: !process.env.DEV, 8 | bundle: true 9 | } 10 | 11 | const importUncached = (module: string) => { 12 | const path = Bun.resolveSync(module, import.meta.dir) 13 | delete require.cache[path] 14 | return import(path) 15 | } 16 | 17 | const themeGenerator: Plugin = { 18 | name: 'themeGenerator', 19 | setup(build) { 20 | const generate = async () => { 21 | const { createEditorTheme, createSemanticColors, themeOptions } = (await importUncached( 22 | `../src/theme/create` 23 | )) as typeof import('../src/theme/create') 24 | const darkColors = materialColors.flatten(materialColors.generate(themeOptions)) 25 | const lightColors = materialColors.flatten(materialColors.generate({ ...themeOptions, darkMode: false })) 26 | 27 | const darkTheme = createEditorTheme(createSemanticColors(darkColors)) 28 | const lightTheme = createEditorTheme(createSemanticColors(lightColors)) 29 | 30 | await Bun.write(`${options.outdir}/dark.json`, JSON.stringify(darkTheme)) 31 | await Bun.write(`${options.outdir}/light.json`, JSON.stringify(lightTheme)) 32 | console.log(colors.green + 'Themes generated.' + colors.reset) 33 | } 34 | let lastModified: number 35 | build.onEnd(async () => { 36 | const file = Bun.file('./src/theme/create.ts') 37 | if (lastModified == file.lastModified) return 38 | lastModified = file.lastModified 39 | generate() 40 | }) 41 | } 42 | } 43 | 44 | const extension: BuildOptions = { 45 | ...options, 46 | entryPoints: ['./src/extension.ts'], 47 | format: 'cjs', 48 | platform: 'node', 49 | plugins: [themeGenerator], 50 | sourcemap: process.env.DEV ? 'inline' : undefined, 51 | // External path must exactly march the import path. 52 | external: ['vscode'] 53 | } 54 | 55 | const inject: BuildOptions = { 56 | ...options, 57 | entryPoints: ['inject/inject.ts'], 58 | format: 'esm' 59 | } 60 | 61 | if (process.env.DEV) { 62 | const extensionBuild = await context(extension) 63 | await extensionBuild.watch() 64 | const injectBuild = await context(inject) 65 | await injectBuild.watch() 66 | } else { 67 | await build(extension) 68 | await build(inject) 69 | } 70 | -------------------------------------------------------------------------------- /scripts/github.ts: -------------------------------------------------------------------------------- 1 | import { $ } from 'bun' 2 | import { colors } from './config' 3 | 4 | export const createRelease = async (tag: string, body: string) => { 5 | await $`git tag ${tag}` 6 | await $`git push origin ${tag}` 7 | 8 | const origin = (await $`git remote get-url origin`.text()).trim().split('/') 9 | const repo = origin.pop()!.replace('.git', '') 10 | const owner = origin.pop() 11 | const authToken = (await $`git config --get user.password`.text()).trim() 12 | const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`, { 13 | method: 'POST', 14 | headers: { 15 | Authorization: `token ${authToken}`, 16 | Accept: 'application/vnd.github.v3+json' 17 | }, 18 | body: JSON.stringify({ 19 | name: tag, 20 | tag_name: tag, 21 | body: body 22 | }) 23 | }) 24 | if (response.ok) console.log(colors.green + `Released v${tag}.` + `\n\n${body}` + colors.reset) 25 | else { 26 | const { message } = await response.json() 27 | console.log(colors.red + `Unable to create release: ${message}` + colors.reset) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scripts/npm/build.ts: -------------------------------------------------------------------------------- 1 | import { $, type BuildConfig } from 'bun' 2 | import packageJson from '../../package.json' 3 | 4 | const options: BuildConfig = { 5 | outdir: 'build/npm', 6 | minify: true, 7 | format: 'esm', 8 | entrypoints: ['./src/theme/create.ts'] 9 | } 10 | await Bun.build(options) 11 | 12 | const { name, description, version, author } = packageJson 13 | Bun.write( 14 | `${options.outdir}/package.json`, 15 | JSON.stringify({ 16 | name, 17 | description, 18 | version, 19 | author, 20 | files: ['**/*.js', '**/*.d.ts'], 21 | exports: { 22 | // Use `./theme` not `theme`. Otherwise VS Code don't resolve import types. 23 | './theme': { 24 | import: './theme.js', 25 | types: './theme.d.ts' 26 | } 27 | }, 28 | peerDependencies: { 29 | 'material-colors': packageJson.dependencies['material-colors'] 30 | } 31 | }) 32 | ) 33 | 34 | await $`tsc --project ./scripts/npm/tsconfig.json` 35 | 36 | process.chdir(options.outdir!) 37 | await $`bun pm pack` 38 | -------------------------------------------------------------------------------- /scripts/npm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "declarationDir": "../../build/npm", 6 | "declaration": true 7 | }, 8 | "include": ["../../src/theme/create.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { $ } from 'bun' 2 | import { colors } from './config' 3 | import { createRelease } from './github' 4 | 5 | const tags = (await $`git tag -l --sort=-v:refname`.text()).split('\n').flatMap(tag => (tag ? tag.trim() : [])) 6 | const lastTag = tags[0] 7 | const lastCommit = lastTag ? `${(await $`git rev-list -1 ${lastTag}`.text()).trim()}..HEAD` : 'HEAD' 8 | const newCommits = (await $`git log --format=%s__%h ${lastCommit}`.text()).split('\n').flatMap(commit => { 9 | if (!commit) return [] 10 | const [message, short_hash] = commit.split('__') 11 | return { message, short_hash } 12 | }) 13 | if (!newCommits.length) { 14 | console.log(colors.red + `No new commits since tag ${lastTag}` + colors.reset) 15 | process.exit(1) 16 | } 17 | 18 | const getCommitsBulletedMarkdown = (prefixes: string[], prefix_emojis: string[]) => { 19 | const relevent = newCommits.filter(commit => prefixes.some(prefix => commit.message.startsWith(prefix))) 20 | relevent.sort((commit, commit2) => { 21 | const index = prefixes.findIndex(prefix => commit.message.startsWith(prefix)) 22 | const index2 = prefixes.findIndex(prefix => commit2.message.startsWith(prefix)) 23 | return index - index2 24 | }) 25 | return relevent 26 | .map(commit => { 27 | const index = prefixes.findIndex(prefix => commit.message.startsWith(prefix)) 28 | return `${commit.message.replace(`${prefixes[index]}:`, prefix_emojis[index])} ${commit.short_hash}` 29 | }) 30 | .join('\n') 31 | } 32 | 33 | const features = getCommitsBulletedMarkdown(['feat'], ['✨']) 34 | const improvements = getCommitsBulletedMarkdown( 35 | ['refactor!', 'refactor', 'perf', 'chore', 'docs', 'style'], 36 | ['♻️!', '♻️', '⚡', '🔧', '📄', '🌈'] 37 | ) 38 | const fixes = getCommitsBulletedMarkdown(['fix'], ['🐞']) 39 | 40 | let releaseNotes: string[] | string = [] 41 | if (features.length) releaseNotes.push(`## New features\n${features}`) 42 | if (improvements.length) releaseNotes.push(`## Improvements\n${improvements}`) 43 | if (fixes.length) releaseNotes.push(`## Fixes\n${fixes}`) 44 | 45 | const { name: extension_name, version, publisher } = await Bun.file('package.json').json() 46 | releaseNotes.push( 47 | `[Install extension from marketplace](https://marketplace.visualstudio.com/items?itemName=${publisher}.${extension_name})` 48 | ) 49 | releaseNotes = releaseNotes.join('\n\n') 50 | 51 | await createRelease(version, releaseNotes) 52 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode' 2 | import { AppData } from './utils/appdata' 3 | import { buildDir, packageJson } from './utils/config' 4 | import * as inject from './inject' 5 | import { openSyntaxThemePicker } from './theme/syntax' 6 | import { saveTheme } from './theme/utils' 7 | 8 | // todo: Improve markdown highlighting. 9 | // const isWeb = vscode.env.appHost != 'desktop' 10 | 11 | const updateThemes = async () => { 12 | await saveTheme(vscode.Uri.joinPath(buildDir, 'dark.json'), true) 13 | await saveTheme(vscode.Uri.joinPath(buildDir, 'light.json'), false) 14 | } 15 | 16 | export const activate = async (context: vscode.ExtensionContext) => { 17 | const appData = new AppData(context) 18 | await appData.initialize() 19 | 20 | inject.init(appData.dir) 21 | 22 | const version = appData.get().version 23 | if (version != packageJson.version) { 24 | appData.set('version', packageJson.version) 25 | updateThemes() 26 | 27 | if (!version) { 28 | vscode.window 29 | .showInformationMessage(`${packageJson.displayName} installed!`, 'Open README', 'Cancel') 30 | .then(action => { 31 | if (action == 'Open README') vscode.env.openExternal(vscode.Uri.parse(packageJson.repository.url)) 32 | }) 33 | } 34 | } 35 | 36 | vscode.workspace.onDidChangeConfiguration(event => { 37 | if ( 38 | event.affectsConfiguration('material-code.primaryColor') || 39 | event.affectsConfiguration('material-code.syntaxTheme') 40 | ) { 41 | updateThemes() 42 | } 43 | }) 44 | 45 | const commands = [ 46 | vscode.commands.registerCommand('material-code.applyStyles', inject.applyStyles), 47 | vscode.commands.registerCommand('material-code.removeStyles', inject.removeStyles), 48 | vscode.commands.registerCommand('material-code.selectSyntaxTheme', openSyntaxThemePicker) 49 | ] 50 | commands.forEach(command => context.subscriptions.push(command)) 51 | } 52 | -------------------------------------------------------------------------------- /src/inject.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode' 2 | import { extensionUri, packageJson, settings } from './utils/config' 3 | import { errorNotification } from './utils/extension' 4 | import { normalizeInjectPath, readFile, writeFile } from './utils/file' 5 | 6 | let appDataDir: vscode.Uri 7 | let workbenchFile: vscode.Uri 8 | 9 | export const init = (dir: vscode.Uri) => { 10 | appDataDir = dir 11 | 12 | const vscodeRoot = vscode.Uri.file(vscode.env.appRoot) 13 | ;[ 14 | 'out/vs/code/electron-sandbox/workbench/workbench.esm.html', // Above 1.94.0 15 | 'out/vs/code/electron-sandbox/workbench/workbench.html' 16 | ].forEach(async file => { 17 | try { 18 | const uri = vscode.Uri.joinPath(vscodeRoot, file) 19 | await vscode.workspace.fs.stat(uri) 20 | workbenchFile = uri 21 | } catch {} 22 | }) 23 | } 24 | 25 | export const updateWorkbenchFile = async (workbenchHtml: string) => { 26 | const onSuccess = () => { 27 | vscode.commands.executeCommand('workbench.action.reloadWindow') 28 | } 29 | 30 | try { 31 | await writeFile(workbenchFile, workbenchHtml) 32 | onSuccess() 33 | } catch { 34 | const tempFile = vscode.Uri.joinPath(appDataDir, 'workbench.html') 35 | await writeFile(tempFile, workbenchHtml) 36 | const moveCommand = process.platform.includes('win') ? 'move' : 'mv' 37 | const command = `${moveCommand} "${tempFile.fsPath}" "${workbenchFile.fsPath}"` 38 | 39 | const sudo = await import('@vscode/sudo-prompt') 40 | sudo.exec(command, { name: packageJson.displayName }, async (error: any) => { 41 | if (error) { 42 | await vscode.workspace.fs.delete(tempFile) 43 | errorNotification( 44 | /EPERM|EACCES|ENOENT/.test(error.code) 45 | ? 'Permission denied. Run editor as admin and try again.' 46 | : error.message 47 | ) 48 | } else onSuccess() 49 | }) 50 | } 51 | } 52 | 53 | export const clearInjection = (workbenchHtml: string) => 54 | workbenchHtml.replace(/\n*?.*?\n*?/s, '\n\n') 55 | 56 | const htmlWrapCode = (code: string) => { 57 | if (/^<[^>]+>/.test(code)) return code 58 | const isCss = /\{[^}]+:[^}]+\}/.test(code) 59 | return `<${isCss ? 'style' : 'script'}>${code}` 60 | } 61 | 62 | export const applyStyles = async () => { 63 | const inject = settings().get('inject', []) 64 | let code = '' 65 | 66 | for (const line of inject) { 67 | const isFile = line.endsWith('.css') || line.endsWith('.js') 68 | if (isFile) { 69 | try { 70 | if (line.startsWith('https://')) { 71 | vscode.window.showInformationMessage('Fetching ' + line) 72 | const response = await fetch(line) 73 | if (response.ok) { 74 | code += htmlWrapCode(await response.text()) 75 | } else throw new Error(response.statusText) 76 | } else { 77 | const uri = normalizeInjectPath(line) 78 | await vscode.workspace.fs.stat(uri) 79 | if (uri.fsPath.startsWith(extensionUri.fsPath)) { 80 | const withSchema = uri.with({ scheme: 'vscode-file', authority: 'vscode-app' }) 81 | code += line.endsWith('.css') 82 | ? `\n\n` 83 | : `\n\n` 84 | } else { 85 | // External `src`, `href` is blocked by CORS. 86 | code += htmlWrapCode(await readFile(uri)) 87 | } 88 | } 89 | } catch (error: any) { 90 | errorNotification(`${line}: ${error.message}`) 91 | } 92 | } else code += htmlWrapCode(line) 93 | } 94 | 95 | let html = await readFile(workbenchFile) 96 | html = clearInjection(html) 97 | .replace(//s, '') 98 | .replace(/\n*?<\/html>/, `\n\n${code}\n\n`) 99 | updateWorkbenchFile(html) 100 | } 101 | 102 | export const removeStyles = async () => { 103 | const html = await readFile(workbenchFile) 104 | updateWorkbenchFile(clearInjection(html)) 105 | } 106 | -------------------------------------------------------------------------------- /src/theme/create.ts: -------------------------------------------------------------------------------- 1 | import { type FlatMaterialColors } from 'material-colors' 2 | 3 | export const themeOptions = { 4 | darkMode: true, 5 | colors: { 6 | primary: '#00adff', 7 | 8 | // Syntax colors. 9 | blue: '#0091ff', 10 | sky_blue: '#00adff', 11 | red: '#ff002b', 12 | green: '#00ffac', 13 | pink: '#ff00d9', 14 | yellow: '#ffee00' 15 | }, 16 | tones: [80, 50, 40, 20] as const 17 | } 18 | 19 | type ThemeOptions = typeof themeOptions 20 | 21 | export type SemanticColors = { 22 | background: string 23 | foreground: string 24 | muted: string // Secondary text, placeholders, inactive elements 25 | border: string 26 | 27 | primary: string // Buttons, links, badges, breakpoints, progress bars 28 | primaryForeground: string 29 | 30 | error: string 31 | errorForeground: string 32 | 33 | success: string // Success indicators, green terminal colors 34 | warning: string // Warning indicators, yellow terminal colors 35 | 36 | surface: string // Elevated panels, dropdowns, cards, tabs 37 | surfaceHover: string // Hover states, active selections 38 | 39 | // Syntax highlighting 40 | syntax: { 41 | comment: string // Comments and punctuation 42 | string: string // String literals 43 | keyword: string // Keywords, operators, control flow 44 | variable: string // Variables, tags, HTML elements 45 | attribute: string // HTML attributes, CSS selectors 46 | property: string // Object properties, CSS properties 47 | function: string // Functions, methods, CSS values 48 | constant: string // Numbers, constants, types 49 | 50 | // Bracket pair colors 51 | bracket1: string // First level brackets 52 | bracket2: string // Second level brackets 53 | bracket3: string // Third level brackets 54 | bracket4: string // Fourth level brackets 55 | } 56 | } 57 | 58 | export const createSemanticColors = ( 59 | colors: FlatMaterialColors 60 | ): SemanticColors => { 61 | return { 62 | background: colors.primary_surface, 63 | foreground: colors.neutral_20, 64 | muted: colors.neutral_40, 65 | border: colors.primary_surface_3, 66 | 67 | primary: colors.primary_40, 68 | primaryForeground: colors.primary_surface, 69 | 70 | error: colors.red_40, 71 | errorForeground: colors.red_20, 72 | success: colors.green_40, 73 | warning: colors.yellow_40, 74 | 75 | surface: colors.primary_surface_2, 76 | surfaceHover: colors.primary_surface_3, 77 | 78 | syntax: { 79 | comment: colors.neutral_40, 80 | string: colors.green_40, 81 | keyword: colors.pink_50, 82 | variable: colors.blue_50, 83 | attribute: colors.blue_40, 84 | property: colors.sky_blue_40, 85 | function: colors.red_50, 86 | constant: colors.yellow_40, 87 | 88 | bracket1: colors.pink_50, 89 | bracket2: colors.yellow_40, 90 | bracket3: colors.red_50, 91 | bracket4: colors.green_50 92 | } 93 | } 94 | } 95 | 96 | export const createEditorTheme = (colors: SemanticColors) => { 97 | const transparent = '#ffffff00' 98 | 99 | return { 100 | name: 'Material Code', 101 | colors: { 102 | 'banner.iconForeground': colors.foreground, 103 | 'textLink.foreground': colors.primary, 104 | foreground: colors.foreground, 105 | 'icon.foreground': colors.foreground, 106 | 'textLink.activeForeground': colors.primary, 107 | errorForeground: colors.error, 108 | 'selection.background': colors.primary + '20', 109 | focusBorder: transparent, 110 | 'sash.hoverBorder': colors.primary, 111 | 'widget.shadow': transparent, 112 | 113 | 'button.foreground': colors.primaryForeground, 114 | 'button.background': colors.primary, 115 | 'button.secondaryForeground': colors.muted, 116 | 'button.secondaryBackground': colors.surfaceHover, 117 | 118 | 'badge.background': colors.primary, 119 | 'badge.foreground': colors.primaryForeground, 120 | 'activityBarBadge.background': colors.primary, 121 | 'activityBarBadge.foreground': colors.primaryForeground, 122 | 123 | 'checkbox.background': transparent, 124 | 'checkbox.foreground': colors.primary, 125 | 'checkbox.border': colors.primary, 126 | 127 | 'dropdown.background': colors.surface, 128 | 129 | 'input.background': colors.surface, 130 | 'input.placeholderForeground': colors.muted, 131 | 'inputValidation.errorBackground': colors.error + '40', 132 | 'inputValidation.errorForeground': colors.errorForeground, 133 | 'inputValidation.errorBorder': transparent, 134 | 135 | 'inputOption.activeBackground': colors.primary, 136 | 'inputOption.activeForeground': colors.primaryForeground, 137 | 'inputOption.activeBorder': transparent, 138 | 139 | 'scrollbar.shadow': transparent, 140 | 'scrollbarSlider.background': colors.surfaceHover, 141 | 'scrollbarSlider.hoverBackground': colors.surfaceHover, 142 | 'scrollbarSlider.activeBackground': colors.surfaceHover, 143 | 144 | 'list.hoverBackground': colors.surfaceHover, 145 | 'list.activeSelectionBackground': colors.surfaceHover, 146 | 'list.activeSelectionForeground': colors.foreground, 147 | 'list.inactiveSelectionBackground': colors.surface, 148 | 'list.dropBackground': colors.primary + '50', 149 | 'list.highlightForeground': colors.primary, 150 | 'listFilterWidget.background': colors.surfaceHover, 151 | 152 | 'activityBar.background': colors.background, 153 | 'activityBar.foreground': colors.foreground, 154 | 'activityBar.activeBorder': transparent, 155 | 'activityBar.activeBackground': colors.surface, 156 | 157 | 'sideBar.background': colors.background, 158 | 'sideBar.foreground': colors.foreground, 159 | 'sideBar.border': transparent, 160 | 'sideBarSectionHeader.background': transparent, 161 | 162 | 'editorGroup.dropBackground': colors.primary + '50', 163 | 'editorGroup.border': colors.border, 164 | 'editorGroupHeader.tabsBackground': colors.background, 165 | 166 | 'tab.activeBackground': colors.surface, 167 | 'tab.activeforeground': colors.foreground, 168 | 'tab.hoverBackground': colors.surfaceHover, 169 | 'tab.inactiveBackground': colors.background, 170 | 'tab.inactiveForeground': colors.muted, 171 | 'tab.border': transparent, 172 | 173 | 'panel.background': colors.surface, 174 | 'panel.border': transparent, 175 | 'panel.dropBorder': colors.primary, 176 | 'panelTitle.activeforeground': colors.foreground, 177 | 'panelTitle.activeBorder': colors.foreground, 178 | 'panelTitle.inactiveForeground': colors.muted, 179 | 180 | 'outputView.background': colors.surface, 181 | 182 | 'statusBar.debuggingBackground': colors.background, 183 | 'debugIcon.breakpointForeground': colors.primary, 184 | 'debugIcon.breakpointUnverifiedForeground': colors.primary, 185 | 'editor.stackFrameHighlightBackground': colors.primary + '20', 186 | 'debugIcon.breakpointCurrentStackframeForeground': colors.primary, 187 | 188 | 'debugToolBar.background': colors.surface, 189 | 'debugIcon.restartForeground': colors.success, 190 | 'debugIcon.stepOverForeground': colors.primary, 191 | 'debugIcon.stepIntoForeground': colors.primary, 192 | 'debugIcon.stepOutForeground': colors.primary, 193 | 'debugIcon.continueForeground': colors.primary, 194 | 195 | 'debugTokenExpression.name': colors.syntax.attribute, 196 | 'debugTokenExpression.value': colors.muted, 197 | 'debugTokenExpression.string': colors.success, 198 | 'debugTokenExpression.number': colors.warning, 199 | 'debugTokenExpression.boolean': colors.warning, 200 | 'debugTokenExpression.error': colors.error, 201 | 202 | 'editorGutter.foldingControlForeground': colors.muted, 203 | 204 | 'editorGutter.modifiedBackground': colors.primary, 205 | 'editorGutter.addedBackground': colors.success + '80', 206 | 'editorGutter.deletedBackground': colors.error + '80', 207 | 208 | 'diffEditor.diagonalFill': colors.border + '50', 209 | 'diffEditor.insertedTextBackground': transparent, 210 | 'diffEditor.removedTextBackground': transparent, 211 | 'diffEditor.insertedLineBackground': colors.success + '20', 212 | 'diffEditor.removedLineBackground': colors.error + '20', 213 | 214 | 'editorRuler.foreground': colors.surface, 215 | 'editorWhitespace.foreground': colors.muted, 216 | 217 | 'editorHoverWidget.background': colors.surface, 218 | 'editorHoverWidget.border': transparent, 219 | 220 | 'editorOverviewRuler.border': transparent, 221 | 'editorOverviewRuler.errorForeground': colors.error, 222 | 'editorOverviewRuler.findMatchForeground': colors.primary, 223 | 'editorOverviewRuler.infoForeground': colors.success, 224 | 'editorOverviewRuler.warningForeground': colors.warning, 225 | 226 | 'menubar.selectionBackground': colors.surface, 227 | 'menu.background': colors.surface, 228 | 'menu.foreground': colors.foreground, 229 | 'menu.selectionBackground': colors.surfaceHover, 230 | 'menu.separatorBackground': transparent, 231 | 232 | 'pickerGroup.border': transparent, 233 | 'pickerGroup.foreground': colors.primary, 234 | 235 | 'keybindingLabel.background': colors.surfaceHover, 236 | 'keybindingLabel.foreground': colors.foreground, 237 | 'keybindingLabel.border': transparent, 238 | 'keybindingLabel.bottomBorder': transparent, 239 | 240 | 'titleBar.activeBackground': colors.background, 241 | 'titleBar.activeforeground': colors.foreground, 242 | 'titleBar.inactiveBackground': colors.background, 243 | 'titleBar.inactiveforeground': colors.foreground, 244 | 245 | 'statusBar.background': colors.background, 246 | 'statusBar.foreground': colors.foreground, 247 | 'statusBar.border': transparent, 248 | 'statusBar.focusBorder': transparent, 249 | 'statusBar.noFolderBackground': colors.background, 250 | 'statusBarItem.hoverBackground': colors.surface, 251 | 'statusBarItem.activeBackground': colors.surface, 252 | 'statusBarItem.remoteBackground': colors.background, 253 | 'statusBarItem.remoteforeground': colors.foreground, 254 | 255 | 'editor.background': colors.background, 256 | 'editor.foreground': colors.foreground, 257 | 'editor.lineHighlightBackground': colors.primary + '10', 258 | 'editor.selectionBackground': colors.surfaceHover, 259 | 'editor.wordHighlightBackground': colors.primary + '30', 260 | 'editor.findMatchBackground': colors.primary + '30', 261 | 'editor.findMatchHighlightBackground': colors.primary + '30', 262 | 'editorCursor.foreground': colors.foreground, 263 | 'editorBracketMatch.background': colors.primary + '40', 264 | 'editorBracketMatch.border': transparent, 265 | 266 | 'editorBracketHighlight.foreground1': colors.syntax.bracket1, 267 | 'editorBracketHighlight.foreground2': colors.syntax.bracket2, 268 | 'editorBracketHighlight.foreground3': colors.syntax.bracket3, 269 | 'editorBracketHighlight.foreground4': colors.syntax.bracket4, 270 | 'editorBracketPairGuide.activeBackground1': colors.syntax.bracket1 + '20', 271 | 'editorBracketPairGuide.activeBackground2': colors.syntax.bracket2 + '20', 272 | 'editorBracketPairGuide.activeBackground3': colors.syntax.bracket3 + '20', 273 | 'editorBracketPairGuide.activeBackground4': colors.syntax.bracket4 + '20', 274 | 'editorBracketPairGuide.background1': colors.syntax.bracket1 + '10', 275 | 'editorBracketPairGuide.background2': colors.syntax.bracket2 + '10', 276 | 'editorBracketPairGuide.background3': colors.syntax.bracket3 + '10', 277 | 'editorBracketPairGuide.background4': colors.syntax.bracket4 + '10', 278 | 279 | 'editorIndentGuide.activeBackground': colors.surface, 280 | 'editorIndentGuide.background': transparent, 281 | 'editorInfo.foreground': colors.foreground, 282 | 'editorError.foreground': colors.error, 283 | 'editorWarning.foreground': colors.warning, 284 | 'editorLink.activeforeground': colors.foreground, 285 | 286 | 'editorLineNumber.foreground': colors.border, 287 | 'editorLineNumber.activeForeground': colors.muted, 288 | 289 | 'editorWidget.background': colors.surface, 290 | 'editorWidget.border': transparent, 291 | 'editorSuggestWidget.selectedBackground': colors.surfaceHover, 292 | 293 | 'peekView.border': transparent, 294 | 295 | 'peekViewTitle.background': colors.surface, 296 | 'peekViewTitleLabel.foreground': colors.primary, 297 | 'peekViewTitleDescription.foreground': colors.foreground, 298 | 299 | 'peekViewResult.background': colors.surface, 300 | 'peekViewResult.matchHighlightBackground': colors.primary + '30', 301 | 'peekViewResult.selectionBackground': colors.surfaceHover, 302 | 'peekViewResult.selectionForeground': colors.foreground, 303 | 'peekViewResult.lineforeground': colors.foreground, 304 | 305 | 'peekViewEditor.background': colors.surface, 306 | 'peekViewEditor.matchHighlightBackground': colors.primary + '30', 307 | 308 | 'notificationCenterHeader.background': colors.surfaceHover, 309 | 'notifications.background': colors.surfaceHover, 310 | 'notificationsInfoIcon.foreground': colors.primary, 311 | 312 | 'problemsInfoIcon.foreground': colors.primary, 313 | 314 | 'settings.headerForeground': colors.primary, 315 | 'settings.focusedRowBackground': transparent, 316 | 'settings.focusedRowBorder': transparent, 317 | 'settings.modifiedItemIndicator': colors.primary, 318 | 319 | 'progressBar.background': colors.primary, 320 | 'tree.indentGuidesStroke': colors.surface, 321 | 322 | 'terminal.foreground': colors.foreground, 323 | 'terminal.ansiBrightBlue': colors.syntax.variable, 324 | 'terminal.ansiBrightGreen': colors.success, 325 | 'terminal.ansiBrightRed': colors.syntax.bracket3, 326 | 'terminal.ansiBrightYellow': colors.warning, 327 | 'terminal.ansiBrightCyan': colors.syntax.property, 328 | 329 | 'terminal.ansiBrightMagenta': colors.syntax.bracket1, 330 | 331 | 'breadcrumb.foreground': colors.muted, 332 | 'breadcrumb.focusforeground': colors.foreground, 333 | 'breadcrumb.activeSelectionForeground': colors.foreground, 334 | 'breadcrumbPicker.background': colors.surface, 335 | 336 | 'editorStickyScroll.background': colors.background + '80', 337 | 'editorStickyScrollHover.background': colors.surface 338 | }, 339 | tokenColors: [ 340 | { 341 | scope: [ 342 | '', 343 | 'punctuation', 344 | 'keyword', 345 | 'keyword.other.unit', 346 | 'constant.other', 347 | 'comment', 348 | 'punctuation.definition.comment' 349 | ], 350 | settings: { 351 | foreground: colors.syntax.comment 352 | } 353 | }, 354 | { 355 | scope: ['string', 'punctuation.definition.string'], 356 | settings: { 357 | foreground: colors.syntax.string 358 | } 359 | }, 360 | { 361 | scope: [ 362 | 'storage.type', // const 363 | 'storage.modifier', 364 | 'keyword.operator', // =, ?, :, + 365 | 'keyword.control', // new, import, from 366 | 'punctuation.definition.template-expression' // ${} 367 | ], 368 | settings: { 369 | foreground: colors.syntax.keyword 370 | } 371 | }, 372 | { 373 | scope: [ 374 | 'variable', 375 | 'meta.objectliteral', 376 | 'entity.name.tag', // div 377 | 'punctuation.definition.tag', // tag bracket <> 378 | 'invalid.illegal.character-not-allowed-here.html' // closing bracket / 379 | ], 380 | settings: { 381 | foreground: colors.syntax.variable 382 | } 383 | }, 384 | { 385 | scope: [ 386 | 'entity.other.attribute-name', 387 | 'punctuation.separator.key-value.html', // class= 388 | 'punctuation.attribute-shorthand.bind.html.vue', // :class 389 | 'punctuation.attribute-shorthand.event.html.vue' // @click 390 | ], 391 | settings: { 392 | foreground: colors.syntax.attribute 393 | } 394 | }, 395 | { 396 | scope: ['variable.other', 'punctuation.definition'], 397 | settings: { 398 | foreground: colors.syntax.property 399 | } 400 | }, 401 | { 402 | scope: [ 403 | 'support.variable', 404 | 'support.function', 405 | 'entity.name.function', // function() 406 | 'keyword.other', 407 | 'support.type', // css property 408 | 'punctuation.separator.key-value.css' // color: blue 409 | ], 410 | settings: { 411 | foreground: colors.syntax.function 412 | } 413 | }, 414 | { 415 | scope: [ 416 | 'constant', // numbers, \n 417 | 'support.constant', // css value 418 | 'keyword.other.unit', // 5px 419 | 'punctuation.terminator.rule.css', // px; 420 | 'entity.name.type' // class name 421 | ], 422 | settings: { 423 | foreground: colors.syntax.constant 424 | } 425 | } 426 | ], 427 | semanticTokenColors: {} 428 | } 429 | } 430 | 431 | export type Theme = ReturnType 432 | -------------------------------------------------------------------------------- /src/theme/syntax.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode' 2 | import { packageJson, settings } from '../utils/config' 3 | import { getInstalledThemes, readTheme } from './utils' 4 | import { type Theme } from './create' 5 | 6 | export const openSyntaxThemePicker = async () => { 7 | const themes = getInstalledThemes() 8 | const current = settings().get('syntaxTheme') || packageJson.displayName 9 | 10 | const icon = `$(symbol-color)` 11 | const quickPick = vscode.window.createQuickPick() 12 | quickPick.items = [ 13 | ...Object.keys(themes) 14 | .sort((item, item2) => { 15 | if (item == current) return -1 16 | if (item2 == current) return 1 17 | return item.localeCompare(item2) 18 | }) 19 | .map(name => ({ 20 | label: `${icon} ${name}`, 21 | description: name == current ? '(current)' : undefined 22 | })) 23 | ] 24 | quickPick.matchOnDescription = true 25 | 26 | quickPick.onDidChangeSelection(async ([item]) => { 27 | if (!item) return 28 | const selected = item.label.replace(icon, '').trim() 29 | const isDefault = selected.startsWith(packageJson.displayName) 30 | await settings().update('syntaxTheme', isDefault ? undefined : selected, vscode.ConfigurationTarget.Global) 31 | quickPick.hide() 32 | }) 33 | 34 | quickPick.onDidHide(() => quickPick.dispose()) 35 | quickPick.show() 36 | } 37 | 38 | export const mergeSyntaxTheme = async (target: Theme, source: Theme) => { 39 | target.tokenColors = source.tokenColors 40 | target.semanticTokenColors = source.semanticTokenColors 41 | for (const key of Object.keys(target.colors) as Array) { 42 | if (key.startsWith('editorBracket')) target.colors[key] = source.colors?.[key] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/theme/utils.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path' 2 | import * as vscode from 'vscode' 3 | import { createEditorTheme, themeOptions, type Theme } from './create' 4 | import { settings } from '../utils/config' 5 | import { errorNotification } from '../utils/extension' 6 | import { readFile, writeFile } from '../utils/file' 7 | import { deepMerge } from '../utils/object' 8 | import { mergeSyntaxTheme } from './syntax' 9 | import * as materialColors from 'material-colors' 10 | import { createSemanticColors } from './create' 11 | 12 | export const getInstalledThemes = () => { 13 | const result: Record = {} 14 | vscode.extensions.all.forEach(extension => { 15 | const themes = extension.packageJSON.contributes?.themes 16 | if (!themes) return 17 | for (const theme of themes) { 18 | result[theme.label] = vscode.Uri.joinPath(vscode.Uri.file(extension.extensionPath), theme.path) 19 | } 20 | }) 21 | return result 22 | } 23 | 24 | export const readTheme = async (uri: vscode.Uri, parent = {}) => { 25 | let content: Record = {} 26 | 27 | try { 28 | let jsonString = await readFile(uri) 29 | 30 | jsonString = jsonString 31 | // Remove comments, not URLs in strings (e.g. "$schema": "vscode://schemas/color-theme") 32 | .replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => (g ? '' : m)) 33 | // Remove trailing commas 34 | .replace(/,(\s*[}\]])/g, '$1') 35 | 36 | content = JSON.parse(jsonString) 37 | } catch (error: any) { 38 | errorNotification(error.message) 39 | return parent 40 | } 41 | if (content.include) { 42 | const includeUri = vscode.Uri.joinPath(vscode.Uri.file(dirname(uri.path)), content.include) 43 | content = await readTheme(includeUri, content) 44 | } 45 | 46 | content = deepMerge(content, parent) 47 | 48 | return content as Theme 49 | } 50 | 51 | export const saveTheme = async (uri: vscode.Uri, darkMode: boolean) => { 52 | const options = deepMerge(themeOptions, { darkMode, colors: { primary: settings().get('primaryColor') } }) 53 | const flatColors = materialColors.flatten(materialColors.generate(options)) 54 | const theme = createEditorTheme(createSemanticColors(flatColors)) 55 | 56 | const syntaxThemeName = settings().get('syntaxTheme') 57 | if (syntaxThemeName) { 58 | const themes = getInstalledThemes() 59 | const syntaxThemeUri = themes[syntaxThemeName] 60 | if (syntaxThemeUri) await mergeSyntaxTheme(theme, (await readTheme(syntaxThemeUri)) as Theme) 61 | else return errorNotification(`Syntax theme "${syntaxThemeName}" not found.`) 62 | } 63 | 64 | await writeFile(uri, JSON.stringify(theme)) 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/appdata.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode' 2 | import { errorNotification } from './extension' 3 | import { readFile, writeFile } from './file' 4 | 5 | export class AppData { 6 | private content: Record = {} 7 | dir: vscode.Uri 8 | file: vscode.Uri 9 | 10 | constructor(context: vscode.ExtensionContext) { 11 | this.dir = context.globalStorageUri 12 | this.file = vscode.Uri.joinPath(this.dir, 'storage.json') 13 | } 14 | 15 | async initialize() { 16 | try { 17 | await vscode.workspace.fs.stat(this.file) 18 | this.content = JSON.parse(await readFile(this.file)) 19 | } catch (error: any) { 20 | if (error.code == 'FileNotFound') await vscode.workspace.fs.createDirectory(this.dir) 21 | else errorNotification(error.message) 22 | } 23 | } 24 | 25 | get(key?: string) { 26 | return key ? this.content[key] : this.content 27 | } 28 | 29 | set(key: string, value: any) { 30 | this.content[key] = value 31 | return writeFile(this.file, JSON.stringify(this.content)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode' 2 | 3 | const extension = vscode.extensions.getExtension('rakib13332.material-code') 4 | export const extensionUri = extension!.extensionUri 5 | export const buildDir = vscode.Uri.joinPath(extensionUri, 'build') 6 | export const packageJson = extension!.packageJSON 7 | 8 | // Using `settings().get()`, not `settings.get()` to avoid cache. 9 | export const settings = () => vscode.workspace.getConfiguration('material-code') 10 | -------------------------------------------------------------------------------- /src/utils/extension.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode' 2 | import { packageJson } from './config' 3 | 4 | export const errorNotification = (message: string) => { 5 | const button = 'Report on GitHub' 6 | vscode.window.showErrorMessage(message, button).then(async action => { 7 | if (action == button) { 8 | const body = `**OS:** ${process.platform}\n**Visual Studio Code:** ${vscode.version}\n**Error:** \`${message}\`` 9 | vscode.env.openExternal( 10 | vscode.Uri.parse(packageJson.repository.url + `/issues/new?body=${encodeURIComponent(body)}`) 11 | ) 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/file.ts: -------------------------------------------------------------------------------- 1 | import vscode from 'vscode' 2 | import { extensionUri } from './config' 3 | 4 | const arrayFromString = (value: string) => { 5 | return new TextEncoder().encode(value) 6 | } 7 | 8 | const stringFromArray = (value: Uint8Array) => { 9 | return new TextDecoder().decode(value) 10 | } 11 | 12 | export const writeFile = (uri: vscode.Uri, content: string) => { 13 | return vscode.workspace.fs.writeFile(uri, arrayFromString(content)) 14 | } 15 | 16 | export const readFile = async (uri: vscode.Uri) => { 17 | return stringFromArray(await vscode.workspace.fs.readFile(uri)) 18 | } 19 | 20 | export const normalizeInjectPath = (path: string) => { 21 | path = path.replaceAll('${extensionDir}', extensionUri.fsPath) 22 | 23 | let uri: vscode.Uri = vscode.Uri.file(path) 24 | 25 | if (path[0] == '~') { 26 | const home = process.env.HOME || process.env.USERPROFILE 27 | uri = vscode.Uri.joinPath(vscode.Uri.file(home), path.substring(1)) 28 | } 29 | 30 | return uri 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = { 2 | [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] 3 | } 4 | 5 | export const deepMerge = (target: T, source: DeepPartial) => { 6 | const result = { ...target } 7 | for (const key in source) { 8 | if (source[key] == null || source[key] == undefined) continue 9 | if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { 10 | result[key] = deepMerge(target[key], source[key]) 11 | } else { 12 | result[key] = source[key] as T[typeof key] 13 | } 14 | } 15 | return result 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "verbatimModuleSyntax": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------