refactor(editor): support dynamic languages python/javascript in codemirror

- editor.ts: 新增 languageCompartment + buildLanguageExtension(lang, schema)
  python → lang-python, node/javascript → lang-javascript, sql/缺省 → lang-sql
  新增 setLanguage(lang) 热切换; setSchema 保留当前语言不被强制切回 sql
- package.json: +@codemirror/lang-python +lang-javascript
- codemirror_bridge.rs: EditorInstance 新增 set_language wasm-bindgen extern

(产物 public/codemirror/ 被 gitignore,本地已重建)
This commit is contained in:
xfy 2026-07-03 18:22:21 +08:00
parent a77a2e3502
commit 576803302d
4 changed files with 109 additions and 17 deletions

View File

@ -14,6 +14,8 @@
"@catppuccin/codemirror": "^1.0.3", "@catppuccin/codemirror": "^1.0.3",
"@codemirror/autocomplete": "^6.18.0", "@codemirror/autocomplete": "^6.18.0",
"@codemirror/commands": "^6.8.0", "@codemirror/commands": "^6.8.0",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-python": "^6.2.0",
"@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-sql": "^6.10.0",
"@codemirror/language": "^6.11.0", "@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",

View File

@ -1,5 +1,7 @@
import { javascript } from '@codemirror/lang-javascript';
import { python } from '@codemirror/lang-python';
import { PostgreSQL, sql } from '@codemirror/lang-sql'; import { PostgreSQL, sql } from '@codemirror/lang-sql';
import { Compartment, EditorState } from '@codemirror/state'; import { Compartment, EditorState, type Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view'; import { EditorView } from '@codemirror/view';
import { vim } from '@replit/codemirror-vim'; import { vim } from '@replit/codemirror-vim';
import { basicSetup } from 'codemirror'; import { basicSetup } from 'codemirror';
@ -25,20 +27,48 @@ export class EditorOptions {
onReady?: () => void; onReady?: () => void;
} }
/**
* CodeMirror Extension
* - `python` @codemirror/lang-python
* - `node` / `javascript` / `js` @codemirror/lang-javascript
* - `sql` / @codemirror/lang-sqlPostgreSQL + schema
*
* SQL 使 schema SQL schema SQL
*/
function buildLanguageExtension(lang: string, schema: SqlSchema): Extension {
const normalized = (lang ?? '').toLowerCase();
if (normalized === 'python') {
return python();
}
if (normalized === 'node' || normalized === 'javascript' || normalized === 'js') {
return javascript();
}
// sql / 缺省:保留原有 PostgreSQL + schema 补全行为
return sql({
dialect: PostgreSQL,
schema: schemaToCompletions(schema),
upperCaseKeywords: true,
});
}
/** /**
* CodeMirror * CodeMirror
* create() Compartment theme/schema/vim * create() Compartment theme/language/schema/vim
* reconfigure Vim * reconfigure Vim
*/ */
export class CodeMirrorInstance { export class CodeMirrorInstance {
private view: EditorView; private view: EditorView;
private themeCompartment = new Compartment(); private themeCompartment = new Compartment();
private schemaCompartment = new Compartment(); private languageCompartment = new Compartment();
private vimCompartment = new Compartment(); private vimCompartment = new Compartment();
private language: string;
private schema: SqlSchema;
constructor(container: HTMLElement, options: EditorOptions) { constructor(container: HTMLElement, options: EditorOptions) {
const theme: ThemeName = options.theme ?? 'light'; const theme: ThemeName = options.theme ?? 'light';
const schema = options.schema ?? { tables: [] }; const schema = options.schema ?? { tables: [] };
this.schema = schema;
this.language = options.language ?? 'sql';
this.view = new EditorView({ this.view = new EditorView({
state: EditorState.create({ state: EditorState.create({
@ -48,12 +78,8 @@ export class CodeMirrorInstance {
// vim 必须在 keymap 最前(@replit/codemirror-vim 仓库要求) // vim 必须在 keymap 最前(@replit/codemirror-vim 仓库要求)
this.vimCompartment.of(options.vim ? [vim()] : []), this.vimCompartment.of(options.vim ? [vim()] : []),
this.themeCompartment.of(themeExtension(theme)), this.themeCompartment.of(themeExtension(theme)),
this.schemaCompartment.of( this.languageCompartment.of(
sql({ buildLanguageExtension(this.language, schema),
dialect: PostgreSQL,
schema: schemaToCompletions(schema),
upperCaseKeywords: true,
}),
), ),
EditorView.updateListener.of((v) => { EditorView.updateListener.of((v) => {
if (v.docChanged) { if (v.docChanged) {
@ -85,19 +111,30 @@ export class CodeMirrorInstance {
}); });
} }
/** 更新 SQL 补全 schemaCompartment.reconfigure。 */ /** 热切换语言python/node/javascript/sql不重建实例Compartment.reconfigure。 */
setSchema(schema: SqlSchema): void { setLanguage(lang: string): void {
this.language = lang;
this.view.dispatch({ this.view.dispatch({
effects: this.schemaCompartment.reconfigure( effects: this.languageCompartment.reconfigure(
sql({ buildLanguageExtension(lang, this.schema),
dialect: PostgreSQL,
schema: schemaToCompletions(schema),
upperCaseKeywords: true,
}),
), ),
}); });
} }
/** SQL schemaCompartment.reconfigure
* SQL SQL schema SQL */
setSchema(schema: SqlSchema): void {
this.schema = schema;
// 仅当当前是 SQL 语言时才重配 extension其它语言不消费 schema
if ((this.language ?? '').toLowerCase() === 'sql' || !this.language) {
this.view.dispatch({
effects: this.languageCompartment.reconfigure(
buildLanguageExtension('sql', schema),
),
});
}
}
focus(): void { focus(): void {
this.view.focus(); this.view.focus();
} }

48
libs/pnpm-lock.yaml generated
View File

@ -35,6 +35,12 @@ importers:
'@codemirror/commands': '@codemirror/commands':
specifier: ^6.8.0 specifier: ^6.8.0
version: 6.10.4 version: 6.10.4
'@codemirror/lang-javascript':
specifier: ^6.2.3
version: 6.2.5
'@codemirror/lang-python':
specifier: ^6.2.0
version: 6.2.1
'@codemirror/lang-sql': '@codemirror/lang-sql':
specifier: ^6.10.0 specifier: ^6.10.0
version: 6.10.0 version: 6.10.0
@ -162,6 +168,12 @@ packages:
'@codemirror/commands@6.10.4': '@codemirror/commands@6.10.4':
resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==}
'@codemirror/lang-javascript@6.2.5':
resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==}
'@codemirror/lang-python@6.2.1':
resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==}
'@codemirror/lang-sql@6.10.0': '@codemirror/lang-sql@6.10.0':
resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
@ -207,9 +219,15 @@ packages:
'@lezer/highlight@1.2.3': '@lezer/highlight@1.2.3':
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
'@lezer/javascript@1.5.4':
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
'@lezer/lr@1.4.10': '@lezer/lr@1.4.10':
resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
'@lezer/python@1.1.19':
resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==}
'@marijn/find-cluster-break@1.0.3': '@marijn/find-cluster-break@1.0.3':
resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==}
@ -970,6 +988,24 @@ snapshots:
'@codemirror/view': 6.43.4 '@codemirror/view': 6.43.4
'@lezer/common': 1.5.2 '@lezer/common': 1.5.2
'@codemirror/lang-javascript@6.2.5':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/lint': 6.9.7
'@codemirror/state': 6.7.0
'@codemirror/view': 6.43.4
'@lezer/common': 1.5.2
'@lezer/javascript': 1.5.4
'@codemirror/lang-python@6.2.1':
dependencies:
'@codemirror/autocomplete': 6.20.3
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.0
'@lezer/common': 1.5.2
'@lezer/python': 1.1.19
'@codemirror/lang-sql@6.10.0': '@codemirror/lang-sql@6.10.0':
dependencies: dependencies:
'@codemirror/autocomplete': 6.20.3 '@codemirror/autocomplete': 6.20.3
@ -1046,10 +1082,22 @@ snapshots:
dependencies: dependencies:
'@lezer/common': 1.5.2 '@lezer/common': 1.5.2
'@lezer/javascript@1.5.4':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@lezer/lr@1.4.10': '@lezer/lr@1.4.10':
dependencies: dependencies:
'@lezer/common': 1.5.2 '@lezer/common': 1.5.2
'@lezer/python@1.1.19':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.3
'@lezer/lr': 1.4.10
'@marijn/find-cluster-break@1.0.3': {} '@marijn/find-cluster-break@1.0.3': {}
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':

View File

@ -89,6 +89,11 @@ pub mod wasm {
#[wasm_bindgen(method, js_name = setTheme)] #[wasm_bindgen(method, js_name = setTheme)]
pub fn set_theme(this: &EditorInstance, theme: &str); pub fn set_theme(this: &EditorInstance, theme: &str);
/// 热切换语言python/node/javascript/sqlCompartment.reconfigure
/// 由 CodeRunner 组件在挂载时按 data-lang 调用。
#[wasm_bindgen(method, js_name = setLanguage)]
pub fn set_language(this: &EditorInstance, lang: &str);
/// 更新 SQL 补全 schemaCompartment.reconfigure /// 更新 SQL 补全 schemaCompartment.reconfigure
/// 参数为 serde_wasm_bindgen::to_value 序列化后的 JsValue /// 参数为 serde_wasm_bindgen::to_value 序列化后的 JsValue
///SqlSchema 是 serde 类型,非 wasm-bindgen 类型,故不能直接传 &SqlSchema ///SqlSchema 是 serde 类型,非 wasm-bindgen 类型,故不能直接传 &SqlSchema