├── .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 |
5 |
6 |
7 |
8 |
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 | 
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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------