Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 47 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
<!-- Plugin description -->

**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.

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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object Shared {
return files
}

private val allExtensions: Array<String>
val allExtensions: Array<String>
get() {
val settings = instance
return (settings.classFileExtensions.trim { it <= ' ' } + " " +
Expand All @@ -62,7 +62,7 @@ object Shared {
return if (file == null || !file.exists()) null else file
}

private fun getExtensionIndex(path: String, extensions: Array<String>): Int {
fun getExtensionIndex(path: String, extensions: Array<String>): Int {
var extension = ""
var index = -1
for (i in extensions.indices) {
Expand All @@ -78,7 +78,7 @@ object Shared {
return index
}

private fun getBasePath(path: String, extensionIndex: Int, extensions: Array<String>): String? {
fun getBasePath(path: String, extensionIndex: Int, extensions: Array<String>): String? {
if (extensionIndex == -1) return null
val extensionLength = extensions[extensionIndex].length
val baseLength = path.length - extensionLength
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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!!
)
})
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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: ",
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class AppSettingsState : PersistentStateComponent<AppSettingsState?> {

@JvmField
var closeBehavior = DEFAULT_CLOSE_BEHAVIOR

override fun getState(): AppSettingsState {
return this
}
Expand Down
35 changes: 33 additions & 2 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<keyboard-shortcut first-keystroke="alt S" keymap="$default"/>
</action>

<action
id="angular.SwitchToTypeScript"
class="com.jaredrobertson.plugins.angularFileSwitcher.TsFileSwitchAction"
text="Open TypeScript File"
description="Switch to the TypeScript file of the Angular component">
<keyboard-shortcut first-keystroke="alt T" keymap="$default"/>
</action>

<action
id="angular.SwitchToHtml"
class="com.jaredrobertson.plugins.angularFileSwitcher.HtmlFileSwitchAction"
text="Open HTML Template File"
description="Switch to the HTML template file of the Angular component">
<keyboard-shortcut first-keystroke="alt H" keymap="$default"/>
</action>

<action
id="angular.SwitchToStyle"
class="com.jaredrobertson.plugins.angularFileSwitcher.CssFileSwitchAction"
text="Open CSS/Style File"
description="Switch to the CSS/Style file of the Angular component">
<keyboard-shortcut first-keystroke="alt C" keymap="$default"/>
</action>

<action
id="angular.SwitchToTest"
class="com.jaredrobertson.plugins.angularFileSwitcher.TestFileSwitchAction"
text="Open Test File"
description="Switch to the Test file of the Angular component">
<keyboard-shortcut first-keystroke="alt P" keymap="$default"/>
</action>
</actions>

<projectListeners>
Expand Down