diff --git a/src/main/kotlin/com/github/qczone/switch2cursor/actions/OpenFileInCursorAction.kt b/src/main/kotlin/com/github/qczone/switch2cursor/actions/OpenFileInCursorAction.kt index bae398d..76fc56b 100644 --- a/src/main/kotlin/com/github/qczone/switch2cursor/actions/OpenFileInCursorAction.kt +++ b/src/main/kotlin/com/github/qczone/switch2cursor/actions/OpenFileInCursorAction.kt @@ -31,7 +31,8 @@ class OpenFileInCursorAction : AnAction() { val settings = AppSettingsState.getInstance() val cursorPath = settings.cursorPath - val command = when { + // Command to open file and position cursor + val fileCommand = when { System.getProperty("os.name").lowercase().contains("mac") -> { arrayOf("open", "-a", "$cursorPath", "cursor://file$filePath:$line:$column") } @@ -43,27 +44,65 @@ class OpenFileInCursorAction : AnAction() { } } - try { - logger.info("Executing command: ${command.joinToString(" ")}") - ProcessBuilder(*command).start() - } catch (ex: Exception) { - logger.error("Failed to execute cursor command: ${ex.message}", ex) - com.intellij.openapi.ui.Messages.showErrorDialog( - project, - """ - ${ex.message} + if (settings.openProjectWithFile) { + // Command to open project + val projectPath = project.basePath ?: return + val projectCommand = when { + System.getProperty("os.name").lowercase().contains("mac") -> { + arrayOf("open", "-a", "$cursorPath", projectPath) + } + System.getProperty("os.name").lowercase().contains("windows") -> { + arrayOf("cmd", "/c", "$cursorPath", projectPath) + } + else -> { + arrayOf(cursorPath, projectPath) + } + } + + try { + logger.info("Executing project command: ${projectCommand.joinToString(" ")}") + ProcessBuilder(*projectCommand).start() + + // Give some time for the project to open, then open the file and position the cursor + Thread.sleep(1000) - Please check: - 1. Cursor path is correctly configured in Settings > Tools > Switch2Cursor - 2. Cursor is properly installed on your system - 3. The configured path points to a valid Cursor executable - """.trimIndent(), - "Error" - ) + // Then open the file and position the cursor + logger.info("Executing file command: ${fileCommand.joinToString(" ")}") + ProcessBuilder(*fileCommand).start() + } catch (ex: Exception) { + logger.error("Failed to execute cursor command: ${ex.message}", ex) + showErrorDialog(project, ex) + return + } + } else { + // Only open the file and position the cursor + try { + logger.info("Executing file command: ${fileCommand.joinToString(" ")}") + ProcessBuilder(*fileCommand).start() + } catch (ex: Exception) { + logger.error("Failed to execute cursor command: ${ex.message}", ex) + showErrorDialog(project, ex) + return + } } WindowUtils.activeWindow() } + + private fun showErrorDialog(project: Project, ex: Exception) { + com.intellij.openapi.ui.Messages.showErrorDialog( + project, + """ + ${ex.message} + + Please check: + 1. Cursor path is correctly configured in Settings > Tools > Switch2Cursor + 2. Cursor is properly installed on your system + 3. The configured path points to a valid Cursor executable + """.trimIndent(), + "Error" + ) + } override fun update(e: AnActionEvent) { val project = e.project diff --git a/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsConfigurable.kt b/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsConfigurable.kt index f9612a7..7bd268b 100644 --- a/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsConfigurable.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.options.Configurable import javax.swing.JComponent import javax.swing.JPanel import javax.swing.JTextField +import javax.swing.JCheckBox import com.intellij.ui.components.JBLabel import com.intellij.util.ui.FormBuilder @@ -19,17 +20,20 @@ class AppSettingsConfigurable : Configurable { override fun isModified(): Boolean { val settings = AppSettingsState.getInstance() - return mySettingsComponent!!.cursorPath != settings.cursorPath + return mySettingsComponent!!.cursorPath != settings.cursorPath || + mySettingsComponent!!.openProjectWithFile != settings.openProjectWithFile } override fun apply() { val settings = AppSettingsState.getInstance() settings.cursorPath = mySettingsComponent!!.cursorPath + settings.openProjectWithFile = mySettingsComponent!!.openProjectWithFile } override fun reset() { val settings = AppSettingsState.getInstance() mySettingsComponent!!.cursorPath = settings.cursorPath + mySettingsComponent!!.openProjectWithFile = settings.openProjectWithFile } override fun disposeUIResources() { @@ -40,10 +44,12 @@ class AppSettingsConfigurable : Configurable { class AppSettingsComponent { val panel: JPanel private val cursorPathText = JTextField() + private val openProjectWithFileCheckBox = JCheckBox("Also open project when using Option+Shift+O") init { panel = FormBuilder.createFormBuilder() .addLabeledComponent(JBLabel("Cursor Path: "), cursorPathText, 1, false) + .addComponent(openProjectWithFileCheckBox, 1) .addComponentFillVertically(JPanel(), 0) .panel } @@ -53,4 +59,10 @@ class AppSettingsComponent { set(value) { cursorPathText.text = value } + + var openProjectWithFile: Boolean + get() = openProjectWithFileCheckBox.isSelected + set(value) { + openProjectWithFileCheckBox.isSelected = value + } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsState.kt b/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsState.kt index e1fa4d0..5b0b04b 100644 --- a/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsState.kt +++ b/src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsState.kt @@ -12,6 +12,7 @@ import com.intellij.util.xmlb.XmlSerializerUtil ) class AppSettingsState : PersistentStateComponent { var cursorPath: String = "cursor" + var openProjectWithFile: Boolean = true override fun getState(): AppSettingsState = this diff --git a/switch2cursor_explanation.md b/switch2cursor_explanation.md new file mode 100644 index 0000000..6e635cf --- /dev/null +++ b/switch2cursor_explanation.md @@ -0,0 +1,434 @@ +# Switch2Cursor 插件工作原理详解 + +> 本文档详细解释了Switch2Cursor插件是如何运作的,以及我们对其进行的修改 + +## 插件功能概述 + +Switch2Cursor是一个JetBrains IDE插件,主要功能是: +- 在JetBrains IDE和Cursor编辑器之间无缝切换 +- 精确保持光标位置(行号和列号) +- 支持在Cursor中打开整个项目 + +## 插件逻辑工作流程 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌────────────────────┐ +│ 用户触发动作 │ │ 获取项目和文件 │ │ 获取光标位置信息 │ +│ (快捷键/菜单) │────>│ 相关信息 │────>│ (行号和列号) │ +└─────────────────┘ └─────────────────┘ └────────────────────┘ + │ + ▼ +┌─────────────────┐ ┌─────────────────┐ ┌────────────────────┐ +│ 在Cursor中 │ │ 构建相应的 │ │ 获取用户设置 │ +│ 打开项目/文件 │<────│ 系统命令 │<────│ (配置选项) │ +└─────────────────┘ └─────────────────┘ └────────────────────┘ +``` + +插件的核心工作流程包括: + +1. 用户通过快捷键或菜单触发动作 +2. 插件获取当前项目、文件和光标位置信息 +3. 根据用户设置和触发类型,执行相应命令 +4. 在Cursor中打开项目或文件,并定位到相应位置 + +## 主要组件和功能逻辑 + +### 1. 设置组件 + +**路径**: `src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsState.kt` + +```kotlin +@State( + name = "com.github.qczone.switch2cursor.settings.AppSettingsState", + storages = [Storage("Switch2CursorSettings.xml")] +) +class AppSettingsState : PersistentStateComponent { + var cursorPath: String = "cursor" + var openProjectWithFile: Boolean = true + + override fun getState(): AppSettingsState = this + + override fun loadState(state: AppSettingsState) { + XmlSerializerUtil.copyBean(state, this) + } + + companion object { + fun getInstance(): AppSettingsState = ApplicationManager.getApplication().getService(AppSettingsState::class.java) + } +} +``` + +**功能**: +- 存储插件配置(Cursor路径和是否同时打开项目和文件) +- 使用`PersistentStateComponent`保证配置持久化 +- 通过单例模式提供全局访问点 + +### 2. 设置界面 + +**路径**: `src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsConfigurable.kt` + +```kotlin +class AppSettingsComponent { + val panel: JPanel + private val cursorPathText = JTextField() + private val openProjectWithFileCheckBox = JCheckBox("Option+Shift+O 时同时打开项目和文件") + + init { + panel = FormBuilder.createFormBuilder() + .addLabeledComponent(JBLabel("Cursor Path: "), cursorPathText, 1, false) + .addComponent(openProjectWithFileCheckBox, 1) + .addComponentFillVertically(JPanel(), 0) + .panel + } + + var cursorPath: String + get() = cursorPathText.text + set(value) { + cursorPathText.text = value + } + + var openProjectWithFile: Boolean + get() = openProjectWithFileCheckBox.isSelected + set(value) { + openProjectWithFileCheckBox.isSelected = value + } +} +``` + +**功能**: +- 创建设置界面组件 +- 提供UI元素让用户配置Cursor路径 +- 提供复选框控制Option+Shift+O的行为 + +### 3. 打开文件动作 + +**路径**: `src/main/kotlin/com/github/qczone/switch2cursor/actions/OpenFileInCursorAction.kt` + +```kotlin +class OpenFileInCursorAction : AnAction() { + // ... + override fun actionPerformed(e: AnActionEvent) { + val project: Project = e.project ?: return + val virtualFile: VirtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return + + val editor: Editor? = e.getData(CommonDataKeys.EDITOR) + + val line = editor?.caretModel?.logicalPosition?.line?.plus(1) ?: 1 + val column = editor?.caretModel?.logicalPosition?.column?.plus(1) ?: 1 + + val filePath = virtualFile.path + val settings = AppSettingsState.getInstance() + val cursorPath = settings.cursorPath + + // Command to open file and position cursor + val fileCommand = when { + System.getProperty("os.name").lowercase().contains("mac") -> { + arrayOf("open", "-a", "$cursorPath", "cursor://file$filePath:$line:$column") + } + System.getProperty("os.name").lowercase().contains("windows") -> { + arrayOf("cmd", "/c", "$cursorPath", "--goto", "$filePath:$line:$column") + } + else -> { + arrayOf(cursorPath, "--goto", "$filePath:$line:$column") + } + } + + if (settings.openProjectWithFile) { + // Command to open project + val projectPath = project.basePath ?: return + val projectCommand = when { + // ...系统相关命令构建代码 + } + + try { + // 先打开项目 + ProcessBuilder(*projectCommand).start() + + // Give some time for the project to open, then open the file and position the cursor + Thread.sleep(1000) + + // Then open the file and position the cursor + ProcessBuilder(*fileCommand).start() + } catch (ex: Exception) { + // 错误处理 + } + } else { + // Only open the file and position the cursor + try { + ProcessBuilder(*fileCommand).start() + } catch (ex: Exception) { + // 错误处理 + } + } + + WindowUtils.activeWindow() + } + // ... +} +``` + +**功能**: +- 响应Option+Shift+O快捷键或菜单点击 +- 获取当前文件路径和光标位置 +- 根据设置决定是只打开文件还是同时打开项目和文件 +- 执行相应的命令在Cursor中打开文件/项目 + +### 4. 打开项目动作 + +**路径**: `src/main/kotlin/com/github/qczone/switch2cursor/actions/OpenProjectInCursorAction.kt` + +```kotlin +class OpenProjectInCursorAction : AnAction() { + // ... + override fun actionPerformed(e: AnActionEvent) { + val project: Project = e.project ?: return + val projectPath = project.basePath ?: return + + val settings = AppSettingsState.getInstance() + val cursorPath = settings.cursorPath + + val command = when { + System.getProperty("os.name").lowercase().contains("mac") -> { + arrayOf("open", "-a", "$cursorPath", projectPath) + } + // 其他系统的命令 + } + try { + ProcessBuilder(*command).start() + } catch (ex: Exception) { + // 错误处理 + } + + WindowUtils.activeWindow() + } + // ... +} +``` + +**功能**: +- 响应Option+Shift+P快捷键或菜单点击 +- 获取当前项目路径 +- 执行命令在Cursor中打开项目 + +## 标注处理(Annotation Processing) + +### 什么是标注处理 + +标注处理(Annotation Processing)是Java编译器的一个功能,允许在编译期间处理Java代码中的注解(Annotations)。处理器可以读取、修改、生成Java代码。 + +### 在此项目中的应用 + +在Switch2Cursor项目中,主要使用了IntelliJ Platform提供的注解如`@State`和`@Storage`: + +```kotlin +@State( + name = "com.github.qczone.switch2cursor.settings.AppSettingsState", + storages = [Storage("Switch2CursorSettings.xml")] +) +``` + +这些注解告诉IntelliJ平台: +- 这个类是一个应用状态组件 +- 它的状态应该存储在特定的XML文件中 +- 平台应该自动处理状态的保存和加载 + +IntelliJ平台在编译和运行时会处理这些注解,自动生成必要的代码来支持配置的持久化。 + +## 我们的代码修改 + +### 修改概述 + +我们对插件进行了功能增强,添加了一个新选项,允许用户配置Option+Shift+O的行为: +- 勾选选项时:同时打开项目和文件(并定位光标) +- 未勾选时:只打开文件(并定位光标) + +### 修改详情 + +#### 1. 添加设置选项 + +**文件**: `src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsState.kt` + +```kotlin +// 修改前 +class AppSettingsState : PersistentStateComponent { + var cursorPath: String = "cursor" + // ... +} + +// 修改后 +class AppSettingsState : PersistentStateComponent { + var cursorPath: String = "cursor" + var openProjectWithFile: Boolean = false + // ... +} +``` + +**修改说明**: 添加了`openProjectWithFile`配置项,默认为false,用于控制Option+Shift+O的行为。 + +#### 2. 更新设置界面 + +**文件**: `src/main/kotlin/com/github/qczone/switch2cursor/settings/AppSettingsConfigurable.kt` + +```kotlin +// 修改前 +class AppSettingsComponent { + // ... + private val cursorPathText = JTextField() + + init { + panel = FormBuilder.createFormBuilder() + .addLabeledComponent(JBLabel("Cursor Path: "), cursorPathText, 1, false) + .addComponentFillVertically(JPanel(), 0) + .panel + } + // ... +} + +// 修改后 +class AppSettingsComponent { + // ... + private val cursorPathText = JTextField() + private val openProjectWithFileCheckBox = JCheckBox("Option+Shift+O 时同时打开项目和文件") + + init { + panel = FormBuilder.createFormBuilder() + .addLabeledComponent(JBLabel("Cursor Path: "), cursorPathText, 1, false) + .addComponent(openProjectWithFileCheckBox, 1) + .addComponentFillVertically(JPanel(), 0) + .panel + } + + // 添加了新属性的getter/setter + var openProjectWithFile: Boolean + get() = openProjectWithFileCheckBox.isSelected + set(value) { + openProjectWithFileCheckBox.isSelected = value + } + // ... +} +``` + +**修改说明**: 添加了复选框UI组件,并更新了相应的配置读写逻辑。 + +#### 3. 修改文件打开行为 + +**文件**: `src/main/kotlin/com/github/qczone/switch2cursor/actions/OpenFileInCursorAction.kt` + +```kotlin +// 修改前 +override fun actionPerformed(e: AnActionEvent) { + // ... + val command = when { + // 构建打开文件的命令 + } + + try { + ProcessBuilder(*command).start() + } catch (ex: Exception) { + // 错误处理 + } + // ... +} + +// 修改后 +override fun actionPerformed(e: AnActionEvent) { + // ... + // Command to open file and position cursor + val fileCommand = when { + // 构建打开文件的命令 + } + + if (settings.openProjectWithFile) { + // Command to open project + val projectCommand = when { + // 构建打开项目的命令 + } + + try { + // 先打开项目 + ProcessBuilder(*projectCommand).start() + + // Give some time for the project to open, then open the file and position the cursor + Thread.sleep(1000) + + // Then open the file and position the cursor + ProcessBuilder(*fileCommand).start() + } catch (ex: Exception) { + // 错误处理 + } + } else { + // Only open the file and position the cursor + try { + ProcessBuilder(*fileCommand).start() + } catch (ex: Exception) { + // 错误处理 + } + } + // ... +} +``` + +**修改说明**: +- 重构了代码逻辑,根据`openProjectWithFile`设置决定行为 +- 当设置为true时,先打开项目,等待1秒,再打开并定位文件 +- 当设置为false时,仅打开并定位文件 + +### 修改前后对比 + +``` +┌──────────────────────────────────────────┐ +│ 修改前 │ +└──────────────────────────────────────────┘ + ▲ + │ +┌──────────────────────────────────────────┐ +│ 设置界面: │ +│ ┌────────────────────────────────────┐ │ +│ │ Cursor Path: [____________] │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ 按下 Option+Shift+O: │ +│ -> 只打开文件并定位光标 │ +│ │ +│ 按下 Option+Shift+P: │ +│ -> 只打开项目 │ +└──────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────┐ +│ 修改后 │ +└──────────────────────────────────────────┘ + ▲ + │ +┌──────────────────────────────────────────┐ +│ 设置界面: │ +│ ┌────────────────────────────────────┐ │ +│ │ Cursor Path: [____________] │ │ +│ └────────────────────────────────────┘ │ +│ [✓] Option+Shift+O 时同时打开项目和文件 │ +│ │ +│ 按下 Option+Shift+O (勾选): │ +│ -> 先打开项目 │ +│ -> 再打开文件并定位光标 │ +│ │ +│ 按下 Option+Shift+O (未勾选): │ +│ -> 只打开文件并定位光标 │ +└──────────────────────────────────────────┘ +``` + +#### 功能对比 + +| 功能 | 修改前 | 修改后 | +|------|--------|--------| +| Option+Shift+O | 只能打开文件并定位光标 | 可配置是否同时打开项目和文件 | +| 设置选项 | 只有Cursor路径设置 | 增加了"同时打开项目和文件"选项 | +| 打开项目和文件 | 需要分别使用两个快捷键 | 可以使用一个快捷键完成 | + +## 总结 + +这次修改增强了插件的灵活性,使用户可以根据自己的工作流程需求自定义Option+Shift+O的行为。关键改进包括: + +1. 添加了新的配置项以支持灵活的行为选择 +2. 扩展了设置界面使用户可以方便地配置选项 +3. 重构了代码逻辑以支持新的功能 +4. 当用户选择同时打开项目和文件时,确保了先打开项目,再打开并定位文件的正确顺序 \ No newline at end of file