├── .gitignore ├── .idea ├── hydra.xml ├── libraries │ ├── io_spray_spray_json_2_11_1_3_3.xml │ └── org_scalaj_scalaj_http_2_11_2_3_0.xml ├── misc.xml ├── modules.xml ├── uiDesigner.xml └── vcs.xml ├── LICENSE ├── README.md ├── demo.gif ├── intellij-zeppelin.iml ├── intellij_tools_zeppelin.png ├── intellij_zeppelin_settings.png ├── lib ├── jackson-annotations-2.8.9.jar ├── jackson-core-2.8.9.jar ├── jackson-databind-2.8.9.jar ├── jackson-datatype-jdk8-2.8.9.jar ├── jackson-datatype-jsr310-2.8.9.jar ├── joda-time-2.9.9.jar ├── macro-compat_2.11-1.1.1.jar ├── play-functional_2.11-2.6.3.jar ├── play-json_2.11-2.6.3.jar ├── scala-library-2.11.11.jar ├── scala-library-2.11.8.jar ├── scala-reflect-2.11.11.jar ├── scalaj-http_2.11-2.3.0.jar └── spray-json_2.11-1.3.3.jar ├── resources └── META-INF │ └── plugin.xml └── src └── intellij └── zeppelin ├── IdeaDocumentApi.scala ├── ZeppelinAction.scala ├── ZeppelinAddParagraph.scala ├── ZeppelinApi.scala ├── ZeppelinConfigurable.form ├── ZeppelinConfigurable.java ├── ZeppelinConnection.scala ├── ZeppelinDeleteParagraph.scala ├── ZeppelinNewNoteAction.scala └── ZeppelinRunParagraph.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.idea/hydra.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /.idea/libraries/io_spray_spray_json_2_11_1_3_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/libraries/org_scalaj_scalaj_http_2_11_2_3_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 chilang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Intellij Zeppelin 2 | ================= 3 | 4 | [Download](https://plugins.jetbrains.com/plugin/10023-intellij-zeppelin) 5 | 6 | ![Demo](demo.gif) 7 | 8 | ## Features 9 | 10 | ### 0.3 11 | - Support for SOCKS proxy (useful when connecting to Zeppelin on AWS). Thanks to [**Roberto Congiu**](https://github.com/rcongiu) 12 | ### 0.2 13 | 14 | ![IntelliJ Tools Zeppelin Menu](intellij_tools_zeppelin.png) 15 | 16 | - Create new Zeppelin notebook from within IntelliJ. 17 | With an opened `.scala` file, `Shift+Option+Command+Enter (Tools -> Zeppelin -> New Zeppelin Notebook)` 18 | - Create and evaluate fragment of code in Zeppelin paragraph `Shift+Option+Enter (Tools -> Zeppelin - Add Zeppelin paragraph)` 19 | - Delete a Zeppelin paragraph `Tools -> Zeppelin -> Delete Zeppelin paragraph` 20 | - Run current line of code in Zeppelin `Shift+Enter (Tools -> Zeppelin -> Run Zeppelin paragraph)` 21 | - Set Zeppelin host & (optionally) username/password `IntelliJ Settings -> Languages and Frameworks -> Zeppelin Notebook` 22 | ![IntelliJ Settings Zeppelin Notebook](intellij_zeppelin_settings.png) 23 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/demo.gif -------------------------------------------------------------------------------- /intellij-zeppelin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /intellij_tools_zeppelin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/intellij_tools_zeppelin.png -------------------------------------------------------------------------------- /intellij_zeppelin_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/intellij_zeppelin_settings.png -------------------------------------------------------------------------------- /lib/jackson-annotations-2.8.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/jackson-annotations-2.8.9.jar -------------------------------------------------------------------------------- /lib/jackson-core-2.8.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/jackson-core-2.8.9.jar -------------------------------------------------------------------------------- /lib/jackson-databind-2.8.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/jackson-databind-2.8.9.jar -------------------------------------------------------------------------------- /lib/jackson-datatype-jdk8-2.8.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/jackson-datatype-jdk8-2.8.9.jar -------------------------------------------------------------------------------- /lib/jackson-datatype-jsr310-2.8.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/jackson-datatype-jsr310-2.8.9.jar -------------------------------------------------------------------------------- /lib/joda-time-2.9.9.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/joda-time-2.9.9.jar -------------------------------------------------------------------------------- /lib/macro-compat_2.11-1.1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/macro-compat_2.11-1.1.1.jar -------------------------------------------------------------------------------- /lib/play-functional_2.11-2.6.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/play-functional_2.11-2.6.3.jar -------------------------------------------------------------------------------- /lib/play-json_2.11-2.6.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/play-json_2.11-2.6.3.jar -------------------------------------------------------------------------------- /lib/scala-library-2.11.11.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/scala-library-2.11.11.jar -------------------------------------------------------------------------------- /lib/scala-library-2.11.8.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/scala-library-2.11.8.jar -------------------------------------------------------------------------------- /lib/scala-reflect-2.11.11.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/scala-reflect-2.11.11.jar -------------------------------------------------------------------------------- /lib/scalaj-http_2.11-2.3.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/scalaj-http_2.11-2.3.0.jar -------------------------------------------------------------------------------- /lib/spray-json_2.11-1.3.3.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chilang/intellij-zeppelin/d4f4e71ba1ffcbbfab3331a4964a3b72f55b5b61/lib/spray-json_2.11-1.3.3.jar -------------------------------------------------------------------------------- /resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | intellij.zeppelin. 3 | IntelliJ Zeppelin 4 | 0.3 5 | Chi Lang Ngo 6 | 7 | 11 |
  • Create new Zeppelin notebook from within IntelliJ .Scala file (CTRL+ALT+SHIFT+ENTER) 12 |
  • Create and run new code paragraph (ALT+SHIFT+ENTER) 13 |
  • Run existing code paragraph (SHIFT+ENTER) 14 |
  • Delete existing paragraph (ALT+SHIFT+D) 15 | 16 | 17 | Access Zeppelin actions via Tools->Zeppelin or keyboard shortcuts. 18 | 19 | ]]> 20 | 21 | Language & Framework -> Zeppelin Notebook 24 | 0.3 Fixed short-cuts for deleting paragraph. (Roberto Congiu) Added support for SOCKS proxy (useful for connecting to Zeppelin on AWS) 25 | ]]> 26 | 27 | 28 | 29 | 30 | 31 | 33 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 53 | 54 | 55 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | intellij.zeppelin.ZeppelinConnection 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/IdeaDocumentApi.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import com.intellij.notification.{NotificationType, Notifications} 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.application.ApplicationManager 6 | import com.intellij.openapi.editor.{Document, Editor} 7 | import com.intellij.openapi.fileEditor.FileDocumentManager 8 | import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.vfs.VirtualFile 11 | 12 | 13 | sealed trait SelectionMode 14 | object SelectedText extends SelectionMode 15 | object SingleLine extends SelectionMode 16 | 17 | case class CodeFragment(selectionMode: SelectionMode, content:String) 18 | 19 | trait IdeaDocumentApi { 20 | 21 | def currentEditor(anActionEvent: AnActionEvent): Editor = { 22 | FileEditorManagerEx.getInstanceEx(anActionEvent.getProject).getSelectedTextEditor 23 | } 24 | 25 | def currentFile(anActionEvent: AnActionEvent): VirtualFile = { 26 | FileEditorManagerEx.getInstanceEx(anActionEvent.getProject).getCurrentFile 27 | } 28 | 29 | def invokeLater(f: => Unit): Unit = { 30 | ApplicationManager.getApplication.invokeLater(new Runnable { 31 | override def run(): Unit = f 32 | }) 33 | } 34 | 35 | def show(message:String): Unit = invokeLater{ 36 | val notification = new com.intellij.notification.Notification( 37 | "", 38 | "Zeppelin Idea", 39 | message, 40 | NotificationType.INFORMATION, 41 | null 42 | ) 43 | ApplicationManager.getApplication.getMessageBus.syncPublisher(Notifications.TOPIC).notify(notification) 44 | } 45 | 46 | 47 | def replaceLine(editor: Editor, line: Int, withText:String): Unit = { 48 | editor.getDocument.replaceString( 49 | editor.getDocument.getLineStartOffset(line), 50 | editor.getDocument.getLineEndOffset(line), 51 | withText 52 | ) 53 | } 54 | 55 | def findPreviousLineMatching(editor: Editor, lineMatching:String => Boolean): Option[Int] = { 56 | val currentLine = editor.getCaretModel.getLogicalPosition.line 57 | val previousParagraphMarkerLine: Option[Int] = Range(currentLine, 1, -1).map { line => 58 | val start = editor.getDocument.getLineStartOffset(line) 59 | val end = editor.getDocument.getLineEndOffset(line) 60 | (line, editor.getDocument.getCharsSequence.subSequence(start, end).toString) 61 | }.collectFirst { 62 | case (line, text) if lineMatching(text) => line 63 | } 64 | previousParagraphMarkerLine 65 | } 66 | 67 | def currentCodeFragment(editor: Editor): CodeFragment = { 68 | val text = currentSelectedText(editor) 69 | if (text.isEmpty) CodeFragment(SingleLine, currentLineText(editor)) else CodeFragment(SelectedText, text) 70 | } 71 | 72 | def currentLineText(editor: Editor):String = { 73 | val currentLine = editor.getCaretModel.getLogicalPosition.line 74 | editor.getDocument.getCharsSequence.subSequence( 75 | editor.getDocument.getLineStartOffset(currentLine), 76 | editor.getDocument.getLineEndOffset(currentLine) 77 | ).toString 78 | } 79 | def currentSelectedText(editor: Editor): String = { 80 | val selectionModel = editor.getSelectionModel 81 | val blockStarts = selectionModel.getBlockSelectionStarts 82 | val blockEnds = selectionModel.getBlockSelectionEnds 83 | editor.getDocument.getCharsSequence.subSequence(blockStarts(0), blockEnds(0)).toString 84 | } 85 | 86 | def insertAfterFragment(editor: Editor, fragment:CodeFragment, text: String): Unit = { 87 | editor.getDocument.insertString(lineStartOffsetAfter(editor, fragment), text) 88 | } 89 | 90 | private def lineStartOffsetAfter(editor: Editor, fragment: CodeFragment): Int = { 91 | fragment.selectionMode match { 92 | case SelectedText => 93 | val currentLine = editor.getSelectionModel.getSelectionEndPosition.line 94 | editor.getDocument.getLineEndOffset(currentLine) 95 | case SingleLine => 96 | val currentLine = editor.getCaretModel.getLogicalPosition.line 97 | editor.getDocument.getLineEndOffset(currentLine) 98 | } 99 | } 100 | 101 | def insertBeforeFragment(editor: Editor, fragment: CodeFragment, text: String): Unit = { 102 | val lineStartOffset = fragment.selectionMode match { 103 | case SelectedText => editor.getDocument.getLineStartOffset(editor.getSelectionModel.getSelectionStartPosition.line) 104 | case SingleLine => editor.getDocument.getLineStartOffset(editor.getCaretModel.getLogicalPosition.line) 105 | } 106 | editor.getDocument.insertString(lineStartOffset, text) 107 | } 108 | 109 | 110 | def currentDocument(file: VirtualFile): Document = FileDocumentManager.getInstance().getDocument(file) 111 | 112 | def currentFileIn(project: Project): VirtualFile = FileEditorManagerEx.getInstanceEx(project).getCurrentFile 113 | } 114 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinAction.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import com.intellij.openapi.actionSystem.{AnAction, AnActionEvent} 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.command.{CommandProcessor, UndoConfirmationPolicy} 6 | import com.intellij.openapi.editor.{Document, Editor} 7 | import com.intellij.openapi.util.Computable 8 | 9 | import scala.collection.immutable 10 | 11 | abstract class ZeppelinAction extends AnAction with IdeaDocumentApi { 12 | 13 | def zeppelin(anActionEvent: AnActionEvent): ZeppelinApi = { 14 | ZeppelinConnection.connectionFor(anActionEvent.getProject).api 15 | } 16 | 17 | def findNotebook(editor: Editor): Option[Notebook] = precedingLines(editor).flatMap(x => Notebook.parse(x._2)).headOption 18 | 19 | def findParagraph(editor: Editor): Option[Paragraph] = precedingLines(editor).flatMap(x => Paragraph.parse(x._2)).headOption 20 | 21 | private def precedingLines(editor: Editor): immutable.Seq[(Int, String)] = { 22 | val currentLine = editor.getCaretModel.getLogicalPosition.line 23 | Range(currentLine, 1, -1).map { line => 24 | val start = editor.getDocument.getLineStartOffset(line - 1) 25 | val end = editor.getDocument.getLineStartOffset(line) 26 | (line, editor.getDocument.getCharsSequence.subSequence(start, end).toString) 27 | }.map(x => x.copy(_2 = x._2.stripLineEnd)) 28 | } 29 | 30 | def findNote(editor: Editor, line: Int): Option[Notebook] = { 31 | val currentLine = editor.getCaretModel.getLogicalPosition.line 32 | val linesInReverse = Range(currentLine, 1, -1).map { line => 33 | val start = editor.getDocument.getLineStartOffset(line - 1) 34 | val end = editor.getDocument.getLineStartOffset(line) 35 | editor.getDocument.getCharsSequence.subSequence(start, end).toString 36 | }.map(_.stripLineEnd) 37 | 38 | linesInReverse.flatMap(Notebook.parse).headOption 39 | } 40 | 41 | protected def runWriteAction(anActionEvent: AnActionEvent)(f: Document => Unit): Unit = ApplicationManager.getApplication.runWriteAction{ 42 | val document = currentDocument(currentFileIn(anActionEvent.getProject)) 43 | new Computable[Unit] { 44 | override def compute(): Unit = { 45 | CommandProcessor.getInstance().executeCommand( 46 | anActionEvent.getProject, 47 | new Runnable { 48 | override def run(): Unit = { 49 | f(document) 50 | } 51 | }, 52 | "Modified from Zeppelin Idea plugin", 53 | "ZeppelinIdea", 54 | UndoConfirmationPolicy.DEFAULT, 55 | document 56 | ) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinAddParagraph.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.editor.Editor 5 | 6 | import scala.util.Try 7 | 8 | class ZeppelinAddParagraph extends ZeppelinAction { 9 | 10 | override def actionPerformed(anActionEvent: AnActionEvent): Unit = { 11 | 12 | val editor = currentEditor(anActionEvent) 13 | val api = zeppelin(anActionEvent) 14 | findNotebook(editor) 15 | .map { notebook => 16 | val codeFragment = currentCodeFragment(editor) 17 | (for { 18 | paragraph <- api.createParagraph(notebook, codeFragment.content) 19 | _ <- Try(runWriteAction(anActionEvent){ _ => 20 | 21 | updateNotebookMarker(editor, notebook.copy(size = notebook.size+1)) 22 | insertBeforeFragment(editor, codeFragment, paragraph.markerText + "\n") 23 | }) 24 | result <- api.runParagraph(notebook, paragraph) 25 | } yield { 26 | runWriteAction(anActionEvent) { _ => 27 | insertAfterFragment(editor, codeFragment, result.markerText) 28 | } 29 | }).recover { case t: Throwable => show(t.toString) } 30 | }.getOrElse(show("No Zeppelin NoteId found.")) 31 | 32 | } 33 | 34 | private def updateNotebookMarker(editor: Editor, notebook: Notebook): Unit = { 35 | findPreviousLineMatching(editor, text => Notebook.parse(text).isDefined).foreach { line => 36 | replaceLine(editor, line, notebook.markerText) 37 | } 38 | } 39 | 40 | 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinApi.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import java.net.{HttpCookie, InetSocketAddress, SocketAddress} 4 | 5 | import spray.json.{JsString, _} 6 | 7 | import scala.util.matching.Regex 8 | import scala.util.{Failure, Success, Try} 9 | import scalaj.http.Http 10 | 11 | case class Notebook(id:String, size:Int) { 12 | def notebookHeader(url:String):String = Seq(markerText, s"//$url/#/notebook/$id").mkString("\n") 13 | def markerText:String = s"//Notebook:$id,$size" 14 | } 15 | case class Paragraph(id:String, index:Int) { 16 | def markerText:String = s"//Paragraph:$id,$index" 17 | } 18 | 19 | case class ParagraphResult(paragraph: Paragraph, results:Seq[String]) { 20 | def markerText:String = { 21 | results.flatMap(_.split("\n").map(x => s"\n//$x")).mkString("") 22 | } 23 | } 24 | 25 | object Notebook { 26 | private val NoteId: Regex = """.*//Notebook:(\w+),(\d+).*""".r 27 | def parse(text:String):Option[Notebook] = text match { 28 | case NoteId(id, size) => Some(Notebook(id, size.toInt)) 29 | case _ => None 30 | } 31 | } 32 | 33 | object Paragraph{ 34 | private val ParagraphId: Regex = """.*//Paragraph:([\w_-]+),(\d+).*""".r 35 | def parse(text:String):Option[Paragraph] = text match { 36 | case ParagraphId(id, size) => Some(Paragraph(id, size.toInt)) 37 | case _ => None 38 | } 39 | 40 | } 41 | case class Credentials(username:String, password:String) 42 | 43 | class ZeppelinApi(val url:String, credentials:Option[Credentials], proxy:Option[String]){ 44 | private[this] def buildReq(url:String) = { 45 | val h = Http(url) 46 | proxy match { 47 | case None => h 48 | case Some(p) => 49 | val pinfo = p.split(":") 50 | h.proxy(new java.net.Proxy(java.net.Proxy.Type.SOCKS, new InetSocketAddress(pinfo.head, pinfo.last.toInt))) 51 | } 52 | } 53 | 54 | lazy val sessionToken: Option[HttpCookie] = credentials.flatMap { c => 55 | val r = buildReq(s"$url/api/login").postForm(Seq( 56 | ("username", c.username), 57 | ("password", c.password) 58 | )) 59 | r.asString.cookies.headOption 60 | } 61 | 62 | 63 | def createNotebook(name:String):Try[Notebook] = { 64 | val req = request("/api/notebook").postData( 65 | s""" 66 | |{"name": "$name"} 67 | """.stripMargin) 68 | 69 | val response = req.asString.body 70 | response.parseJson.asJsObject().fields("body") match { 71 | case JsString(s) => Success(Notebook(s, 0)) 72 | case _ => Failure(new RuntimeException("Error creating new Zeppelin notebook")) 73 | } 74 | } 75 | 76 | private def request(path:String) = { 77 | val r = buildReq(s"$url$path") 78 | sessionToken.fold(r)(cookie => r.header("Cookie", s"${cookie.getName}=${cookie.getValue}")) 79 | } 80 | 81 | def createParagraph(notebook: Notebook, text: String, atIndex:Option[Int] = None):Try[Paragraph] = { 82 | val escaped = text.replaceAll("\\\"", "\\\\\"") 83 | 84 | val body = atIndex match { 85 | case Some(index) => s"""{"title":"new note", "text": "$escaped", "index": $index}""" 86 | case None => s"""{"title":"new note", "text": "$escaped"}""" 87 | } 88 | val req = request(s"/api/notebook/${notebook.id}/paragraph").postData(body) 89 | 90 | req.asString.body.parseJson.asJsObject.fields("body") match { 91 | case JsString(paragraphId) => Success(Paragraph(paragraphId, notebook.size)) 92 | case _ => Failure(new RuntimeException("Error creating new Zeppelin paragraph")) 93 | } 94 | } 95 | 96 | def deleteParagraph(notebook: Notebook, paragraph: Paragraph):Try[Paragraph] = { 97 | val result = request(s"/api/notebook/${notebook.id}/paragraph/${paragraph.id}").method("DELETE") 98 | .asString 99 | .body.parseJson.asJsObject 100 | 101 | result.fields("status") match { 102 | case JsString(status) if status == "OK" => Success(paragraph) 103 | case unknown => Failure(new RuntimeException(s"Unrecognized response $unknown")) 104 | } 105 | } 106 | 107 | def updateParagraph(notebook: Notebook, paragraph: Paragraph, text:String): Try[Paragraph] = { 108 | val escaped = text.replaceAll("\\\"", "\\\\\"") 109 | val result = request(s"/api/notebook/${notebook.id}/paragraph/${paragraph.id}").put(s"""{"text": "$escaped"}""") 110 | .asString 111 | .body.parseJson.asJsObject 112 | 113 | result.fields("status") match { 114 | case JsString(status) if status == "OK" => Success(paragraph) 115 | case unknown => Failure(new RuntimeException(s"Unrecognized response $unknown")) 116 | } 117 | } 118 | 119 | def runParagraph(notebook: Notebook, paragraph: Paragraph):Try[ParagraphResult] = { 120 | val result = request(s"/api/notebook/run/${notebook.id}/${paragraph.id}").postData("") 121 | .asString 122 | .body.parseJson.asJsObject 123 | 124 | result.fields("status") match { 125 | case JsString(status) if status == "OK" => 126 | result.fields("body").asJsObject.fields("msg") match { 127 | case JsArray(arr) => Success(arr.map(_.asJsObject.fields("data")) 128 | .collect { case JsString(s) => s }) 129 | .map(ParagraphResult(paragraph, _)) 130 | case other => Failure(new RuntimeException(s"Unrecognized result $other")) 131 | } 132 | case unknown => Failure(new RuntimeException(s"Unrecognized response $unknown")) 133 | } 134 | } 135 | } 136 | 137 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinConfigurable.form: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
    89 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinConfigurable.java: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin; 2 | 3 | import com.intellij.openapi.options.SearchableConfigurable; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.ui.components.JBTextField; 6 | import com.intellij.util.ui.UIUtil; 7 | import org.jetbrains.annotations.Nls; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import javax.swing.*; 11 | import java.awt.event.FocusAdapter; 12 | import java.awt.event.FocusEvent; 13 | 14 | public class ZeppelinConfigurable implements SearchableConfigurable { 15 | private JPanel mainPanel; 16 | private JBTextField usernameField; 17 | private JPasswordField passwordField; 18 | private JPanel configPanel; 19 | private JBTextField hostTextField; 20 | private JBTextField proxyField; 21 | 22 | private final Project myProject; 23 | 24 | public ZeppelinConfigurable(@NotNull Project project) { 25 | myProject = project; 26 | 27 | usernameField.addFocusListener(createInitialTextFocusAdapter(usernameField, DEFAULT_USERNAME_TEXT)); 28 | ZeppelinConnection connection = ZeppelinConnection$.MODULE$.connectionFor(project); 29 | setInitialText(usernameField, connection.getUsername(), DEFAULT_USERNAME_TEXT); 30 | passwordField.setText(connection.getPassword()); 31 | hostTextField.setText(connection.getHostUrl()); 32 | proxyField.setText(connection.getProxyUrl()); 33 | } 34 | 35 | @Nls 36 | @Override 37 | public String getDisplayName() { 38 | return "Zeppelin Notebook"; 39 | } 40 | 41 | @Override 42 | public String getHelpTopic() { 43 | return ""; 44 | } 45 | 46 | @Override 47 | public JComponent createComponent() { 48 | return mainPanel; 49 | } 50 | 51 | @NotNull 52 | @Override 53 | public String getId() { 54 | return "ZeppelinConfigurable"; 55 | } 56 | 57 | private static final String DEFAULT_USERNAME_TEXT = "Leave empty for anonymous access"; 58 | 59 | 60 | 61 | public void apply() { 62 | final ZeppelinConnection connection = ZeppelinConnection$.MODULE$.connectionFor(myProject); 63 | 64 | if (configPanel.isVisible()) { 65 | final String oldUsername = connection.getUsername(); 66 | final String oldPassword = connection.getPassword(); 67 | final String oldHost = connection.getHostUrl(); 68 | final String newUsername = getUsername(); 69 | final String newPassword = String.valueOf(passwordField.getPassword()); 70 | final String newHostUrl = hostTextField.getText(); 71 | final String newProxyUrl = proxyField.getText(); 72 | 73 | if (!oldUsername.equals(newUsername) || !oldPassword.equals(newPassword) || !oldHost.equals(newHostUrl)) { 74 | connection.setUsername(newUsername); 75 | connection.setPassword(newPassword); 76 | connection.setHostUrl(newHostUrl); 77 | connection.setProxyUrl(newProxyUrl); 78 | connection.resetApi(); 79 | } 80 | } 81 | } 82 | 83 | public void reset() { 84 | final ZeppelinConnection connection = ZeppelinConnection$.MODULE$.connectionFor(myProject); 85 | if (configPanel.isVisible()) { 86 | setInitialText(usernameField, connection.getUsername(), DEFAULT_USERNAME_TEXT); 87 | passwordField.setText(connection.getPassword()); 88 | hostTextField.setText(connection.getHostUrl()); 89 | } 90 | } 91 | 92 | public boolean isModified() { 93 | final ZeppelinConnection connection = ZeppelinConnection$.MODULE$.connectionFor(myProject); 94 | if (configPanel.isVisible()) { 95 | final String oldUsername = connection.getUsername(); 96 | final String oldPassword = connection.getPassword(); 97 | final String oldHost = connection.getHostUrl(); 98 | 99 | final String newPassword = String.valueOf(passwordField.getPassword()); 100 | final String newUsername = getUsername(); 101 | final String newHost = hostTextField.getText(); 102 | 103 | return !oldPassword.equals(newPassword) || !oldUsername.equals(newUsername) || !oldHost.equals(newHost) ; 104 | } 105 | return false; 106 | } 107 | 108 | private String getUsername() { 109 | final String usernameText = usernameField.getText(); 110 | return DEFAULT_USERNAME_TEXT.equals(usernameText) ? "" : usernameText; 111 | } 112 | 113 | @NotNull 114 | private static FocusAdapter createInitialTextFocusAdapter(@NotNull JBTextField field, @NotNull String initialText) { 115 | return new FocusAdapter() { 116 | @Override 117 | public void focusGained(FocusEvent e) { 118 | if (field.getText().equals(initialText)) { 119 | field.setForeground(UIUtil.getActiveTextColor()); 120 | field.setText(""); 121 | } 122 | } 123 | 124 | @Override 125 | public void focusLost(FocusEvent e) { 126 | if (field.getText().isEmpty()) { 127 | field.setForeground(UIUtil.getInactiveTextColor()); 128 | field.setText(initialText); 129 | } 130 | } 131 | }; 132 | } 133 | 134 | private static void setInitialText(@NotNull JBTextField field, 135 | @NotNull String savedValue, 136 | @NotNull String defaultText) { 137 | if (savedValue.isEmpty()) { 138 | field.setForeground(UIUtil.getInactiveTextColor()); 139 | field.setText(defaultText); 140 | } 141 | else { 142 | field.setForeground(UIUtil.getActiveTextColor()); 143 | field.setText(savedValue); 144 | } 145 | } 146 | } 147 | 148 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinConnection.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import com.intellij.openapi.components.ProjectComponent 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.ui.{InputValidator, Messages} 6 | 7 | class ZeppelinConnection(val project:Project) extends ProjectComponent{ 8 | 9 | 10 | private[this] var username:String = "" 11 | private[this] var password:String = "" 12 | private[this] var hostUrl:String = ZeppelinConnection.DefaultZeppelinHost 13 | private[this] var proxyUrl:String = "" 14 | private[this] var maybeApi: Option[ZeppelinApi] = None 15 | 16 | def getUsername:String = username 17 | def getPassword:String = password 18 | def getHostUrl:String = hostUrl 19 | def getProxyUrl:String = proxyUrl 20 | 21 | def setUsername(value:String): Unit = { username = value } 22 | def setPassword(value:String): Unit = { password = value } 23 | def setHostUrl(value:String): Unit = { hostUrl = value } 24 | def setProxyUrl(value:String):Unit = { proxyUrl = value } 25 | 26 | def promptForZeppelinHost():ZeppelinApi = { 27 | hostUrl = Messages.showInputDialog( 28 | "Please, enter your Zeppelin Notebook", 29 | "Zeppelin Notebook", 30 | null, 31 | hostUrl, 32 | new InputValidator() { 33 | override def checkInput(inputString: String): Boolean = true 34 | 35 | override def canClose(inputString: String) = true 36 | }) 37 | maybeApi = Some(new ZeppelinApi(hostUrl, credentials, proxyInfo)) 38 | maybeApi.get 39 | } 40 | 41 | private def credentials:Option[Credentials] = if (username != "" && password != "") Some(Credentials(username, password)) else None 42 | private def proxyInfo:Option[String] = if(proxyUrl!="") Some(proxyUrl) else None 43 | 44 | private [zeppelin] def resetApi():Unit = { 45 | maybeApi = Some(new ZeppelinApi(hostUrl, credentials, proxyInfo)) 46 | } 47 | def api: ZeppelinApi = maybeApi.getOrElse(promptForZeppelinHost()) 48 | } 49 | 50 | object ZeppelinConnection{ 51 | val DefaultZeppelinHost = "http://localhost:8080" 52 | def connectionFor(project: Project): ZeppelinConnection = project.getComponent(classOf[ZeppelinConnection]) 53 | } 54 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinDeleteParagraph.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | 5 | class ZeppelinDeleteParagraph extends ZeppelinAction { 6 | 7 | override def actionPerformed(anActionEvent: AnActionEvent): Unit = { 8 | 9 | val editor = currentEditor(anActionEvent) 10 | (for { 11 | notebook <- findNotebook(editor) 12 | paragraph <- findParagraph(editor) 13 | } yield { 14 | (for { 15 | _ <- zeppelin(anActionEvent).deleteParagraph(notebook, paragraph) 16 | } yield { 17 | runWriteAction(anActionEvent){ _ => 18 | findPreviousLineMatching(editor, line => Paragraph.parse(line).isDefined).foreach{ line => 19 | replaceLine(editor, line, "") 20 | } 21 | } 22 | }).recover { case t: Throwable => show(t.toString) } 23 | }).getOrElse(show("No Zeppelin NoteId found.")) 24 | 25 | } 26 | 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinNewNoteAction.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.editor.Editor 5 | 6 | class ZeppelinNewNoteAction extends ZeppelinAction { 7 | 8 | override def actionPerformed(anActionEvent: AnActionEvent): Unit = { 9 | 10 | val selectedText = currentSelectedText(currentEditor(anActionEvent)) 11 | val name = if (selectedText.isEmpty) "IntelliJ Notebook" else selectedText 12 | val api = zeppelin(anActionEvent) 13 | api.createNotebook(name).map { notebook => 14 | show(s"Created new Zeppelin notebook '$name': ${notebook.id}") 15 | 16 | runWriteAction(anActionEvent){ _ => 17 | insertBefore(currentEditor(anActionEvent), notebook, api.url) 18 | } 19 | 20 | } recover { case t: Throwable => show(t.toString) } 21 | } 22 | 23 | 24 | def insertBefore(editor: Editor, notebook: Notebook, url:String): Unit = { 25 | val offset = editor.getCaretModel.getOffset 26 | val currentLine = editor.getCaretModel.getLogicalPosition.line 27 | val lineStartOffset = editor.getDocument.getLineStartOffset(currentLine) 28 | 29 | val message = notebook.notebookHeader(url) 30 | editor.getDocument.insertString(lineStartOffset, message) 31 | editor.getCaretModel.moveToOffset(offset + message.length) 32 | } 33 | 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/intellij/zeppelin/ZeppelinRunParagraph.scala: -------------------------------------------------------------------------------- 1 | package intellij.zeppelin 2 | 3 | import com.intellij.openapi.actionSystem.AnActionEvent 4 | import com.intellij.openapi.editor.Editor 5 | 6 | class ZeppelinRunParagraph extends ZeppelinAction { 7 | 8 | override def actionPerformed(anActionEvent: AnActionEvent): Unit = { 9 | val editor = currentEditor(anActionEvent) 10 | val api = zeppelin(anActionEvent) 11 | for { 12 | note <- findNotebook(editor) 13 | paragraph <- findParagraph(editor) 14 | } yield { 15 | 16 | val codeFragment = currentCodeFragment(editor) 17 | 18 | (for { 19 | newParagraph <- api.updateParagraph(note, paragraph, codeFragment.content) 20 | result <- api.runParagraph(note, newParagraph) 21 | } yield { 22 | runWriteAction(anActionEvent) { _ => 23 | insertAfterFragment(editor, codeFragment, result.markerText) 24 | } 25 | }).recover { case t: Throwable => show(t.toString) } 26 | }.getOrElse(show("No Zeppelin //Notebook: marker found.")) 27 | 28 | } 29 | } 30 | 31 | 32 | --------------------------------------------------------------------------------