first commit

This commit is contained in:
douboer
2025-11-04 17:50:43 +08:00
commit 0cd9a27593
22 changed files with 3296 additions and 0 deletions

450
main.ts Normal file
View File

@@ -0,0 +1,450 @@
import { App, Modal, Notice, Plugin, PluginSettingTab, Setting, Workspace, MarkdownView, TFile, WorkspaceLeaf } from 'obsidian';
import { exec } from "child_process";
import { promisify } from "util";
import * as CodeMirror from 'codemirror';
interface VimIMSwitchSettings {
fcitxRemotePath_macOS: string;
fcitxRemotePath_windows: string;
fcitxRemotePath_linux: string;
englishInputMethod: string;
chineseInputMethod: string;
}
const DEFAULT_SETTINGS: VimIMSwitchSettings = {
fcitxRemotePath_macOS: '/usr/local/bin/fcitx-remote',
fcitxRemotePath_windows: 'C:\\Program Files\\bin\\fcitx-remote',
fcitxRemotePath_linux: '/usr/bin/fcitx-remote',
englishInputMethod: 'com.apple.keylayout.ABC',
chineseInputMethod: 'auto-detect', // 将自动检测当前中文输入法
}
const pexec = promisify(exec);
enum IMStatus {
None,
Activate,
Deactivate,
}
export default class VimIMSwitchPlugin extends Plugin {
settings: VimIMSwitchSettings;
imStatus = IMStatus.None;
fcitxRemotePath = "";
private editorMode: 'cm5' | 'cm6' = null;
private initialized = false;
private cmEditor: CodeMirror.Editor = null;
private lastInsertModeIMStatus = IMStatus.None; // 记住上一次insert模式的输入法状态
private keyboardListenerSetup = false; // 防止重复设置键盘监听器
private lastKeyTime = 0; // 防抖:记录上次按键时间
private currentVimMode = 'normal'; // 跟踪当前vim模式
async onload() {
console.log('🚀 [VimIMSwitch] Loading plugin...');
await this.loadSettings();
// 尽早设置全局键盘监听器
this.setupObsidianEditorEvents();
this.addSettingTab(new IMSwitchSettingTab(this.app, this));
this.addCommand({
id: 'test-im-switch-to-english',
name: 'Test: Switch to English',
callback: () => {
this.deactivateIM();
}
});
this.addCommand({
id: 'test-im-switch-to-chinese',
name: 'Test: Switch to Chinese',
callback: () => {
this.activateIM();
}
});
this.addCommand({
id: 'test-check-im-status',
name: 'Test: Check IM Status',
callback: () => {
this.getFcitxRemoteStatus();
}
});
this.app.workspace.on('file-open', async (file: TFile) => {
if (!this.initialized) {
await this.initialize();
}
if (this.cmEditor) {
await this.getFcitxRemoteStatus();
this.lastInsertModeIMStatus = this.imStatus;
await this.deactivateIM();
if (typeof this.cmEditor.off === 'function') {
this.cmEditor.off("vim-mode-change", this.onVimModeChange);
}
if (typeof this.cmEditor.on === 'function') {
this.cmEditor.on("vim-mode-change", this.onVimModeChange);
}
}
});
this.app.workspace.on('active-leaf-change', async (leaf: WorkspaceLeaf) => {
if(this.app.workspace.activeLeaf.view.getViewType() == "markdown") {
if (!this.initialized) {
await this.initialize();
}
if (this.cmEditor) {
await this.getFcitxRemoteStatus();
this.lastInsertModeIMStatus = this.imStatus;
await this.deactivateIM();
this.setupObsidianEditorEvents();
}
}
});
}
async initialize() {
if (this.initialized) {
return;
}
if ('editor:toggle-source' in (this.app as any).commands.editorCommands) {
this.editorMode = 'cm6';
} else {
this.editorMode = 'cm5';
}
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
if (this.editorMode == 'cm6') {
const possiblePaths = [
(view as any).sourceMode?.cmEditor?.cm?.cm,
(view as any).sourceMode?.cmEditor?.cm,
(view as any).sourceMode?.cmEditor,
(view as any).editor?.cm,
(view.editor as any)?.cm
];
for (let i = 0; i < possiblePaths.length; i++) {
const path = possiblePaths[i];
if (path && !this.cmEditor) {
this.cmEditor = path;
break;
}
}
} else {
const possiblePaths = [
(view as any).sourceMode?.cmEditor,
(view as any).editor?.cm,
(view.editor as any)?.cm
];
for (let i = 0; i < possiblePaths.length; i++) {
const path = possiblePaths[i];
if (path && !this.cmEditor) {
this.cmEditor = path;
break;
}
}
}
}
}
setupVimModePolling() {
// 防止重复设置轮询
if ((this as any).vimModePollingInterval) {
clearInterval((this as any).vimModePollingInterval);
}
let lastMode = "";
const pollInterval = setInterval(() => {
if (!this.cmEditor) {
return;
}
try {
const realCM = (this.cmEditor as any).cm;
const currentMode = realCM?.state?.vim?.mode || "";
if (currentMode && currentMode !== lastMode) {
this.onVimModeChange({ mode: currentMode });
lastMode = currentMode;
}
} catch (error) {
// 忽略轮询错误
}
}, 100);
(this as any).vimModePollingInterval = pollInterval;
}
setupObsidianEditorEvents() {
if (this.keyboardListenerSetup) {
return;
}
const handleKeyDown = async (event: KeyboardEvent) => {
const currentTime = Date.now();
// 防抖100ms内只处理一次
if (currentTime - this.lastKeyTime < 100) {
return;
}
this.lastKeyTime = currentTime;
// 处理ESC键退出到normal模式
if (event.key === 'Escape') {
// 退出insert模式前先保存当前输入法状态
const beforeIM = await this.runCmd(this.fcitxRemotePath, ["-n"]);
const currentIMName = beforeIM.trim();
// 检查当前输入法是中文还是英文
if (currentIMName === this.settings.chineseInputMethod) {
this.lastInsertModeIMStatus = IMStatus.Activate;
console.log('🔤 [VimIMSwitch] ESC → English (saved Chinese)');
} else {
this.lastInsertModeIMStatus = IMStatus.Deactivate;
console.log('🔤 [VimIMSwitch] ESC → English (saved English)');
}
// 切换到英文输入法
this.currentVimMode = 'normal';
await this.deactivateIM();
}
// 处理进入insert模式的按键只在normal模式下
else if (this.currentVimMode === 'normal' &&
['i', 'I', 'a', 'A', 'o', 'O', 's', 'S', 'c', 'C'].includes(event.key) &&
!event.ctrlKey && !event.metaKey && !event.altKey) {
// 延迟一下让Vim先切换模式
setTimeout(async () => {
this.currentVimMode = 'insert';
// 恢复上次的输入法状态
if (this.lastInsertModeIMStatus == IMStatus.Activate) {
console.log("🈳 [VimIMSwitch] → Chinese");
await this.activateIM();
} else {
console.log("🔤 [VimIMSwitch] → English");
await this.deactivateIM();
}
}, 10);
}
};
// 移除旧的监听器
if ((this as any).obsidianKeyDownListener) {
document.removeEventListener('keydown', (this as any).obsidianKeyDownListener, { capture: true });
}
// 使用capture模式确保更早接收事件
(this as any).obsidianKeyDownListener = handleKeyDown;
document.addEventListener('keydown', handleKeyDown, { capture: true });
this.keyboardListenerSetup = true;
}
onVimModeChange = async (cm: any) => {
// 防止短时间内重复处理相同的模式切换
const currentTime = Date.now();
if (cm.mode === this.currentVimMode && currentTime - this.lastKeyTime < 100) {
return;
}
// 更新当前vim模式状态
this.currentVimMode = cm.mode;
if (cm.mode == "normal" || cm.mode == "visual") {
// 进入normal/visual模式前先保存当前输入法状态
await this.getFcitxRemoteStatus();
if (this.imStatus == IMStatus.Activate) {
this.lastInsertModeIMStatus = IMStatus.Activate;
}
console.log("🔤 [VimIMSwitch] → English");
await this.deactivateIM();
} else if (cm.mode == "insert" || cm.mode == "replace") {
// 进入insert模式时恢复上次的输入法状态
if (this.lastInsertModeIMStatus == IMStatus.Activate) {
console.log("🈳 [VimIMSwitch] → Chinese");
await this.activateIM();
} else {
console.log("🔤 [VimIMSwitch] → English");
await this.deactivateIM();
}
}
}
async runCmd(cmd: string, args: string[] = []) : Promise<string>{
const output = await pexec(`${cmd} ${args.join(" ")}`);
return output.stdout;
}
async getFcitxRemoteStatus() {
if (this.fcitxRemotePath == "") {
console.log("❌ [VimIMSwitch] Cannot get fcitx-remote path, please set it correctly.");
return;
}
try {
let fcitxRemoteOutput = await this.runCmd(this.fcitxRemotePath);
fcitxRemoteOutput = fcitxRemoteOutput.trimRight();
if (fcitxRemoteOutput == "1") {
this.imStatus = IMStatus.Deactivate;
} else if (fcitxRemoteOutput == "2") {
this.imStatus = IMStatus.Activate;
} else {
this.imStatus = IMStatus.None;
}
} catch (error) {
console.log(`❌ [VimIMSwitch] Error getting IM status:`, error);
}
}
async activateIM() {
if (this.fcitxRemotePath == "") {
console.log("❌ [VimIMSwitch] Cannot get fcitx-remote path, please set it correctly.");
return;
}
try {
await this.runCmd(this.fcitxRemotePath, ["-s", this.settings.chineseInputMethod]);
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.log("❌ [VimIMSwitch] Error activating IM:", error);
}
}
async deactivateIM() {
if (this.fcitxRemotePath == "") {
console.log("❌ [VimIMSwitch] Cannot get fcitx-remote path, please set it correctly.");
return;
}
try {
await this.runCmd(this.fcitxRemotePath, ["-s", this.settings.englishInputMethod]);
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.log("❌ [VimIMSwitch] Error deactivating IM:", error);
}
}
onunload() {
// 清理 CodeMirror 事件监听器
if (this.cmEditor && typeof this.cmEditor.off === 'function') {
this.cmEditor.off("vim-mode-change", this.onVimModeChange);
}
// 清理轮询定时器
if ((this as any).vimModePollingInterval) {
clearInterval((this as any).vimModePollingInterval);
}
// 清理键盘事件监听器
if ((this as any).obsidianKeyDownListener) {
document.removeEventListener('keydown', (this as any).obsidianKeyDownListener, { capture: true });
this.keyboardListenerSetup = false;
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
await this.updateCurrentPath();
await this.detectInputMethods();
}
async detectInputMethods() {
if (this.settings.chineseInputMethod === 'auto-detect') {
try {
const currentIM = await this.runCmd(this.fcitxRemotePath, ["-n"]);
const currentName = currentIM.trim();
if (currentName.includes('pinyin') ||
currentName.includes('chinese') ||
currentName.includes('tencent') ||
currentName.includes('sogou') ||
currentName.includes('baidu')) {
this.settings.chineseInputMethod = currentName;
} else {
this.settings.chineseInputMethod = 'com.tencent.inputmethod.wetype.pinyin';
}
} catch (error) {
this.settings.chineseInputMethod = 'com.tencent.inputmethod.wetype.pinyin';
}
}
}
async updateCurrentPath() {
console.log(`🖥️ [VimIMSwitch] Platform detected: ${process.platform}`);
switch (process.platform) {
case 'darwin':
this.fcitxRemotePath = this.settings.fcitxRemotePath_macOS;
console.log(`🍎 [VimIMSwitch] Using macOS path: ${this.fcitxRemotePath}`);
break;
case 'linux':
this.fcitxRemotePath = this.settings.fcitxRemotePath_linux;
console.log(`🐧 [VimIMSwitch] Using Linux path: ${this.fcitxRemotePath}`);
break;
case 'win32':
this.fcitxRemotePath = this.settings.fcitxRemotePath_windows;
console.log(`🪟 [VimIMSwitch] Using Windows path: ${this.fcitxRemotePath}`);
break;
default:
console.log(`❌ [VimIMSwitch] Platform ${process.platform} is not supported currently.`);
break;
}
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class IMSwitchSettingTab extends PluginSettingTab {
plugin: VimIMSwitchPlugin;
constructor(app: App, plugin: VimIMSwitchPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
let {containerEl} = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Settings for Vim IM Switch plugin.'});
new Setting(containerEl)
.setName('Fcitx Remote Path for macOS')
.setDesc('The absolute path to fcitx-remote bin file on macOS.')
.addText(text => text
.setPlaceholder(DEFAULT_SETTINGS.fcitxRemotePath_macOS)
.setValue(this.plugin.settings.fcitxRemotePath_macOS)
.onChange(async (value) => {
this.plugin.settings.fcitxRemotePath_macOS = value;
this.plugin.updateCurrentPath();
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Fcitx Remote Path for Linux')
.setDesc('The absolute path to fcitx-remote bin file on Linux.')
.addText(text => text
.setPlaceholder(DEFAULT_SETTINGS.fcitxRemotePath_linux)
.setValue(this.plugin.settings.fcitxRemotePath_linux)
.onChange(async (value) => {
this.plugin.settings.fcitxRemotePath_linux = value;
this.plugin.updateCurrentPath();
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Fcitx Remote Path for Windows')
.setDesc('The absolute path to fcitx-remote bin file on Windows.')
.addText(text => text
.setPlaceholder(DEFAULT_SETTINGS.fcitxRemotePath_windows)
.setValue(this.plugin.settings.fcitxRemotePath_windows)
.onChange(async (value) => {
this.plugin.settings.fcitxRemotePath_windows = value;
this.plugin.updateCurrentPath();
await this.plugin.saveSettings();
}));
}
}