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