Skip to content

Commit 4bd8735

Browse files
authored
feat: support patch extension (#57)
1 parent 4270a98 commit 4bd8735

File tree

5 files changed

+176
-23
lines changed

5 files changed

+176
-23
lines changed

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
</p>
88
</p>
99

10-
VSCode extension that custom ui css style in both editor and webview
10+
Custom UI Style is a VSCode extension that modify CSS and JS code in editor and webview, unify global font family, setup background image and Electron BrowserWindow options, add your custom CSS or JS code, or patching file in other VSCode extensions
1111

12-
- Works with VSCode 1.101! (Tested on Windows and MacOS)
12+
- Works with VSCode 1.103! (Tested on Windows and MacOS)
1313

1414
> [!warning]
1515
>
@@ -28,6 +28,7 @@ Untested on Linux and VSCode forks (like Cursor, WindSurf, etc.), and I currentl
2828
- [From V0.4.0] Support total restart
2929
- [From V0.4.0] Suppress corrupt message
3030
- [From V0.4.2] Load external CSS or JS file
31+
- [From V0.6.0] Patch files in extension
3132

3233
## Usage
3334

@@ -181,6 +182,28 @@ To disable all external resources, setup:
181182
}
182183
```
183184

185+
### Patch Extension
186+
187+
Find and replace target string or `Regexp` in extension's file
188+
189+
```jsonc
190+
{
191+
"custom-ui-style.extensions": {
192+
// extension id
193+
"github.copilot-chat": [
194+
{
195+
// target file path
196+
"filePath": "dist/extension.js",
197+
// find string (support JavaScript like regexp)
198+
"find": "https://generativelanguage.googleapis.com/v1beta/openai",
199+
// replace string
200+
"replace": "<path/to/url>"
201+
}
202+
]
203+
},
204+
}
205+
```
206+
184207
## FAQ
185208

186209
### What is modified
@@ -245,6 +268,7 @@ According to [#34](https://github.com/subframe7536/vscode-custom-ui-style/issues
245268
| `custom-ui-style.external.loadStrategy` | Load strategy for external CSS or JS resources | `string` | `"refetch"` |
246269
| `custom-ui-style.external.imports` | External CSS or JS resources, support variable: [${userHome}, ${env:your_env_name:optional_fallback_value}], support protocol: 'https://', 'file://' | `array` | `` |
247270
| `custom-ui-style.stylesheet` | Custom css for editor, support nest selectors | `object` | `{}` |
271+
| `custom-ui-style.extensions` | Config to patch extension code, key is extension id, value is config | `object` | `{}` |
248272
| `custom-ui-style.webview.enable` | Enable style patch in webview | `boolean` | `true` |
249273
| `custom-ui-style.webview.removeCSP` | Remove Content-Security-Policy restrict in webview | `boolean` | `true` |
250274
| `custom-ui-style.webview.monospaceSelector` | Custom monospace selector in webview | `array` | `` |

package.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"publisher": "subframe7536",
33
"name": "custom-ui-style",
44
"displayName": "Custom UI Style",
5-
"version": "0.5.9",
5+
"version": "0.5.10",
66
"private": true,
77
"packageManager": "[email protected]",
88
"description": "Custom ui css style in both editor and webview, unify global font family, setup background image",
@@ -170,6 +170,43 @@
170170
"type": "object",
171171
"description": "Custom css for editor, support nest selectors"
172172
},
173+
"custom-ui-style.extensions.enable": {
174+
"scope": "application",
175+
"type": "boolean",
176+
"default": true,
177+
"description": "Enable file patch in other extensions"
178+
},
179+
"custom-ui-style.extensions.config": {
180+
"scope": "application",
181+
"type": "object",
182+
"additionalProperties": {
183+
"type": "array",
184+
"items": {
185+
"type": "object",
186+
"properties": {
187+
"filePath": {
188+
"type": "string",
189+
"description": "Relative path to extension root"
190+
},
191+
"find": {
192+
"type": "string",
193+
"description": "\"find\" in JavaScript code \"str.replace(find, replace)\", support regexp (e.g. /test\\d+/)"
194+
},
195+
"replace": {
196+
"type": "string",
197+
"description": "\"replace\" in JavaScript code \"str.replace(find, replace)\""
198+
}
199+
},
200+
"required": [
201+
"filePath",
202+
"find",
203+
"replace"
204+
],
205+
"additionalProperties": false
206+
}
207+
},
208+
"description": "Config to patch extension code, key is extension id, value is config"
209+
},
173210
"custom-ui-style.webview.enable": {
174211
"scope": "application",
175212
"type": "boolean",

src/manager/base.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,19 @@ export abstract class BaseFileManager implements FileManager {
2727
skipAll?: (() => Promisable<string | undefined | false>) | undefined
2828

2929
async reload() {
30-
await this.skipable(async () => {
31-
if (!this.hasBakFile) {
32-
log.warn(`Backup file [${this.bakPath}] does not exist, backuping...`)
33-
fs.cpSync(this.srcPath, this.bakPath)
34-
log.info(`Create backup file [${this.bakPath}]`)
35-
}
36-
const newContent = await this.patch(readFileSync(this.bakPath, 'utf-8'))
37-
writeFileSync(this.srcPath, newContent)
38-
log.info(`Config reload [${this.srcPath}]`)
39-
})
30+
let skipMessage = await this.skipAll?.()
31+
if (skipMessage) {
32+
promptWarn(skipMessage)
33+
return
34+
}
35+
if (!this.hasBakFile) {
36+
log.warn(`Backup file [${this.bakPath}] does not exist, backuping...`)
37+
fs.cpSync(this.srcPath, this.bakPath)
38+
log.info(`Create backup file [${this.bakPath}]`)
39+
}
40+
const newContent = await this.patch(readFileSync(this.bakPath, 'utf-8'))
41+
writeFileSync(this.srcPath, newContent)
42+
log.info(`Config reload [${this.srcPath}]`)
4043
}
4144

4245
async rollback() {
@@ -48,14 +51,5 @@ export abstract class BaseFileManager implements FileManager {
4851
}
4952
}
5053

51-
async skipable(fn: () => Promisable<void>) {
52-
let skipMessage = await this.skipAll?.()
53-
if (skipMessage) {
54-
promptWarn(skipMessage)
55-
} else {
56-
await fn()
57-
}
58-
}
59-
6054
abstract patch(content: string): Promisable<string>
6155
}

src/manager/extension.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { Promisable } from '@subframe7536/type-utils'
2+
3+
import path from 'node:path'
4+
5+
import { extensions } from 'vscode'
6+
7+
import { config } from '../config'
8+
import { log } from '../logger'
9+
import { promptWarn } from '../utils'
10+
import { BaseFileManager } from './base'
11+
12+
interface ExtensionPatchConfig {
13+
filePath: string
14+
find: string
15+
replace: string
16+
}
17+
18+
function getBackupPath(filePath: string) {
19+
const obj = path.parse(filePath)
20+
return path.join(obj.dir, `${obj.name}.custom-ui-style${obj.ext}`)
21+
}
22+
23+
function tryParseRegex(str: string): RegExp | string {
24+
if (str.length < 3) {
25+
return str
26+
}
27+
if (!str.startsWith('/')) {
28+
return str
29+
}
30+
31+
const lastSlashPos = str.lastIndexOf('/')
32+
if (lastSlashPos <= 0) {
33+
return str
34+
}
35+
36+
const body = str.slice(1, lastSlashPos)
37+
if (body.length === 0) {
38+
return str
39+
}
40+
41+
const flags = str.slice(lastSlashPos + 1)
42+
if (!/^[gimsuy]*$/.test(flags)) {
43+
return str
44+
}
45+
46+
try {
47+
return new RegExp(body, flags)
48+
} catch (e) {
49+
log.error(e)
50+
return str
51+
}
52+
}
53+
54+
class ExtensionFileManager extends BaseFileManager {
55+
constructor(private config: ExtensionPatchConfig) {
56+
super(config.filePath, getBackupPath(config.filePath))
57+
}
58+
59+
patch(content: string): Promisable<string> {
60+
return content.replace(tryParseRegex(this.config.find), this.config.replace)
61+
}
62+
}
63+
64+
export function createExtensionFileManagers() {
65+
if (!config['extensions.enable'] || !config['extensions.config']) {
66+
return []
67+
}
68+
const managers = []
69+
for (const [extensionId, patchConfig] of Object.entries(config['extensions.config'])) {
70+
const rootPath = extensions.getExtension(extensionId)?.extensionPath
71+
if (!rootPath) {
72+
promptWarn(`No such extension: ${extensionId}, skip patch`)
73+
continue
74+
}
75+
if (!Array.isArray(patchConfig)) {
76+
promptWarn(`Config should be an array, skip patching ${extensionId}`)
77+
continue
78+
}
79+
const warningArray = []
80+
for (const conf of patchConfig) {
81+
if (conf.filePath && conf.find && conf.replace) {
82+
managers.push(new ExtensionFileManager({
83+
...conf,
84+
filePath: path.join(rootPath, conf.filePath),
85+
}))
86+
} else {
87+
warningArray.push(conf)
88+
}
89+
}
90+
if (warningArray.length > 0) {
91+
promptWarn(`Configs of ${extensionId} are invalid: ${JSON.stringify(warningArray, null, 2)}`)
92+
}
93+
}
94+
return managers
95+
}

src/manager/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { version } from 'vscode'
55
import { config } from '../config'
66
import { runAndRestart } from '../utils'
77
import { CssFileManager } from './css'
8+
import { createExtensionFileManagers } from './extension'
89
import { ExternalFileManager } from './external'
910
import { JsonFileManager } from './json'
1011
import { MainFileManager } from './main'
@@ -26,8 +27,10 @@ export function createFileManagers() {
2627
new RendererFileManager(),
2728
new ExternalFileManager(),
2829
new WebViewFileManager(),
29-
new JsonFileManager(), // MUST be the end of managers
30+
new JsonFileManager(), // MUST be the end of built-in file managers
31+
...createExtensionFileManagers(),
3032
]
33+
3134
return {
3235
hasBakFile: () => managers.every(m => m.hasBakFile),
3336
reload: async (text: string) => {

0 commit comments

Comments
 (0)