/* * Copyright 2003-2023 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at * https://opensource.org/licenses/MIT. */ package com.maddyhome.idea.vim.group import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.impl.EditorsSplitters import com.intellij.openapi.fileTypes.FileTypeManager import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.psi.search.FilenameIndex import com.intellij.psi.search.ProjectScope import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimFileBase import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.group.LastTabService.Companion.getInstance import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.MessageHelper.message import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.execute import com.maddyhome.idea.vim.newapi.globalIjOptions import java.io.File import java.util.* class FileGroup : VimFileBase() { override fun openFile(filename: String, context: ExecutionContext): Boolean { if (logger.isDebugEnabled) { logger.debug("openFile($filename)") } val project = PlatformDataKeys.PROJECT.getData((context as IjEditorExecutionContext).context) ?: return false // API change - don't merge val found = findFile(filename, project) if (found != null) { if (logger.isDebugEnabled) { logger.debug("found file: $found") } // Can't open a file unless it has a known file type. The next call will return the known type. // If unknown, IDEA will prompt the user to pick a type. val type = FileTypeManager.getInstance().getKnownFileTypeOrAssociate(found, project) if (type != null) { val fem = FileEditorManager.getInstance(project) fem.openFile(found, true) return true } else { // There was no type and user didn't pick one. Don't open the file // Return true here because we found the file but the user canceled by not picking a type. return true } } else { VimPlugin.showMessage(message("unable.to.find.0", filename)) return false } } fun findFile(filename: String, project: Project): VirtualFile? { var found: VirtualFile? // Vim supports both ~/ and ~\ (tested on Mac and Windows). On Windows, it supports forward- and back-slashes, but // it only supports forward slash on Unix (tested on Mac) // VFS works with both directory separators (tested on Mac and Windows) if (filename.startsWith("~/") || filename.startsWith("~\\")) { val relativePath = filename.substring(2) val dir = System.getProperty("user.home") if (logger.isDebugEnabled) { logger.debug("home dir file") logger.debug("looking for $relativePath in $dir") } found = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(File(dir, relativePath)) } else { found = LocalFileSystem.getInstance().findFileByIoFile(File(filename)) if (found == null) { found = findByNameInContentRoots(filename, project) if (found == null) { found = findByNameInProject(filename, project) } } } return found } private fun findByNameInContentRoots(filename: String, project: Project): VirtualFile? { var found: VirtualFile? = null val prm = ProjectRootManager.getInstance(project) val roots = prm.contentRoots for (i in roots.indices) { if (logger.isDebugEnabled) { logger.debug("root[" + i + "] = " + roots[i].path) } found = roots[i].findFileByRelativePath(filename) if (found != null) { break } } return found } /** * Closes the current editor. */ override fun closeFile(editor: VimEditor, context: ExecutionContext) { val project = PlatformDataKeys.PROJECT.getData((context.context as DataContext)) if (project != null) { val fileEditorManager = FileEditorManagerEx.getInstanceEx(project) val window = fileEditorManager.currentWindow val virtualFile = fileEditorManager.currentFile if (virtualFile != null && window != null) { // During the work on VIM-2912 I've changed the close function to this one. // However, the function with manager seems to work weirdly and it causes VIM-2953 //window.getManager().closeFile(virtualFile, true, false); window.closeFile(virtualFile) // Get focus after closing tab window.requestFocus(true) if (!ApplicationManager.getApplication().isUnitTestMode) { // This thing doesn't have an implementation in test mode EditorsSplitters.focusDefaultComponentInSplittersIfPresent(project) } } } } /** * Closes editor. */ override fun closeFile(number: Int, context: ExecutionContext) { val project = PlatformDataKeys.PROJECT.getData((context as IjEditorExecutionContext).context) ?: return val fileEditorManager = FileEditorManagerEx.getInstanceEx(project) val window = fileEditorManager.currentWindow val editors = fileEditorManager.openFiles if (window != null) { if (number >= 0 && number < editors.size) { fileEditorManager.closeFile(editors[number], window) } } if (!ApplicationManager.getApplication().isUnitTestMode) { // This thing doesn't have an implementation in test mode EditorsSplitters.focusDefaultComponentInSplittersIfPresent(project) } } /** * Saves specific file in the project. */ override fun saveFile(editor: VimEditor, context: ExecutionContext) { val action = if (injector.globalIjOptions().ideawrite.contains(IjOptionConstants.ideawrite_all)) { injector.nativeActionManager.saveAll } else { injector.nativeActionManager.saveCurrent } action.execute(editor, context) } /** * Saves all files in the project. */ override fun saveFiles(editor: VimEditor, context: ExecutionContext) { injector.nativeActionManager.saveAll.execute(editor, context) } /** * Selects then next or previous editor. */ override fun selectFile(count: Int, context: ExecutionContext): Boolean { var count = count val project = PlatformDataKeys.PROJECT.getData((context as IjEditorExecutionContext).context) ?: return false val fem = FileEditorManager.getInstance(project) // API change - don't merge val editors = fem.openFiles if (count == 99) { count = editors.size - 1 } if (count < 0 || count >= editors.size) { return false } fem.openFile(editors[count], true) return true } /** * Selects then next or previous editor. */ override fun selectNextFile(count: Int, context: ExecutionContext) { val project = PlatformDataKeys.PROJECT.getData((context as IjEditorExecutionContext).context) ?: return val fem = FileEditorManager.getInstance(project) // API change - don't merge val editors = fem.openFiles val current = fem.selectedFiles[0] for (i in editors.indices) { if (editors[i] == current) { val pos = (i + (count % editors.size) + editors.size) % editors.size fem.openFile(editors[pos], true) } } } /** * Selects previous editor tab. */ override fun selectPreviousTab(context: ExecutionContext) { val project = PlatformDataKeys.PROJECT.getData((context.context as DataContext)) ?: return val vf = getInstance(project).lastTab if (vf != null && vf.isValid) { FileEditorManager.getInstance(project).openFile(vf, true) } else { VimPlugin.indicateError() } } /** * Returns the previous tab. */ fun getPreviousTab(context: DataContext): VirtualFile? { val project = PlatformDataKeys.PROJECT.getData(context) ?: return null val vf = getInstance(project).lastTab if (vf != null && vf.isValid) { return vf } return null } fun selectEditor(project: Project, file: VirtualFile): Editor? { val fMgr = FileEditorManager.getInstance(project) val feditors = fMgr.openFile(file, true) if (feditors.size > 0) { if (feditors[0] is TextEditor) { val editor = (feditors[0] as TextEditor).editor if (!editor.isDisposed) { return editor } } } return null } override fun displayFileInfo(vimEditor: VimEditor, fullPath: Boolean) { val editor = (vimEditor as IjVimEditor).editor val msg = StringBuilder() val vf = EditorHelper.getVirtualFile(editor) if (vf != null) { msg.append('"') if (fullPath) { msg.append(vf.path) } else { val project = editor.project if (project != null) { val root = ProjectRootManager.getInstance(project).fileIndex.getContentRootForFile(vf) if (root != null) { msg.append(vf.path.substring(root.path.length + 1)) } else { msg.append(vf.path) } } } msg.append("\" ") } else { msg.append("\"[No File]\" ") } val doc = editor.document if (!doc.isWritable) { msg.append("[RO] ") } else if (FileDocumentManager.getInstance().isDocumentUnsaved(doc)) { msg.append("[+] ") } val lline = editor.caretModel.logicalPosition.line val total = IjVimEditor(editor).lineCount() val pct = (lline.toFloat() / total.toFloat() * 100f + 0.5).toInt() msg.append("line ").append(lline + 1).append(" of ").append(total) msg.append(" --").append(pct).append("%-- ") val lp = editor.caretModel.logicalPosition val col = editor.caretModel.offset - doc.getLineStartOffset(lline) msg.append("col ").append(col + 1) if (col != lp.column) { msg.append("-").append(lp.column + 1) } VimPlugin.showMessage(msg.toString()) } override fun selectEditor(projectId: String, documentPath: String, protocol: String?): VimEditor? { val fileSystem = VirtualFileManager.getInstance().getFileSystem(protocol) ?: return null val virtualFile = fileSystem.findFileByPath(documentPath) ?: return null val project = Arrays.stream(ProjectManager.getInstance().openProjects) .filter { p: Project? -> injector.file.getProjectId(p!!) == projectId } .findFirst().orElseThrow() val editor = selectEditor(project, virtualFile) ?: return null return IjVimEditor(editor) } override fun getProjectId(project: Any): String { require(project is Project) return project.name + "-" + project.locationHash } companion object { private fun findByNameInProject(filename: String, project: Project): VirtualFile? { val projectScope = ProjectScope.getProjectScope(project) val names = FilenameIndex.getVirtualFilesByName(filename, projectScope) if (!names.isEmpty()) { return names.stream().findFirst().get() } return null } private val logger = Logger.getInstance( FileGroup::class.java.name ) /** * Respond to editor tab selection and remember the last used tab */ fun fileEditorManagerSelectionChangedCallback(event: FileEditorManagerEvent) { if (event.oldFile != null) { getInstance(event.manager.project).lastTab = event.oldFile } } } }