diff --git a/README.md b/README.md index 217c231..3983738 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ + **Angular File Switcher** is a plugin that switches between similarly named files within a directory. It is specifically designed for Angular component files but works with any set of files within a directory where file names ( except for the file extensions) match. @@ -6,13 +7,54 @@ except for the file extensions) match. I built this as a reaction to Angular CLI QuickSwitch Plugin. While I find it a valuable plugin, it doesn't function _exactly_ how I want in all circumstances. It has the following enhancements: -* Choose file extensions to include or exclude. -* This plugin works with multiple tab groups how one would expect. -* It doesn't unexpectedly rearrange the tab order. -* Optionally keep files open - by tab group or workspace. -* If you open a similarly named file using a non-file-switcher command (such as Ctrl+Click), you can optionally close +- Choose file extensions to include or exclude. +- This plugin works with multiple tab groups how one would expect. +- It doesn't unexpectedly rearrange the tab order. +- Optionally keep files open - by tab group or workspace. +- If you open a similarly named file using a non-file-switcher command (such as Ctrl+Click), you can optionally close other similarly named files. +## Features + +- **Cycle Between Files**: Press `Alt+S` to cycle through all component files (TypeScript, HTML, CSS, etc.) +- **Direct File Access**: Jump directly to specific file types: + - `Alt+T` - TypeScript file (.ts) + - `Alt+H` - HTML template file (.html) + - `Alt+C` - CSS/Style file (.scss, .css) + - `Alt+P` - Test file (.spec.ts) + +## Configuration + +### File Extensions + +Configure which file extensions to include when switching: + +1. Open **Settings/Preferences** (`Ctrl+Alt+S` or `⌘,`) +2. Navigate to **Tools > Angular File Switcher** +3. Customize the file extension types: + - **Class**: TypeScript/JavaScript files (.ts, .js) + - **Template**: HTML and other template files (.html, .php, etc.) + - **Style**: CSS and other stylesheet files (.css, .scss, etc.) + - **Test**: Test files (.spec.ts, .spec.js, etc.) + +### Customizing Keyboard Shortcuts + +To customize the keyboard shortcuts: + +1. Open **Settings/Preferences** (`Ctrl+Alt+S` or `⌘,`) +2. Navigate to **Keymap** +3. Search for "Angular File Switcher" to see all available actions +4. Right-click on an action and select **Add Keyboard Shortcut** to set your preferred keys +5. To remove the default shortcut, right-click again and select **Remove Shortcut** + +Available actions: + +- **Open Next Similarly Named File**: Cycle through all matching files (Default: `Alt+S`) +- **Open TypeScript File**: Jump directly to TypeScript file (Default: `Alt+T`) +- **Open HTML Template File**: Jump directly to HTML file (Default: `Alt+H`) +- **Open CSS/Style File**: Jump directly to CSS/style file (Default: `Alt+C`) +- **Open Test File**: Jump directly to test file (Default: `Alt+P`) + ## Links Source code: https://github.com/jyjor/angular-file-switcher diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/CssFileSwitchAction.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/CssFileSwitchAction.kt new file mode 100644 index 0000000..f520d7d --- /dev/null +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/CssFileSwitchAction.kt @@ -0,0 +1,14 @@ +package com.jaredrobertson.plugins.angularFileSwitcher + +import com.jaredrobertson.plugins.angularFileSwitcher.settings.AppSettingsState + +class CssFileSwitchAction : TypedFileSwitchAction("Open CSS/Style File") { + override fun getTargetExtension(): String { + // 從AppSettingsState獲取第一個CSS擴展名 + val extensions = AppSettingsState.instance.styleFileExtensions.trim().split(" +".toRegex()) + // 優先使用scss,因為Angular常用這種擴展名 + return extensions.firstOrNull { it.contains(".scss") } + ?: extensions.firstOrNull { it.contains(".css") } + ?: ".scss" + } +} \ No newline at end of file diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/HtmlFileSwitchAction.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/HtmlFileSwitchAction.kt new file mode 100644 index 0000000..369fe1f --- /dev/null +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/HtmlFileSwitchAction.kt @@ -0,0 +1,11 @@ +package com.jaredrobertson.plugins.angularFileSwitcher + +import com.jaredrobertson.plugins.angularFileSwitcher.settings.AppSettingsState + +class HtmlFileSwitchAction : TypedFileSwitchAction("Open HTML Template File") { + override fun getTargetExtension(): String { + // 從AppSettingsState獲取第一個HTML擴展名 + val extensions = AppSettingsState.instance.templateFileExtensions.trim().split(" +".toRegex()) + return extensions.firstOrNull { it.contains(".html") } ?: ".html" + } +} \ No newline at end of file diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/Shared.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/Shared.kt index 87f3b07..c8f725c 100644 --- a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/Shared.kt +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/Shared.kt @@ -40,7 +40,7 @@ object Shared { return files } - private val allExtensions: Array + val allExtensions: Array get() { val settings = instance return (settings.classFileExtensions.trim { it <= ' ' } + " " + @@ -62,7 +62,7 @@ object Shared { return if (file == null || !file.exists()) null else file } - private fun getExtensionIndex(path: String, extensions: Array): Int { + fun getExtensionIndex(path: String, extensions: Array): Int { var extension = "" var index = -1 for (i in extensions.indices) { @@ -78,7 +78,7 @@ object Shared { return index } - private fun getBasePath(path: String, extensionIndex: Int, extensions: Array): String? { + fun getBasePath(path: String, extensionIndex: Int, extensions: Array): String? { if (extensionIndex == -1) return null val extensionLength = extensions[extensionIndex].length val baseLength = path.length - extensionLength diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TestFileSwitchAction.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TestFileSwitchAction.kt new file mode 100644 index 0000000..00b1a34 --- /dev/null +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TestFileSwitchAction.kt @@ -0,0 +1,11 @@ +package com.jaredrobertson.plugins.angularFileSwitcher + +import com.jaredrobertson.plugins.angularFileSwitcher.settings.AppSettingsState + +class TestFileSwitchAction : TypedFileSwitchAction("Open Test File") { + override fun getTargetExtension(): String { + // 從AppSettingsState獲取第一個測試擴展名 + val extensions = AppSettingsState.instance.testFileExtensions.trim().split(" +".toRegex()) + return extensions.firstOrNull { it.contains(".spec.ts") } ?: ".spec.ts" + } +} \ No newline at end of file diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TsFileSwitchAction.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TsFileSwitchAction.kt new file mode 100644 index 0000000..858e7f7 --- /dev/null +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TsFileSwitchAction.kt @@ -0,0 +1,11 @@ +package com.jaredrobertson.plugins.angularFileSwitcher + +import com.jaredrobertson.plugins.angularFileSwitcher.settings.AppSettingsState + +class TsFileSwitchAction : TypedFileSwitchAction("Open TypeScript File") { + override fun getTargetExtension(): String { + // 從AppSettingsState獲取第一個TS擴展名 + val extensions = AppSettingsState.instance.classFileExtensions.trim().split(" +".toRegex()) + return extensions.firstOrNull { it.contains(".ts") && !it.contains(".spec.ts") && !it.contains(".test.ts") } ?: ".ts" + } +} \ No newline at end of file diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TypedFileSwitchAction.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TypedFileSwitchAction.kt new file mode 100644 index 0000000..7cefca4 --- /dev/null +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/TypedFileSwitchAction.kt @@ -0,0 +1,96 @@ +package com.jaredrobertson.plugins.angularFileSwitcher + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx +import com.intellij.openapi.fileEditor.impl.EditorWindow +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.jaredrobertson.plugins.angularFileSwitcher.models.CloseBehavior +import com.jaredrobertson.plugins.angularFileSwitcher.models.Grouping +import com.jaredrobertson.plugins.angularFileSwitcher.settings.AppSettingsState.Companion.instance +import java.util.function.Consumer +import java.util.stream.Collectors + +abstract class TypedFileSwitchAction(actionName: String) : AnAction(actionName) { + + protected abstract fun getTargetExtension(): String + + override fun actionPerformed(event: AnActionEvent) { + val dataContext = event.dataContext + val currentFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext) ?: return + val currentPath = currentFile.canonicalPath ?: return + + val targetExtension = getTargetExtension() + val newPath = getPathWithExtension(currentPath, targetExtension) ?: return + + if (currentPath == newPath) return + val newFile = LocalFileSystem.getInstance().findFileByPath(newPath) + if (newFile == null || !newFile.exists()) return + val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return + + if (instance.grouping === Grouping.EVERYWHERE) { + switchFileEverywhere(project, newFile) + } else { + switchFileInEditorGroup(dataContext, project, newFile) + } + } + + private fun getPathWithExtension(currentPath: String, targetExtension: String): String? { + val extensions = Shared.allExtensions + val pathExtensionIndex = Shared.getExtensionIndex(currentPath, extensions) + if (pathExtensionIndex == -1) return null + + val basePath = Shared.getBasePath(currentPath, pathExtensionIndex, extensions) + val targetPath = basePath + targetExtension + + val targetFile = LocalFileSystem.getInstance().findFileByPath(targetPath) + return if (targetFile != null && targetFile.exists()) targetPath else null + } + + private fun switchFileEverywhere(project: Project, newFile: VirtualFile) { + OpenFileDescriptor(project, newFile).navigate(true) + if (instance.closeBehavior === CloseBehavior.ONLY_ON_ACTION) { + val fileEditorManager = FileEditorManager.getInstance(project) + val path = newFile.canonicalPath ?: return + val otherFiles = Shared.getOtherFiles(path) + val otherOpenFiles = otherFiles.stream() + .filter { file: VirtualFile? -> fileEditorManager.isFileOpen(file!!) } + .collect(Collectors.toList()) + otherOpenFiles.forEach(Consumer { file: VirtualFile? -> + fileEditorManager.closeFile( + file!! + ) + }) + } + } + + private fun switchFileInEditorGroup( + dataContext: DataContext, + project: Project, + newFile: VirtualFile + ) { + val window = EditorWindow.DATA_KEY.getData(dataContext) + ?: return + FileEditorManagerEx.getInstanceEx(project).openFileWithProviders(newFile, true, window) + if (instance.closeBehavior === CloseBehavior.ONLY_ON_ACTION) { + val path = newFile.canonicalPath ?: return + val otherFiles = Shared.getOtherFiles(path) + val otherOpenFiles = otherFiles.stream().filter { file: VirtualFile? -> + window.isFileOpen( + file!! + ) + }.collect(Collectors.toList()) + otherOpenFiles.forEach(Consumer { file: VirtualFile? -> + window.closeFile( + file!! + ) + }) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsComponent.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsComponent.kt index e3d4efe..34c913b 100644 --- a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsComponent.kt +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsComponent.kt @@ -3,10 +3,13 @@ package com.jaredrobertson.plugins.angularFileSwitcher.settings import com.intellij.openapi.ui.ComboBox import com.intellij.ui.IdeBorderFactory +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextField import com.intellij.util.ui.FormBuilder +import com.intellij.util.ui.UIUtil import com.jaredrobertson.plugins.angularFileSwitcher.models.CloseBehavior import com.jaredrobertson.plugins.angularFileSwitcher.models.Grouping +import java.awt.Font import javax.swing.JComponent import javax.swing.JPanel @@ -30,6 +33,7 @@ class AppSettingsComponent { .addLabeledComponent("Test: ", myTestFileExtensionsText, 5, false) .panel fileExtensionTypePanel.border = IdeBorderFactory.createTitledBorder("File Extension Types") + val otherSettingsPanel = FormBuilder.createFormBuilder() .addLabeledComponent( "Open and close same component files: ", @@ -40,13 +44,36 @@ class AppSettingsComponent { .addLabeledComponent("Close other component files: ", myCloseBehaviorCombo, 5, false) .panel otherSettingsPanel.border = IdeBorderFactory.createTitledBorder("Other Settings") + + // 添加快捷鍵設定提示 + val shortcutInfoPanel = FormBuilder.createFormBuilder() + .addComponent(createKeymapInfoPanel()) + .panel + shortcutInfoPanel.border = IdeBorderFactory.createTitledBorder("Keyboard Shortcuts") + panel = FormBuilder.createFormBuilder() .addComponent(fileExtensionTypePanel) .addComponent(otherSettingsPanel) + .addComponent(shortcutInfoPanel) .addComponentFillVertically(JPanel(), 0) .panel } + // 創建快捷鍵信息面板 + private fun createKeymapInfoPanel(): JPanel { + val infoLabel1 = JBLabel("To configure keyboard shortcuts, please use the IDE's Keymap settings.") + val infoLabel2 = JBLabel("Go to Settings > Keymap and search for 'Angular File Switcher'.") + + // 設定字體風格 + infoLabel1.font = UIUtil.getLabelFont().deriveFont(Font.PLAIN) + infoLabel2.font = UIUtil.getLabelFont().deriveFont(Font.PLAIN) + + return FormBuilder.createFormBuilder() + .addComponent(infoLabel1) + .addComponent(infoLabel2) + .panel + } + val preferredFocusedComponent: JComponent get() = myClassFileExtensionsText var classFileExtensionsText: String diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsConfigurable.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsConfigurable.kt index bcec8fb..4fc6151 100644 --- a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsConfigurable.kt +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsConfigurable.kt @@ -35,6 +35,7 @@ class AppSettingsConfigurable : Configurable { modified or (mySettingsComponent!!.testFileExtensionsText != settings.testFileExtensions) modified = modified or (mySettingsComponent!!.switcherGrouping !== settings.grouping) modified = modified or (mySettingsComponent!!.closeBehavior !== settings.closeBehavior) + return modified } diff --git a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsState.kt b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsState.kt index 469a5a0..fbcf0f5 100644 --- a/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsState.kt +++ b/src/main/java/com/jaredrobertson/plugins/angularFileSwitcher/settings/AppSettingsState.kt @@ -36,6 +36,7 @@ class AppSettingsState : PersistentStateComponent { @JvmField var closeBehavior = DEFAULT_CLOSE_BEHAVIOR + override fun getState(): AppSettingsState { return this } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7e1faec..071503b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -22,10 +22,41 @@ id="angular.QuickSwitch" class="com.jaredrobertson.plugins.angularFileSwitcher.FileSwitchAction" text="Open Next Similarly Named File" - description="Switch between corresponding Angular class, component, and style files" - > + description="Switch between corresponding Angular class, component, and style files"> + + + + + + + + + + + + + + + +