From ac7bc5f3ea391e113f4845042e66827888a802cb Mon Sep 17 00:00:00 2001 From: Chaerim YEO Date: Mon, 23 Jul 2018 20:25:53 +0900 Subject: [PATCH] Initial commit --- .gitignore | 5 + .scalafmt.conf | 17 +++ LICENSE | 21 ++++ README.md | 32 +++++ resources/META-INF/plugin.xml | 44 +++++++ src/me/chaerim/yapf/Document.scala | 64 ++++++++++ src/me/chaerim/yapf/FormatAction.scala | 14 +++ .../chaerim/yapf/FormatOnSaveComponent.scala | 27 ++++ src/me/chaerim/yapf/Result.scala | 17 +++ src/me/chaerim/yapf/Settings.scala | 31 +++++ .../chaerim/yapf/SettingsConfigurable.scala | 28 +++++ src/me/chaerim/yapf/SettingsPanel.scala | 117 ++++++++++++++++++ src/me/chaerim/yapf/Util.scala | 49 ++++++++ yapf.iml | 14 +++ 14 files changed, 480 insertions(+) create mode 100644 .gitignore create mode 100755 .scalafmt.conf create mode 100644 LICENSE create mode 100644 README.md create mode 100755 resources/META-INF/plugin.xml create mode 100755 src/me/chaerim/yapf/Document.scala create mode 100755 src/me/chaerim/yapf/FormatAction.scala create mode 100755 src/me/chaerim/yapf/FormatOnSaveComponent.scala create mode 100755 src/me/chaerim/yapf/Result.scala create mode 100755 src/me/chaerim/yapf/Settings.scala create mode 100755 src/me/chaerim/yapf/SettingsConfigurable.scala create mode 100755 src/me/chaerim/yapf/SettingsPanel.scala create mode 100755 src/me/chaerim/yapf/Util.scala create mode 100755 yapf.iml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b65624d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.AppleDouble +.LSOverride +.idea +out diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100755 index 0000000..78b250e --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,17 @@ +style = defaultWithAlign +maxColumn = 120 +continuationIndent.callSite = 2 +continuationIndent.defnSite = 2 +assumeStandardLibraryStripMargin = true +spaces.inImportCurlyBraces = false +align.tokens = [ + { code = "<-", owner = "Enumerator.Generator" } + { code = "=>", owner = "Case" } + { code = "=" } + { code = "->", owner = "Term.ApplyInfix" } + { code = "," , owner = "Term" } + { code = "extends" , owner = "Term|Defn" } + "//" +] +rewrite.rules = [RedundantParens, PreferCurlyFors] + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..091b960 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Chaerim Yeo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5ccd09 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# yapf +[YAPF](https://github.com/google/yapf) plugin for Jetbrains IDEs. + +## Getting Started + +### Prerequisites +- You should install [YAPF](https://github.com/google/yapf) before using this plugin. +- You should know the path of YAPF executable. + +### Installing +- Find `YAPF` in `Preferences` > `Plugins` > `Browse Repositories' on your Jetbrains IDE. +- Install it! + +### Setting +You can set following settings in `Preferences` > `YAPF`. +- Format on save +- YAPF executable path (default: `/usr/local/bin/yapf`) +- YAPF style file name (default: `.style.yapf`) + +Note that this plugin passes style file to YAPF in the following order: +1. `PROJECT_ROOT/[style_file_name]` +2. `VERSION_CONTROL_SYSTEM_ROOT/[style_file_name]` +3. `PROJECT_ROOT/.style.yapf` +4. `VERSION_CONTROL_SYSTEM_ROOT/.style.yapf` +5. No style option + +## TODO +- Write unit tests + +## License +This project is licensed under the MIT License. + diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml new file mode 100755 index 0000000..ef9f8ea --- /dev/null +++ b/resources/META-INF/plugin.xml @@ -0,0 +1,44 @@ + + me.chaerim.yapf + yapf + 0.1 + Chaerim Yeo + + google/yapf plugin for Jetbrains IDEs. + ]]> + + + + + + + + + com.intellij.modules.lang + com.intellij.modules.platform + com.intellij.modules.vcs + + + + + + + + + + + + + + + + me.chaerim.yapf.FormatOnSaveComponent + + + diff --git a/src/me/chaerim/yapf/Document.scala b/src/me/chaerim/yapf/Document.scala new file mode 100755 index 0000000..309749d --- /dev/null +++ b/src/me/chaerim/yapf/Document.scala @@ -0,0 +1,64 @@ +package me.chaerim.yapf + +import java.nio.file.{Files, Paths} + +import com.intellij.notification.NotificationType +import com.intellij.openapi.editor.{Document => IdeaDocument} +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.{Project, ProjectManager} +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.vcsUtil.VcsUtil +import me.chaerim.yapf.Result.{AlreadyFormattedCode, NotFoundExecutable, UnformattableFile} +import me.chaerim.yapf.Util._ + +case class Document(document: IdeaDocument) { + private val virtualFileOfDocument: Option[VirtualFile] = Option(FileDocumentManager.getInstance.getFile(document)) + + private val projectOfDocument: Option[Project] = virtualFileOfDocument.flatMap { virtualFile => + ProjectManager.getInstance.getOpenProjects.find { project => + ProjectRootManager.getInstance(project).getFileIndex.isInContent(virtualFile) + } + } + + val settings: Option[Settings] = projectOfDocument.map(Settings(_)) + + val isFormattable: Boolean = virtualFileOfDocument.exists { virtualFile => + virtualFile.getFileType.getName.compareToIgnoreCase("Python") == 0 && + virtualFile.getExtension.compareToIgnoreCase("py") == 0 + } + + private def findExecutable: Either[Result, String] = + (for { + maybeExecutable <- List(settings.map(_.executablePath), Option(Settings.DefaultExecutablePath)).distinct + executable <- maybeExecutable + if Files.exists(Paths.get(executable)) + } yield executable).headOption.toRight(NotFoundExecutable) + + private def findConfigFile: Option[String] = { + (for { + project <- projectOfDocument.toList + maybeConfigFile <- List(settings.map(_.styleFileName), Option(Settings.DefaultStyleFileName)).distinct + maybeDirectory <- List(Option(project.getBasePath), + virtualFileOfDocument.map(VcsUtil.getVcsRootFor(project, _).getPath)).distinct + directory <- maybeDirectory + configFile <- maybeConfigFile + fullConfigFilePath = Paths.get(directory, configFile) + if Files.exists(fullConfigFilePath) + } yield fullConfigFilePath.toAbsolutePath.toString).headOption + } + + def format: Unit = + (for { + executable <- findExecutable + _ <- Either.cond(isFormattable, "", UnformattableFile) + configFile = findConfigFile + originalCode = document.getText + formattedCode <- runYapfCommand(executable, originalCode, configFile) + result <- Either.cond(originalCode != formattedCode, formattedCode, AlreadyFormattedCode) + } yield result) match { + case Right(formattedCode) => setFormattedCode(document, formattedCode) + case Left(result) if result.shouldNotify => + notifyMessage(s"${result.message}\n${result.detail.getOrElse("")}".stripMargin, NotificationType.ERROR) + } +} diff --git a/src/me/chaerim/yapf/FormatAction.scala b/src/me/chaerim/yapf/FormatAction.scala new file mode 100755 index 0000000..8736505 --- /dev/null +++ b/src/me/chaerim/yapf/FormatAction.scala @@ -0,0 +1,14 @@ +package me.chaerim.yapf + +import com.intellij.openapi.actionSystem.{AnAction, AnActionEvent, CommonDataKeys} +import com.intellij.openapi.fileEditor.FileEditorManager + +class FormatAction extends AnAction { + override def actionPerformed(event: AnActionEvent): Unit = + for { + currentProject <- Option(event.getData(CommonDataKeys.PROJECT)) + currentEditor <- Option(FileEditorManager.getInstance(currentProject).getSelectedTextEditor) + currentDocument <- Option(currentEditor.getDocument) + document <- Option(Document(currentDocument)) + } yield document.format +} diff --git a/src/me/chaerim/yapf/FormatOnSaveComponent.scala b/src/me/chaerim/yapf/FormatOnSaveComponent.scala new file mode 100755 index 0000000..f15a84a --- /dev/null +++ b/src/me/chaerim/yapf/FormatOnSaveComponent.scala @@ -0,0 +1,27 @@ +package me.chaerim.yapf + +import com.intellij.AppTopics +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.ApplicationComponent +import com.intellij.openapi.editor.{Document => IdeaDocument} +import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter + +class FormatOnSaveComponent extends ApplicationComponent { + override def getComponentName: String = s"${Settings.PluginName}.FormatOnSave" + + private val fileDocumentManagerAdapter: FileDocumentManagerAdapter = + new FileDocumentManagerAdapter { + override def beforeDocumentSaving(ideaDocument: IdeaDocument): Unit = + for { + document <- Option(Document(ideaDocument)) + settings <- document.settings + if settings.formatOnSave + } yield document.format + } + + override def initComponent(): Unit = + ApplicationManager.getApplication.getMessageBus.connect + .subscribe(AppTopics.FILE_DOCUMENT_SYNC, fileDocumentManagerAdapter) + + override def disposeComponent(): Unit = () +} diff --git a/src/me/chaerim/yapf/Result.scala b/src/me/chaerim/yapf/Result.scala new file mode 100755 index 0000000..09ee013 --- /dev/null +++ b/src/me/chaerim/yapf/Result.scala @@ -0,0 +1,17 @@ +package me.chaerim.yapf + +abstract class Result(val code: Int, + val message: String, + val detail: Option[String] = None, + val shouldNotify: Boolean = true) + +object Result { + case object NotFoundExecutable extends Result(1000, "YAPF executable is not found") + case object UnformattableFile extends Result(1001, "File is unformattable") + case object AlreadyFormattedCode extends Result(1002, "Code is already formatted", shouldNotify = false) + + case class IllegalYapfResult(override val detail: Option[String]) + extends Result(2000, "YAPF result is illegal", detail) + case class FailedToRunCommand(override val detail: Option[String]) + extends Result(3000, "Failed to run command", detail) +} diff --git a/src/me/chaerim/yapf/Settings.scala b/src/me/chaerim/yapf/Settings.scala new file mode 100755 index 0000000..dd2c12f --- /dev/null +++ b/src/me/chaerim/yapf/Settings.scala @@ -0,0 +1,31 @@ +package me.chaerim.yapf + +import com.intellij.openapi.components._ +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.XmlSerializerUtil + +import scala.beans.BeanProperty + +@State(name = "YapfSettings", storages = Array(new Storage(StoragePathMacros.WORKSPACE_FILE))) +class Settings extends PersistentStateComponent[Settings] { + @BeanProperty + var formatOnSave: Boolean = false + + @BeanProperty + var executablePath: String = Settings.DefaultExecutablePath + + @BeanProperty + var styleFileName: String = Settings.DefaultStyleFileName + + override def loadState(config: Settings): Unit = XmlSerializerUtil.copyBean(config, this) + + override def getState: Settings = this +} + +object Settings { + val PluginName: String = "YAPF" + val DefaultStyleFileName: String = ".style.yapf" + val DefaultExecutablePath: String = "/usr/local/bin/yapf" + + def apply(project: Project): Settings = ServiceManager.getService(project, classOf[Settings]) +} diff --git a/src/me/chaerim/yapf/SettingsConfigurable.scala b/src/me/chaerim/yapf/SettingsConfigurable.scala new file mode 100755 index 0000000..9a183dd --- /dev/null +++ b/src/me/chaerim/yapf/SettingsConfigurable.scala @@ -0,0 +1,28 @@ +package me.chaerim.yapf + +import com.intellij.openapi.options.SearchableConfigurable +import com.intellij.openapi.project.Project +import javax.swing.JComponent + +class SettingsConfigurable(project: Project) extends SearchableConfigurable { + private val settings: Settings = Settings(project) + private val panel: SettingsPanel = new SettingsPanel(settings) + + override def getDisplayName: String = Settings.PluginName + + override def getId: String = s"preference.${Settings.PluginName.toLowerCase}" + + override def getHelpTopic: String = s"reference.settings.${Settings.PluginName.toLowerCase}" + + override def enableSearch(option: String): Runnable = super.enableSearch(option) + + override def createComponent(): JComponent = panel.createPanel + + override def isModified: Boolean = panel.isModified + + override def disposeUIResources(): Unit = super.disposeUIResources() + + override def apply(): Unit = panel.apply + + override def reset(): Unit = panel.reset +} diff --git a/src/me/chaerim/yapf/SettingsPanel.scala b/src/me/chaerim/yapf/SettingsPanel.scala new file mode 100755 index 0000000..faabc50 --- /dev/null +++ b/src/me/chaerim/yapf/SettingsPanel.scala @@ -0,0 +1,117 @@ +package me.chaerim.yapf + +import java.awt._ + +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.ui.{TextComponentAccessor, TextFieldWithBrowseButton} +import com.intellij.ui.IdeBorderFactory +import com.intellij.uiDesigner.core.{GridConstraints, GridLayoutManager, Spacer} +import javax.swing._ + +final class SettingsPanel(val settings: Settings) { + import GridConstraints._ + + private val panel: JPanel = new JPanel(gridLayoutManager(3, 1)) + + private val yapfPanel: JPanel = new JPanel(gridLayoutManager(2, 2)) + private val pluginPanel: JPanel = new JPanel(gridLayoutManager(1, 1)) + + private val formatOnSaveCheckBox: JCheckBox = new JCheckBox("Format on save") + private val executablePathField: TextFieldWithBrowseButton = new TextFieldWithBrowseButton + private val styleFileNameField: JTextField = new JTextField(Settings.DefaultStyleFileName) + + private val fileChooserDescriptor: FileChooserDescriptor = + new FileChooserDescriptor(true, false, false, false, false, false) + + def createPanel: JComponent = { + // NOTE[cryeo]: set plugin panel + pluginPanel.setBorder(IdeBorderFactory.createTitledBorder("Plugin settings")) + + pluginPanel.add( + formatOnSaveCheckBox, + gridConstraints(0, 0, FILL_NONE, SIZEPOLICY_CAN_SHRINK | SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED) + ) + + // NOTE[cryeo]: set yapf panel + executablePathField.addBrowseFolderListener("YAPF executable path", + "YAPF executable path", + null, + fileChooserDescriptor, + TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT) + + yapfPanel.setBorder(IdeBorderFactory.createTitledBorder("YAPF settings")) + + yapfPanel.add( + new JLabel("Executable path: "), + gridConstraints(0, 0, FILL_NONE, SIZEPOLICY_FIXED, SIZEPOLICY_FIXED) + ) + + yapfPanel.add( + executablePathField, + gridConstraints(0, 1, FILL_HORIZONTAL, SIZEPOLICY_CAN_SHRINK | SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED) + ) + + yapfPanel.add( + new JLabel("Style file name: "), + gridConstraints(1, 0, FILL_NONE, SIZEPOLICY_FIXED, SIZEPOLICY_FIXED) + ) + + yapfPanel.add( + styleFileNameField, + gridConstraints(1, 1, FILL_HORIZONTAL, SIZEPOLICY_CAN_SHRINK | SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED) + ) + + // // NOTE[cryeo]: set entire panel + panel.add( + pluginPanel, + gridConstraints(0, 0, FILL_BOTH, SIZEPOLICY_CAN_SHRINK | SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED) + ) + + panel.add( + yapfPanel, + gridConstraints(1, 0, FILL_BOTH, SIZEPOLICY_CAN_SHRINK | SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED) + ) + + panel.add( + new Spacer, + gridConstraints(2, 0, FILL_VERTICAL, SIZEPOLICY_FIXED, SIZEPOLICY_CAN_GROW | SIZEPOLICY_WANT_GROW) + ) + + panel + } + + def apply: Unit = { + settings.formatOnSave = formatOnSaveCheckBox.isSelected + settings.executablePath = executablePathField.getText + settings.styleFileName = styleFileNameField.getText + } + + def reset: Unit = { + formatOnSaveCheckBox.setSelected(settings.formatOnSave) + executablePathField.setText(settings.executablePath) + styleFileNameField.setText(settings.styleFileName) + } + + def isModified: Boolean = + settings.formatOnSave != formatOnSaveCheckBox.isSelected || + settings.executablePath != executablePathField.getText || + settings.styleFileName == styleFileNameField.getText + + private def gridLayoutManager(row: Int, col: Int): GridLayoutManager = + new GridLayoutManager(row, col, new Insets(0, 0, 0, 0), -1, -1) + + private def gridConstraints(row: Int, col: Int, fill: Int, hSizePolicy: Int, vSizePolicy: Int): GridConstraints = + new GridConstraints(row, + col, + 1, + 1, + GridConstraints.ANCHOR_WEST, + fill, + hSizePolicy, + vSizePolicy, + null, + null, + null, + 0, + false) +} diff --git a/src/me/chaerim/yapf/Util.scala b/src/me/chaerim/yapf/Util.scala new file mode 100755 index 0000000..75b03df --- /dev/null +++ b/src/me/chaerim/yapf/Util.scala @@ -0,0 +1,49 @@ +package me.chaerim.yapf + +import java.io.ByteArrayInputStream + +import com.intellij.notification.Notifications.Bus +import com.intellij.notification.{Notification, NotificationType} +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.editor.{Document => IdeaDocument} +import me.chaerim.yapf.Result.{FailedToRunCommand, IllegalYapfResult} + +import scala.sys.process._ +import scala.util.{Failure, Success, Try} + +object Util { + val DefaultCharset: String = "utf-8" + + def notifyMessage(context: String, notificationType: NotificationType): Unit = + Bus.notify(new Notification(Settings.PluginName, Settings.PluginName, context, notificationType)) + + def makeYapfCommand(executable: String, configFile: Option[String]): List[String] = + executable :: configFile.toList.flatMap(List("--style", _)) + + def runYapfCommand(executable: String, code: String, configFile: Option[String]): Either[Result, String] = { + val stdout: StringBuilder = new StringBuilder + val stderr: StringBuilder = new StringBuilder + + Try { + val source: ByteArrayInputStream = new ByteArrayInputStream(code.getBytes(DefaultCharset)) + + val processLogger: ProcessLogger = ProcessLogger( + (s: String) => stdout.appendAll(s + "\n"), + (s: String) => stderr.appendAll(s + "\n") + ) + + makeYapfCommand(executable, configFile) #< source ! processLogger + } match { + case Success(s) if s != 0 => Left(IllegalYapfResult(Some(stderr.toString))) + case Success(_) => Right(stdout.toString) + case Failure(e) => Left(FailedToRunCommand(Some(e.toString))) + } + } + + def setFormattedCode(ideaDocument: IdeaDocument, formattedCode: String): Unit = + ApplicationManager.getApplication.runWriteAction(new Runnable { + override def run(): Unit = + CommandProcessor.getInstance.runUndoTransparentAction(() => ideaDocument.setText(formattedCode)) + }) +} diff --git a/yapf.iml b/yapf.iml new file mode 100755 index 0000000..278a0dd --- /dev/null +++ b/yapf.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file