feat(editor): add codemirror-editor subproject + Rust bridge
新建 libs/codemirror-editor(pnpm/Vite IIFE/@catppuccin/codemirror Catppuccin Latte+Mocha 主题/@codemirror/lang-sql schema 补全/@replit/codemirror-vim), 输出 public/codemirror/。Rust 桥接 codemirror_bridge.rs 镜像 tiptap_bridge.rs (Reflect::get 取对象字面量 + EditorHandle 持有闭包 + Drop→destroy)。 接入 Makefile/Dioxus.toml/.gitignore。 注意:thememirror 不含 catppuccin 主题,改用官方 @catppuccin/codemirror@1.0.3 (导出 catppuccinLatte/catppuccinMocha)。
This commit is contained in:
parent
2776d39ac1
commit
c74e59485d
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,7 @@ others/
|
||||
public/style.css
|
||||
public/highlight.css
|
||||
public/tiptap
|
||||
public/codemirror
|
||||
public/lightbox
|
||||
public/yggdrasil-core
|
||||
public/doc
|
||||
|
||||
@ -11,8 +11,8 @@ watch_path = ["src", "Cargo.toml"]
|
||||
|
||||
[web.resource]
|
||||
style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css", "/yggdrasil-core/yggdrasil-core.css"]
|
||||
script = ["/tiptap/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"]
|
||||
script = ["/tiptap/editor.js", "/codemirror/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"]
|
||||
|
||||
[web.resource.dev]
|
||||
style = ["/style.css", "/highlight.css", "/tiptap/editor.css", "/lightbox/lightbox.css", "/yggdrasil-core/yggdrasil-core.css"]
|
||||
script = ["/tiptap/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"]
|
||||
script = ["/tiptap/editor.js", "/codemirror/editor.js", "/lightbox/lightbox.js", "/yggdrasil-core/yggdrasil-core.js"]
|
||||
|
||||
16
Makefile
16
Makefile
@ -1,9 +1,10 @@
|
||||
.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental build-core build-core-incremental highlight-css test doc doc-open start clippy fix
|
||||
.PHONY: dev build build-linux css css-watch clean build-editor build-editor-incremental build-lightbox build-lightbox-incremental build-core build-core-incremental build-codemirror build-codemirror-incremental highlight-css test doc doc-open start clippy fix
|
||||
|
||||
build:
|
||||
@$(MAKE) build-editor
|
||||
@$(MAKE) build-lightbox
|
||||
@$(MAKE) build-core
|
||||
@$(MAKE) build-codemirror
|
||||
@$(MAKE) highlight-css
|
||||
@tailwindcss -i input.css -o public/style.css --minify
|
||||
@$(MAKE) doc
|
||||
@ -13,6 +14,7 @@ build-linux:
|
||||
@$(MAKE) build-editor
|
||||
@$(MAKE) build-lightbox
|
||||
@$(MAKE) build-core
|
||||
@$(MAKE) build-codemirror
|
||||
@$(MAKE) highlight-css
|
||||
@tailwindcss -i input.css -o public/style.css --minify
|
||||
@dx build @client --release --debug-symbols=false --wasm-js-cfg false
|
||||
@ -53,7 +55,16 @@ build-core:
|
||||
build-core-incremental:
|
||||
@cd libs/yggdrasil-core && pnpm run build
|
||||
|
||||
dev: build-editor-incremental build-lightbox-incremental build-core-incremental
|
||||
build-codemirror:
|
||||
@echo "Building CodeMirror editor..."
|
||||
@cd libs/codemirror-editor && pnpm ci --include=dev && pnpm run build
|
||||
@echo "CodeMirror editor built."
|
||||
|
||||
# dev 用的增量构建:跳过 pnpm ci(假设 node_modules 已存在),仅 vite build。
|
||||
build-codemirror-incremental:
|
||||
@cd libs/codemirror-editor && pnpm run build
|
||||
|
||||
dev: build-editor-incremental build-lightbox-incremental build-core-incremental build-codemirror-incremental
|
||||
@echo "Cleaning static/..."
|
||||
@rm -rf static/
|
||||
@echo "Building Tiptap editor (incremental)..."
|
||||
@ -74,6 +85,7 @@ test:
|
||||
@cd libs/tiptap-editor && pnpm test
|
||||
@cd libs/lightbox && pnpm test
|
||||
@cd libs/yggdrasil-core && pnpm test
|
||||
@cd libs/codemirror-editor && pnpm test
|
||||
|
||||
clippy:
|
||||
@cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
30
libs/codemirror-editor/package.json
Normal file
30
libs/codemirror-editor/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@yggdrasil/codemirror-editor",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"dev": "vite",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@catppuccin/codemirror": "^1.0.3",
|
||||
"@codemirror/autocomplete": "^6.18.0",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"@codemirror/view": "^6.36.0",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"codemirror": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"happy-dom": "^20.10.6",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.0",
|
||||
"vitest": "^4.1.9"
|
||||
}
|
||||
}
|
||||
1041
libs/codemirror-editor/pnpm-lock.yaml
generated
Normal file
1041
libs/codemirror-editor/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
67
libs/codemirror-editor/src/__tests__/editor.test.ts
Normal file
67
libs/codemirror-editor/src/__tests__/editor.test.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { EditorOptions, CodeMirrorInstance } from '../editor';
|
||||
|
||||
describe('CodeMirrorInstance', () => {
|
||||
let container: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
container.id = 'test-cm';
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
it('getValue/setValue 往返', () => {
|
||||
const inst = new CodeMirrorInstance(container, new EditorOptions());
|
||||
inst.setValue('SELECT 1');
|
||||
expect(inst.getValue()).toBe('SELECT 1');
|
||||
inst.destroy();
|
||||
});
|
||||
|
||||
it('初始 value 正确', () => {
|
||||
const opts = new EditorOptions();
|
||||
opts.value = 'SELECT * FROM posts';
|
||||
const inst = new CodeMirrorInstance(container, opts);
|
||||
expect(inst.getValue()).toBe('SELECT * FROM posts');
|
||||
inst.destroy();
|
||||
});
|
||||
|
||||
it('setTheme 不抛错(走 Compartment reconfigure)', () => {
|
||||
const inst = new CodeMirrorInstance(container, new EditorOptions());
|
||||
expect(() => inst.setTheme('dark')).not.toThrow();
|
||||
expect(() => inst.setTheme('light')).not.toThrow();
|
||||
inst.destroy();
|
||||
});
|
||||
|
||||
it('setSchema 更新 lang-sql 配置', () => {
|
||||
const inst = new CodeMirrorInstance(container, new EditorOptions());
|
||||
expect(() =>
|
||||
inst.setSchema({ tables: [{ name: 'posts', columns: ['id', 'title'] }] }),
|
||||
).not.toThrow();
|
||||
inst.destroy();
|
||||
});
|
||||
|
||||
it('vim 开关:vim:true 注入,false 不注入', () => {
|
||||
const optsOn = new EditorOptions();
|
||||
optsOn.vim = true;
|
||||
const instOn = new CodeMirrorInstance(container, optsOn);
|
||||
instOn.destroy();
|
||||
|
||||
const optsOff = new EditorOptions();
|
||||
optsOff.vim = false;
|
||||
const instOff = new CodeMirrorInstance(container, optsOff);
|
||||
instOff.destroy();
|
||||
// happy-dom 无法验证 keymap 行为,仅验证配置加载不抛错
|
||||
});
|
||||
|
||||
it('onChange 在内容变更时触发', () => {
|
||||
let captured = '';
|
||||
const opts = new EditorOptions();
|
||||
opts.onChange = (v) => {
|
||||
captured = v;
|
||||
};
|
||||
const inst = new CodeMirrorInstance(container, opts);
|
||||
inst.setValue('hello');
|
||||
expect(captured).toBe('hello');
|
||||
inst.destroy();
|
||||
});
|
||||
});
|
||||
127
libs/codemirror-editor/src/editor.ts
Normal file
127
libs/codemirror-editor/src/editor.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { EditorState, Compartment } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { sql, PostgreSQL } from '@codemirror/lang-sql';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { themeExtension, type ThemeName } from './themes';
|
||||
|
||||
/** SQL 补全用 schema 数据(由 Rust 侧从实时库拉取注入)。 */
|
||||
export interface SqlSchema {
|
||||
tables: { name: string; columns: string[] }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 传给 CodeMirrorEditor.create 的配置。
|
||||
* 必须是 class(非 interface),以便 TS 擦除后存活,
|
||||
* wasm 侧能用 `new EditorOptions()` 构造,并通过 setter 填充字段。
|
||||
*/
|
||||
export class EditorOptions {
|
||||
language?: string;
|
||||
theme?: ThemeName;
|
||||
vim?: boolean;
|
||||
schema?: SqlSchema;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onReady?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* CodeMirror 实例封装。
|
||||
* create() 时用三个 Compartment 注入 theme/schema/vim,
|
||||
* 支持后续热切换(reconfigure)而不重建实例——保留 Vim 状态、光标、撤销栈。
|
||||
*/
|
||||
export class CodeMirrorInstance {
|
||||
private view: EditorView;
|
||||
private themeCompartment = new Compartment();
|
||||
private schemaCompartment = new Compartment();
|
||||
private vimCompartment = new Compartment();
|
||||
|
||||
constructor(container: HTMLElement, options: EditorOptions) {
|
||||
const theme: ThemeName = options.theme ?? 'light';
|
||||
const schema = options.schema ?? { tables: [] };
|
||||
|
||||
this.view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: options.value ?? '',
|
||||
extensions: [
|
||||
basicSetup,
|
||||
// vim 必须在 keymap 最前(@replit/codemirror-vim 仓库要求)
|
||||
this.vimCompartment.of(options.vim ? [vim()] : []),
|
||||
this.themeCompartment.of(themeExtension(theme)),
|
||||
this.schemaCompartment.of(
|
||||
sql({
|
||||
dialect: PostgreSQL,
|
||||
schema: schemaToCompletions(schema),
|
||||
upperCaseKeywords: true,
|
||||
}),
|
||||
),
|
||||
EditorView.updateListener.of((v) => {
|
||||
if (v.docChanged) {
|
||||
options.onChange?.(this.view.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
],
|
||||
}),
|
||||
parent: container,
|
||||
});
|
||||
|
||||
options.onReady?.();
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
return this.view.state.doc.toString();
|
||||
}
|
||||
|
||||
setValue(s: string): void {
|
||||
this.view.dispatch({
|
||||
changes: { from: 0, to: this.view.state.doc.length, insert: s },
|
||||
});
|
||||
}
|
||||
|
||||
/** 热切换主题,不重建实例(Compartment.reconfigure)。 */
|
||||
setTheme(theme: ThemeName): void {
|
||||
this.view.dispatch({
|
||||
effects: this.themeCompartment.reconfigure(themeExtension(theme)),
|
||||
});
|
||||
}
|
||||
|
||||
/** 更新 SQL 补全 schema(Compartment.reconfigure)。 */
|
||||
setSchema(schema: SqlSchema): void {
|
||||
this.view.dispatch({
|
||||
effects: this.schemaCompartment.reconfigure(
|
||||
sql({
|
||||
dialect: PostgreSQL,
|
||||
schema: schemaToCompletions(schema),
|
||||
upperCaseKeywords: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.view.focus();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.view.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/** 把 SqlSchema 转成 @codemirror/lang-sql 期望的补全结构。 */
|
||||
function schemaToCompletions(schema: SqlSchema) {
|
||||
return schema.tables.map((t) => ({
|
||||
label: t.name,
|
||||
type: 'table',
|
||||
detail: 'table',
|
||||
columns: t.columns.map((c) => ({ label: c, type: 'column' })),
|
||||
}));
|
||||
}
|
||||
|
||||
// 暴露 EditorOptions 到 window,供 wasm-bindgen 用 new EditorOptions()。
|
||||
// IIFE 的 name 只能挂一个全局(CodeMirrorEditor),故手动 hoist EditorOptions。
|
||||
declare global {
|
||||
interface Window {
|
||||
EditorOptions: typeof EditorOptions;
|
||||
}
|
||||
}
|
||||
window.EditorOptions = EditorOptions;
|
||||
31
libs/codemirror-editor/src/index.ts
Normal file
31
libs/codemirror-editor/src/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { CodeMirrorInstance, EditorOptions } from './editor';
|
||||
|
||||
/**
|
||||
* 模块入口:暴露对象字面量 { create } 作为默认导出。
|
||||
* IIFE 产物挂在 window.CodeMirrorEditor 上,由 Rust 侧用 Reflect::get 取
|
||||
* (对象字面量,不能用 wasm-bindgen 的 extern fn——那会被编成函数调用而失败)。
|
||||
*/
|
||||
const CodeMirrorEditor = {
|
||||
_instances: new Map<string, CodeMirrorInstance>(),
|
||||
|
||||
create(
|
||||
containerId: string,
|
||||
options: EditorOptions = new EditorOptions(),
|
||||
): CodeMirrorInstance | null {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return null;
|
||||
|
||||
// 销毁同 id 的旧实例
|
||||
const existing = this._instances.get(containerId);
|
||||
if (existing) {
|
||||
existing.destroy();
|
||||
this._instances.delete(containerId);
|
||||
}
|
||||
|
||||
const instance = new CodeMirrorInstance(container, options);
|
||||
this._instances.set(containerId, instance);
|
||||
return instance;
|
||||
},
|
||||
};
|
||||
|
||||
export default CodeMirrorEditor;
|
||||
11
libs/codemirror-editor/src/themes.ts
Normal file
11
libs/codemirror-editor/src/themes.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// @catppuccin/codemirror 提供现成的 Catppuccin 主题 Extension,
|
||||
// 与项目 themes/ 下的 Catppuccin Latte/Mocha .tmTheme 视觉一致。
|
||||
import { catppuccinLatte, catppuccinMocha } from '@catppuccin/codemirror';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
|
||||
export type ThemeName = 'light' | 'dark';
|
||||
|
||||
/** 根据主题名返回对应的 CodeMirror 主题 Extension。 */
|
||||
export function themeExtension(name: ThemeName): Extension {
|
||||
return name === 'light' ? catppuccinLatte : catppuccinMocha;
|
||||
}
|
||||
19
libs/codemirror-editor/tsconfig.json
Normal file
19
libs/codemirror-editor/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
27
libs/codemirror-editor/vite.config.ts
Normal file
27
libs/codemirror-editor/vite.config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
// 输出直写 public/codemirror/,Dioxus 直接托管,无需拷贝步骤。
|
||||
outDir: resolve(__dirname, '../../public/codemirror'),
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
// IIFE 产物挂在 window.CodeMirrorEditor 上,Rust 侧用 Reflect::get 取。
|
||||
name: 'CodeMirrorEditor',
|
||||
fileName: () => 'editor.js',
|
||||
formats: ['iife'],
|
||||
},
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
// 默认导出(对象字面量 { create() })成为 window.CodeMirrorEditor。
|
||||
exports: 'default',
|
||||
assetFileNames: 'editor.[ext]',
|
||||
},
|
||||
},
|
||||
cssCodeSplit: false,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
},
|
||||
});
|
||||
8
libs/codemirror-editor/vitest.config.ts
Normal file
8
libs/codemirror-editor/vitest.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
})
|
||||
187
src/codemirror_bridge.rs
Normal file
187
src/codemirror_bridge.rs
Normal file
@ -0,0 +1,187 @@
|
||||
//! CodeMirror 编辑器的 wasm-bindgen 绑定层。
|
||||
//!
|
||||
//! 封装与 `window.CodeMirrorEditor`(IIFE 暴露的全局对象字面量)的全部交互,
|
||||
//! 严格镜像 [`crate::tiptap_bridge`] 的结构:共享纯数据类型双目标编译,
|
||||
//! wasm-bindgen extern + [`EditorHandle`] 仅在 WASM 前端编译(server 构建无 window)。
|
||||
//!
|
||||
//! 与 tiptap 一样,`CodeMirrorEditor` 是 IIFE 挂在 window 上的**对象字面量**
|
||||
//! (`{ create }`),不是函数——因此用 `js_sys::Reflect::get` 做属性访问拿到,
|
||||
//! 不能用 wasm-bindgen 的 extern fn(那会被编成函数调用,"not a function")。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// SQL 补全用 schema 数据,由 `get_db_schema` server function 填充。
|
||||
// 暂未被消费(Task 5 的 get_db_schema 会用到),先 allow dead_code 避免 -D warnings 阻断 clippy。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
|
||||
pub struct SqlSchema {
|
||||
pub tables: Vec<SqlTable>,
|
||||
}
|
||||
|
||||
/// 单张表的补全数据:表名 + 列名列表。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct SqlTable {
|
||||
pub name: String,
|
||||
pub columns: Vec<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 以下全部仅在 WASM 前端编译:wasm-bindgen extern + EditorHandle + 闭包。
|
||||
// 放在 #[cfg] 子模块内,避免 server 构建尝试编译引用 JS 对象的 extern。
|
||||
// ============================================================================
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm {
|
||||
use super::SqlSchema;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
// —— window.CodeMirrorEditor 模块对象 ——
|
||||
//
|
||||
// CodeMirrorEditor 是 IIFE 产物挂在 window 上的对象字面量(含 create 方法),
|
||||
// 不是函数。wasm-bindgen 对 `fn get_module() -> T` 形式的 extern 会生成
|
||||
// `window.CodeMirrorEditor()`(函数调用),会因 "not a function" 失败。
|
||||
// 因此用 js_sys::Reflect::get 做属性访问拿到模块对象,再 unchecked_into。
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
/// `window.CodeMirrorEditor` 模块对象的 Rust 映射(IIFE 产物挂在 window 上的对象字面量)。
|
||||
/// 不是函数——通过 [`get_module`] 用 Reflect::get 取属性而非 extern fn 调用拿到。
|
||||
pub type CodeMirrorEditorModule;
|
||||
|
||||
/// 调用 `CodeMirrorEditor.create(containerId, opts)`。
|
||||
/// 找不到容器返回 null(被 Option 捕获);构造失败抛异常(被 catch 捕获)。
|
||||
#[wasm_bindgen(method, catch)]
|
||||
pub fn create(
|
||||
this: &CodeMirrorEditorModule,
|
||||
container_id: &str,
|
||||
opts: &EditorOptions,
|
||||
) -> Result<Option<EditorInstance>, JsValue>;
|
||||
}
|
||||
|
||||
/// 读取 `window.CodeMirrorEditor`(IIFE 默认导出,顶层 var 即 window 属性)。
|
||||
/// 用 Reflect::get 做属性访问——extern fn 形式会被 wasm-bindgen 编成函数调用。
|
||||
///
|
||||
/// 用 unchecked_into 而非 dyn_into:CodeMirrorEditor 是 JS 对象字面量,
|
||||
/// 不是 wasm-bindgen 注册的构造函数实例,dyn_into 的 instanceof 检查必然失败。
|
||||
/// unchecked_into 只做编译期类型标注,不做运行时校验
|
||||
/// (Reflect.get 已保证拿到的是目标对象)。
|
||||
pub fn get_module() -> CodeMirrorEditorModule {
|
||||
let window = web_sys::window().expect("no window");
|
||||
let val = js_sys::Reflect::get(&window, &"CodeMirrorEditor".into())
|
||||
.expect("window.CodeMirrorEditor missing");
|
||||
val.unchecked_into::<CodeMirrorEditorModule>()
|
||||
}
|
||||
|
||||
// —— 编辑器实例(CodeMirrorInstance)——
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
/// `CodeMirrorEditor.create` 返回的编辑器实例对象,承载 CodeMirror EditorView。
|
||||
pub type EditorInstance;
|
||||
|
||||
/// 返回当前文档全文。
|
||||
#[wasm_bindgen(method, js_name = getValue)]
|
||||
pub fn get_value(this: &EditorInstance) -> String;
|
||||
|
||||
/// 替换整个文档内容(dispatch changes,触发 onChange)。
|
||||
#[wasm_bindgen(method, js_name = setValue)]
|
||||
pub fn set_value(this: &EditorInstance, s: &str);
|
||||
|
||||
/// 热切换主题(Compartment.reconfigure,不重建实例)。
|
||||
#[wasm_bindgen(method, js_name = setTheme)]
|
||||
pub fn set_theme(this: &EditorInstance, theme: &str);
|
||||
|
||||
/// 更新 SQL 补全 schema(Compartment.reconfigure)。
|
||||
#[wasm_bindgen(method, js_name = setSchema)]
|
||||
pub fn set_schema(this: &EditorInstance, schema: &SqlSchema);
|
||||
|
||||
/// 让编辑器获取焦点。
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn focus(this: &EditorInstance);
|
||||
|
||||
/// 销毁编辑器,释放 JS 侧资源。
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn destroy(this: &EditorInstance);
|
||||
}
|
||||
|
||||
// —— EditorOptions:用 builder 模式(setter)构造 JS 对象 ——
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
/// 传给 `CodeMirrorEditor.create` 的配置对象,对应 JS 侧的 EditorOptions。
|
||||
/// 用 `new()` 创建空对象后通过 setter 链式设置字段。
|
||||
pub type EditorOptions;
|
||||
|
||||
/// 构造一个空的 EditorOptions,随后用各 setter 填充。
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> EditorOptions;
|
||||
|
||||
/// 语言(默认 'sql')。
|
||||
#[wasm_bindgen(method, setter, js_name = language)]
|
||||
pub fn set_language(this: &EditorOptions, v: &str);
|
||||
|
||||
/// 主题:'light'(Catppuccin Latte)或 'dark'(Catppuccin Mocha)。
|
||||
#[wasm_bindgen(method, setter, js_name = theme)]
|
||||
pub fn set_theme(this: &EditorOptions, v: &str);
|
||||
|
||||
/// 是否启用 Vim keymap。
|
||||
#[wasm_bindgen(method, setter, js_name = vim)]
|
||||
pub fn set_vim(this: &EditorOptions, v: bool);
|
||||
|
||||
/// SQL 补全 schema(表/列数据)。
|
||||
#[wasm_bindgen(method, setter, js_name = schema)]
|
||||
pub fn set_schema(this: &EditorOptions, v: &SqlSchema);
|
||||
|
||||
/// 初始文档内容。
|
||||
#[wasm_bindgen(method, setter, js_name = value)]
|
||||
pub fn set_value(this: &EditorOptions, v: &str);
|
||||
|
||||
/// 文档变更回调(参数为最新全文)。
|
||||
#[wasm_bindgen(method, setter, js_name = onChange)]
|
||||
pub fn set_on_change(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
|
||||
|
||||
/// 编辑器就绪回调(构造末尾同步触发一次)。
|
||||
#[wasm_bindgen(method, setter, js_name = onReady)]
|
||||
pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
|
||||
}
|
||||
|
||||
/// 编辑器实例句柄:持有 instance + 所有 Closure,Drop 时销毁实例并释放闭包。
|
||||
///
|
||||
/// 闭包字段 `_` 前缀表示仅用于保持生命周期——它们被注入 JS 后,JS 侧持有
|
||||
/// 函数引用;只要 [`EditorHandle`] 存活,闭包就不会被回收。Drop 时随结构释放。
|
||||
pub struct EditorHandle {
|
||||
instance: EditorInstance,
|
||||
_on_change: Closure<dyn FnMut(String)>,
|
||||
_on_ready: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
impl EditorHandle {
|
||||
/// 调用方须先把各 closure set 进 EditorOptions,再 create,
|
||||
/// 然后把返回的 instance + 同名 closure 一起传入 new。
|
||||
pub fn new(
|
||||
instance: EditorInstance,
|
||||
on_change: Closure<dyn FnMut(String)>,
|
||||
on_ready: Closure<dyn FnMut()>,
|
||||
) -> Self {
|
||||
Self {
|
||||
instance,
|
||||
_on_change: on_change,
|
||||
_on_ready: on_ready,
|
||||
}
|
||||
}
|
||||
|
||||
/// 借用底层实例,供宿主调 getValue/setTheme/setSchema 等。
|
||||
pub fn instance(&self) -> &EditorInstance {
|
||||
&self.instance
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EditorHandle {
|
||||
fn drop(&mut self) {
|
||||
// 销毁 JS 侧编辑器;随后 _on_change/_on_ready 字段按声明顺序释放,
|
||||
// 释放 wasm-bindgen 函数表槽位。
|
||||
let _ = self.instance.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm::*;
|
||||
@ -31,6 +31,9 @@ mod theme;
|
||||
// tiptap_bridge:共享类型(UploadsInFlight/UploadErrorEntry)两端都编译;
|
||||
// wasm-bindgen extern 与 EditorHandle 在内部的 #[cfg(wasm32)] 子模块里。
|
||||
mod tiptap_bridge;
|
||||
// codemirror_bridge:SQL 编辑器的 wasm-bindgen 绑定,结构镜像 tiptap_bridge。
|
||||
// 共享类型(SqlSchema/SqlTable)两端都编译;extern 与 EditorHandle 在 #[cfg(wasm32)] 子模块里。
|
||||
mod codemirror_bridge;
|
||||
mod utils;
|
||||
mod webp;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user