diff --git a/build.sbt b/build.sbt index f5f2610..d4f573d 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ lazy val riddlIdeaPlugin: Project = Root( With.build_info, With.coverage(90), With.aliases, - With.riddl("0.54.1") + With.riddl("0.56.0") ) .enablePlugins(KotlinPlugin, JavaAppPackaging) .settings( diff --git a/project/Dependencies.scala b/project/Dependencies.scala index fb806bd..2988b0a 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -24,7 +24,7 @@ object Dep { "com.eclipsesource.minimal-json" % "minimal-json" % "0.9.5" withSources () } val kotlin = "org.jetbrains.kotlin" % "kotlin-stdlib" % "2.0.20" - val riddlCommands = "com.ossuminc" % "riddl-commands_3" % "0.54.1" + val riddlCommands = "com.ossuminc" % "riddl-commands_3" % "0.56.0" val basic: Seq[ModuleID] = Seq(minimalJson, scalactic, scalatest, scalacheck) diff --git a/project/plugins.sbt b/project/plugins.sbt index bfefd6b..c8f5cea 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("com.ossuminc" % "sbt-ossuminc" % "0.16.2") +addSbtPlugin("com.ossuminc" % "sbt-ossuminc" % "0.17.1") addSbtPlugin("org.jetbrains" % "sbt-idea-plugin" % "3.26.2") addSbtPlugin("org.jetbrains.scala" % "sbt-kotlin-plugin" % "3.0.3") diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index e0a7b2a..f1f5dfd 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -31,8 +31,10 @@ - + diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlDocumentListener.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlDocumentListener.scala index b3a02b2..b1d0688 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlDocumentListener.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlDocumentListener.scala @@ -2,35 +2,48 @@ package com.ossuminc.riddl.plugins.idea.files import com.intellij.openapi.editor.event.{DocumentEvent, DocumentListener} import com.intellij.openapi.editor.EditorFactory -import com.intellij.openapi.util.TextRange -import com.ossuminc.riddl.plugins.idea.files.utils.{ - getWholeWordsSubstrings, - highlightKeywordsOnChange -} -import com.ossuminc.riddl.plugins.idea.files.RiddlTokenizer.* +import com.ossuminc.riddl.plugins.idea.files.utils.highlightKeywords +import com.ossuminc.riddl.plugins.idea.utils.highlightForErrorMessage +import com.ossuminc.riddl.plugins.idea.utils.ManagerBasedGetterUtils.* +import com.ossuminc.riddl.plugins.idea.utils.ParsingUtils.runCommandForEditor + +import java.nio.file.Path class RiddlDocumentListener extends DocumentListener { override def documentChanged(event: DocumentEvent): Unit = { val doc = event.getDocument + val editors = EditorFactory.getInstance().getEditors(doc) if editors.nonEmpty then - val newText = doc.getText( - new TextRange(event.getOffset, event.getOffset + event.getNewLength) - ) - val wholeWords = getWholeWordsSubstrings( - doc.getText, - newText, - event.getOffset - ) - highlightKeywordsOnChange( - wholeWords - .zip( - RiddlTokenizer - .tokenize(wholeWords.map(_._1).mkString(" ")) - .filter(!_._1.isBlank) - ) - .map((wwTup, tokTup) => (wwTup._1, wwTup._2, tokTup._3)), - editors.head - ) + editors.find(_.getDocument == doc) match + case Some(editor) if doc.getText.nonEmpty => + if editor.getVirtualFile != null then + val editorFilePath = editor.getVirtualFile.getPath + + highlightKeywords(editor.getDocument.getText, editor) + + getRiddlIdeaStates.allStates.values.toSeq + .filter { state => + state.getTopLevelPath.exists(path => + editorFilePath.startsWith( + Path.of(path).getParent.toString + ) + ) + } + .foreach { state => + runCommandForEditor(state.getWindowNum) + Thread.sleep(350) + state.getMessagesForEditor + .filter(msg => editorFilePath.endsWith(msg.loc.source.origin)) + .foreach { msg => + highlightForErrorMessage( + state, + Seq(), + Right(msg) + ) + } + } + + case _ => () } } diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlFileEditorListener.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlFileEditorListener.scala new file mode 100644 index 0000000..26ba3d3 --- /dev/null +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlFileEditorListener.scala @@ -0,0 +1,25 @@ +package com.ossuminc.riddl.plugins.idea.files + +import com.intellij.openapi.fileEditor.{ + FileEditorManager, + FileEditorManagerEvent, + FileEditorManagerListener, +} +import com.intellij.openapi.vfs.VirtualFile +import com.ossuminc.riddl.plugins.idea.utils.ManagerBasedGetterUtils.getProject +import com.ossuminc.riddl.plugins.idea.files.utils.highlightKeywordsAndErrorsForFile + +class RiddlFileEditorListener extends FileEditorManagerListener { + override def fileOpened( + source: FileEditorManager, + file: VirtualFile + ): Unit = highlightKeywordsAndErrorsForFile(source, file) + + override def selectionChanged(event: FileEditorManagerEvent): Unit = + if event.getNewFile != null then + highlightKeywordsAndErrorsForFile( + FileEditorManager + .getInstance(getProject), + event.getNewFile + ) +} diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlFileListenerHighlighter.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlFileListenerHighlighter.scala deleted file mode 100644 index 495c21c..0000000 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlFileListenerHighlighter.scala +++ /dev/null @@ -1,47 +0,0 @@ -package com.ossuminc.riddl.plugins.idea.files - -import com.intellij.openapi.fileEditor.{ - FileDocumentManager, - FileEditorManager, - FileEditorManagerEvent, - FileEditorManagerListener, - TextEditor -} -import com.intellij.openapi.vfs.VirtualFile -import com.ossuminc.riddl.plugins.idea.utils.ManagerBasedGetterUtils.{ - getProject, - getRiddlIdeaStates -} -import com.ossuminc.riddl.plugins.idea.utils.highlightErrorForFile - -class RiddlFileListenerHighlighter extends FileEditorManagerListener { - override def fileOpened( - source: FileEditorManager, - file: VirtualFile - ): Unit = highlightKeywordsAndErrors(source, file) - - override def selectionChanged(event: FileEditorManagerEvent): Unit = - if event.getNewFile != null then - highlightKeywordsAndErrors( - FileEditorManager - .getInstance(getProject), - event.getNewFile - ) - - private def highlightKeywordsAndErrors( - source: FileEditorManager, - file: VirtualFile - ): Unit = source - .getAllEditors(file) - .foreach { te => - val doc = FileDocumentManager.getInstance().getDocument(file) - if doc != null then { - te match { - case textEditor: TextEditor => - utils.highlightKeywords(doc.getText, textEditor.getEditor) - getRiddlIdeaStates.allStates - .foreach((_, state) => highlightErrorForFile(state, file.getName)) - } - } - } -} diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlTokenizer.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlTokenizer.scala index b78de2d..da05507 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlTokenizer.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/RiddlTokenizer.scala @@ -2,228 +2,8 @@ package com.ossuminc.riddl.plugins.idea.files import com.intellij.ide.highlighter.custom.CustomHighlighterColors import com.intellij.openapi.editor.colors.TextAttributesKey -import com.ossuminc.riddl.plugins.idea.files.utils.splitByBlanks object RiddlTokenizer { - // Outputs: - // String - word - // Int - index - // Boolean - isQuoted, isComment - def tokenize(text: String): Seq[(String, Int, Seq[Boolean])] = { - var currentIndex = 0 - tokenizeWithFlags(text).map { case (word, Seq(isQuoted, isComment)) => - val tuple = (word, currentIndex, Seq(isQuoted, isComment)) - currentIndex += word.length - tuple - } - } - - private def tokenizeWithFlags(text: String): Seq[(String, Seq[Boolean])] = { - var inQuotes: Boolean = false - var inComment: Boolean = false - - def checkForComment(word: String): Unit = - if word.startsWith("//") && !inComment then inComment = true - else if word.contains('\n') && inComment then inComment = false - - def checkForQuotes(word: String): Boolean = - if word.count(c => c == '\"') == 2 then true - else if word.startsWith("\"") then - inQuotes = true - inQuotes - else if word.endsWith("\"") then - inQuotes = false - true - else inQuotes - - splitByBlanks(text).toSeq.foldLeft(List[(String, Seq[Boolean])]()) { - case (acc: List[(String, Seq[Boolean])], word: String) => - checkForComment(word) - acc :+ (word, Seq(checkForQuotes(word), inComment)) - } - } - - val keywords: Seq[String] = Seq( - "acquires", - "adaptor", - "all", - "any", - "append", - "application", - "attachment", - "author", - "become", - "benefit", - "brief", - "briefly", - "body", - "call", - "case", - "capability", - "command", - "commands", - "condition", - "connector", - "constant", - "container", - "contains", - "context", - "create", - "described", - "description", - "details", - "direct", - "presents", - "do", - "domain", - "else", - "email", - "end", - "entity", - "epic", - "error", - "event", - "example", - "execute", - "explanation", - "explained", - "field", - "fields", - "file", - "flow", - "focus", - "for", - "foreach", - "form", - "from", - "fully", - "function", - "graph", - "group", - "handler", - "if", - "import", - "include", - "index", - "init", - "inlet", - "inlets", - "input", - "invariant", - "items", - "label", - "link", - "many", - "mapping", - "merge", - "message", - "module", - "morph", - "name", - "nebula", - "on", - "one", - "organization", - "option", - "optional", - "options", - "other", - "outlet", - "outlets", - "output", - "parallel", - "pipe", - "plant", - "projector", - "query", - "range", - "reference", - "relationship", - "remove", - "replica", - "reply", - "repository", - "requires", - "required", - "record", - "result", - "results", - "return", - "returns", - "reverted", - "router", - "saga", - "schema", - "selects", - "send", - "sequence", - "set", - "show", - "shown", - "sink", - "source", - "split", - "state", - "step", - "stop", - "story", - "streamlet", - "table", - "take", - "tell", - "term", - "then", - "title", - "type", - "url", - "updates", - "user", - "value", - "void", - "when", - "where", - "with" - ) - - val triplePunctuation: Seq[String] = Seq("...", "???") - - val punctuation: Seq[String] = Seq( - "@", - "*", - ",", - ":", - ".", - "=", - "!", - "+", - "?", - "[", - "]", - "|", - "{", - "}", - "(", - ")" - ) - - val readability: Seq[String] = Seq( - "and", - "are", - "as", - "at", - "by", - "for", - "from", - "in", - "is", - "of", - "so", - "that", - "to", - "wants", - "with" - ) - val CUSTOM_KEYWORD_KEYWORD: TextAttributesKey = CustomHighlighterColors.CUSTOM_KEYWORD2_ATTRIBUTES val CUSTOM_KEYWORD_PUNCTUATION: TextAttributesKey = diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/utils.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/utils.scala index 52c8e2b..ed1338e 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/files/utils.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/files/utils.scala @@ -1,181 +1,162 @@ package com.ossuminc.riddl.plugins.idea.files +import com.intellij.openapi.fileEditor.{ + FileDocumentManager, + FileEditorManager, + TextEditor +} import com.intellij.openapi.editor.{DefaultLanguageHighlighterColors, Editor} +import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.editor.markup.{ HighlighterLayer, HighlighterTargetArea } import com.intellij.openapi.editor.colors.TextAttributesKey +import com.ossuminc.riddl.language.AST.{ + CommentTKN, + KeywordTKN, + OtherTKN, + PunctuationTKN, + QuotedStringTKN, + ReadabilityTKN, + Token +} +import com.ossuminc.riddl.language.{At, Messages} +import com.ossuminc.riddl.language.parsing.{RiddlParserInput, TopLevelParser} import com.ossuminc.riddl.plugins.idea.files.RiddlTokenizer.* +import com.ossuminc.riddl.plugins.idea.utils.{ + displayNotification, + highlightErrorForFile +} +import com.ossuminc.riddl.plugins.idea.settings.RiddlIdeaSettings +import com.ossuminc.riddl.plugins.idea.utils.ManagerBasedGetterUtils.getRiddlIdeaStates +import com.ossuminc.riddl.utils.StringLogger object utils { - def splitByBlanks(str: String): Array[String] = - str.split("((?<=\\s)(?=\\S)|(?<=\\S)(?=\\s))") - - def getWholeWordsSubstrings( - doc: String, - eventText: String, - start: Int - ): Seq[(String, Int)] = { - def getSubStringsWithIndices(index: Int, text: String): (String, Int) = { - val indexedStart = index + start - val adjustedStart = - if !doc.charAt(indexedStart).isWhitespace && doc - .lastIndexWhere(_.isWhitespace, indexedStart) > -1 - then doc.lastIndexWhere(_.isWhitespace, indexedStart) + 1 - else if doc.lastIndexWhere(_.isWhitespace, indexedStart) == -1 then 0 - else indexedStart - - val indexedEnd = indexedStart + text.length - val adjustedEnd = - if indexedEnd < doc.length && !doc.charAt(indexedEnd).isWhitespace then - doc.indexWhere(_.isWhitespace, indexedEnd) match { - case -1 => doc.length - case idx => idx - } - else indexedEnd - - val numLeadingSpaces = - "^\\s+".r.findFirstIn(text).map(_.length).getOrElse(0) - val numTrailingSpaces = - "\\s+$".r.findFirstIn(text).map(_.length).getOrElse(0) - - ( - doc - .substring( - adjustedStart + numLeadingSpaces, - adjustedEnd - numTrailingSpaces - ) - .trim, - adjustedStart + numLeadingSpaces - ) - } - - if !eventText.isBlank then - val splitText: Seq[String] = splitByBlanks(eventText).toSeq - val indices: Seq[Int] = splitText.scanLeft(0)((acc, part) => { - acc + part.length - }) - indices - .zip(splitText) - .filter(_._2.trim.nonEmpty) - .map(getSubStringsWithIndices) - else if !doc.isBlank then { - Seq( - getSubStringsWithIndices( - if doc.charAt(start).isWhitespace then 0 else -1, - "" + private def annotateTokensWithBooleans( + ast: Either[Messages.Messages, List[Token]] + ): Seq[(Token, Int, Int, Seq[Boolean])] = ast match { + case Left(msgs) => + displayNotification(msgs.mkString("\n\n")) + Seq((OtherTKN(At()), 0, 0, Seq(false, false))) + case Right(tokens) => + tokens + .map(tok => (tok, tok.at.offset, tok.at.endOffset)) + .zip(tokens.map { + case _: CommentTKN => Seq(false, true) + case _: QuotedStringTKN => Seq(true, false) + case _ => Seq(false, false) + }) + .map((offsetTup, tokSeq) => + (offsetTup._1, offsetTup._2, offsetTup._3, tokSeq) ) - ) - } else Seq() } - def highlightKeywords(text: String, editor: Editor): Unit = { - import com.ossuminc.riddl.plugins.idea.files.RiddlTokenizer.* - RiddlTokenizer.tokenize(text).filter(!_._1.isBlank).foreach { - applyColorToToken(editor) - } + def highlightKeywords(docText: String, editor: Editor): Unit = { + import com.ossuminc.riddl.utils.pc + annotateTokensWithBooleans( + pc.withLogger(StringLogger()) { _ => + TopLevelParser.parseToTokens( + RiddlParserInput( + docText, + "" + ) + ) + } + ).foreach(applyColorToToken(editor)) } - def highlightKeywordsOnChange( - // see applyColorToToken for names of elements in tokens - tokens: Seq[(String, Int, Seq[Boolean])], - editor: Editor - ): Unit = tokens.foreach(applyColorToToken(editor)) - private def applyColorToToken( editor: Editor - )(token: String, index: Int, flags: Seq[Boolean]): Unit = { - def puncIndexInToken(puncList: Seq[String]): Int = - token.indexOf(puncList.find(token.contains).getOrElse("")) - - def callApplyColourKey(): Unit = if keywords.contains(token) then - applyColourKey(editor)( - CUSTOM_KEYWORD_KEYWORD, - index, - token.length, - -1 - ) - else if triplePunctuation.exists(token.contains) then - applyColourKey(editor)( - CUSTOM_KEYWORD_PUNCTUATION, - index, - token.length, - puncIndexInToken(triplePunctuation) - ) - else if punctuation.exists(token.contains) then - token.zipWithIndex - .filter(tokenCharWithIndex => - punctuation.contains(tokenCharWithIndex._1.toString) + )(token: Token, offset: Int, endOffset: Int, flags: Seq[Boolean]): Unit = + token match + case _: KeywordTKN => + applyColourKey(editor)( + CUSTOM_KEYWORD_KEYWORD, + offset, + endOffset - offset ) - .foreach((_, puncIndexInToken) => - applyColourKey(editor)( - CUSTOM_KEYWORD_PUNCTUATION, - index, - 1, - puncIndexInToken - ) + case _: PunctuationTKN => + applyColourKey(editor)( + CUSTOM_KEYWORD_PUNCTUATION, + offset, + endOffset - offset + ) + case _: ReadabilityTKN => + applyColourKey(editor)( + CUSTOM_KEYWORD_READABILITY, + offset, + endOffset - offset + ) + case _ => + applyColourKey(editor)( + DefaultLanguageHighlighterColors.IDENTIFIER, + offset, + endOffset - offset ) - else if readability.contains(token) then - applyColourKey(editor)( - CUSTOM_KEYWORD_READABILITY, - index, - token.length, - -1 - ) - else - applyColourKey(editor)( - DefaultLanguageHighlighterColors.IDENTIFIER, - index, - token.length, - -1 - ) flags match { case Seq(isQuoted, _) if isQuoted => - callApplyColourKey() applyColourKey(editor)( DefaultLanguageHighlighterColors.STRING, - index, - token.length, - -1 + offset, + endOffset - offset ) case Seq(_, isComment) if isComment => applyColourKey(editor)( DefaultLanguageHighlighterColors.LINE_COMMENT, - index, - token.length, - -1 + offset, + endOffset - offset ) - case Seq(_, _) if index > -1 && !token.isBlank => - callApplyColourKey() + case Seq(_, _) => () } - } private def applyColourKey(editor: Editor)( colorKey: TextAttributesKey, index: Int, - length: Int, - puncIndex: Int + length: Int ): Unit = { - val trueIndex: Int = - if puncIndex < 0 then index - else puncIndex + index - editor.getMarkupModel.getAllHighlighters - .find(_.getStartOffset == trueIndex) + .find(_.getStartOffset == index) .foreach(highlighter => editor.getMarkupModel.removeHighlighter(highlighter) ) editor.getMarkupModel.addRangeHighlighter( colorKey, - trueIndex, - trueIndex + length, + index, + index + length, HighlighterLayer.FIRST, HighlighterTargetArea.EXACT_RANGE ) editor.getContentComponent.repaint() } + + def highlightKeywordsAndErrorsForFile( + source: FileEditorManager, + file: VirtualFile + ): Unit = source + .getAllEditors(file) + .foreach { te => + val doc = FileDocumentManager.getInstance().getDocument(file) + if doc != null then { + te match { + case textEditor: TextEditor => + highlightKeywords(doc.getText, textEditor.getEditor) + getRiddlIdeaStates.allStates + .foldRight(Seq[RiddlIdeaSettings.State]()) { (tup, acc) => + tup._2.clearErrorHighlighters() + if tup._2.getMessagesForEditor.exists( + _.loc.source.root.path == file.getPath + ) + then acc :+ tup._2 + else acc + } + .foreach { state => + highlightErrorForFile(state, file.getName) + } + } + } + } } diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettings.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettings.scala index bd70812..d7ec509 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettings.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettings.scala @@ -1,5 +1,8 @@ package com.ossuminc.riddl.plugins.idea.settings +import com.intellij.openapi.editor.markup.{MarkupModel, RangeHighlighter} +import com.ossuminc.riddl.language.Messages.Message + import com.intellij.openapi.components.{ PersistentStateComponent, Storage, @@ -57,6 +60,7 @@ object RiddlIdeaSettings { class State(windowNum: Int) { private var riddlConfPath: Option[String] = None + private var riddlTopLevelPath: Option[String] = None private var riddlRunOutput: Seq[String] = Seq() private var autoCompileOnSave: Boolean = true private var command: String = commands.head @@ -70,11 +74,20 @@ object RiddlIdeaSettings { riddlConfPath.flatMap(readFromOptionsFromConf).toSeq else Seq() + private var messagesForEditor: Seq[Message] = Seq() + private var errorHighlighters: Seq[RangeHighlighter] = Seq() + private var markupModelOpt: Option[MarkupModel] = None + def getWindowNum: Int = windowNum def setConfPath(newPath: Option[String]): Unit = riddlConfPath = newPath def getConfPath: Option[String] = riddlConfPath + def setTopLevelPath(newPath: String): Unit = riddlTopLevelPath = Some( + newPath + ) + def getTopLevelPath: Option[String] = riddlTopLevelPath + def prependRunOutput(newOutput: String): Unit = riddlRunOutput = newOutput +: riddlRunOutput def appendRunOutput(newOutput: String): Unit = riddlRunOutput :+= newOutput @@ -89,12 +102,31 @@ object RiddlIdeaSettings { def getCommand: String = command def getCommonOptions: CommonOptions = commonOptions - def setCommonOptions(newCOs: CommonOptions): Unit = { + def setCommonOptions(newCOs: CommonOptions): Unit = commonOptions = newCOs + + def getMessagesForEditor: Seq[Message] = messagesForEditor + def setMessagesForEditor(newMsgs: Seq[Message]): Unit = + messagesForEditor = newMsgs + + def appendErrorHighlighter( + rangeHighlighter: RangeHighlighter + ): Seq[RangeHighlighter] = + errorHighlighters :+ rangeHighlighter + def clearErrorHighlighters(): Unit = { + markupModelOpt.foreach(mm => + errorHighlighters.foreach(mm.removeHighlighter) + ) + errorHighlighters = Seq() } - def setFromOption(newFromOption: String): Unit = fromOption = - Some(newFromOption) + def setMarkupModel(newModel: MarkupModel): Unit = markupModelOpt = Some( + newModel + ) + + def setFromOption(newFromOption: String): Unit = fromOption = Some( + newFromOption + ) def getFromOption: Option[String] = fromOption def setFromOptionsSeq(newSeq: Seq[String]): Unit = fromOptionsSeq = newSeq diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsComponent.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsComponent.scala index 46bb8dd..880449d 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsComponent.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsComponent.scala @@ -47,6 +47,13 @@ class ConfCondition extends Condition[VirtualFile] { } } +class RiddlCondition extends Condition[VirtualFile] { + def value(virtualFile: VirtualFile): Boolean = { + val fn = virtualFile.getName.toLowerCase + fn.endsWith(".riddl") + } +} + class RiddlIdeaSettingsComponent(private val numToolWindow: Int) { private val state: RiddlIdeaSettings.State = getRiddlIdeaState(numToolWindow) private var areAnyComponentsModified = false @@ -88,7 +95,49 @@ class RiddlIdeaSettingsComponent(private val numToolWindow: Int) { BorderFactory.createTitledBorder("Choose from option") ) + private val topLevelFileTextField = new TextFieldWithBrowseButton() private val confFileTextField = new TextFieldWithBrowseButton() + private val riddlFileDescriptor: FileChooserDescriptor = + FileChooserDescriptorFactory + .createSingleFileDescriptor() + .withFileFilter(RiddlCondition()) + private val confFileDescriptor: FileChooserDescriptor = + FileChooserDescriptorFactory + .createSingleFileDescriptor() + .withFileFilter(ConfCondition()) + + topLevelFileTextField.addBrowseFolderListener( + "Browse for Top Level Path", + null, + getProject, + riddlFileDescriptor + ) + topLevelFileTextField.addPropertyChangeListener(_ => + areAnyComponentsModified = true + ) + if state != null && state.getTopLevelPath.isDefined then + state.getTopLevelPath.foreach(path => + topLevelFileTextField.setText( + path + ) + ) + else topLevelFileTextField.setText(getProject.getBasePath) + + topLevelFileTextField.setBorder( + BorderFactory.createTitledBorder( + "Select the project's top-level .riddl file [for editing]" + ) + ) + + confFileTextField.addBrowseFolderListener( + "Browse for Configuration Path", + null, + getProject, + confFileDescriptor + ) + confFileTextField.addPropertyChangeListener(_ => + areAnyComponentsModified = true + ) confFileTextField.setEditable(false) confFileTextField.setText( if state != null && state.getConfPath.isDefined then @@ -96,14 +145,14 @@ class RiddlIdeaSettingsComponent(private val numToolWindow: Int) { else getProject.getBasePath ) confFileTextField.setBorder( - BorderFactory.createTitledBorder("Select .conf or .riddl File") + BorderFactory.createTitledBorder("Select .conf or .riddl file for console") ) private val commonOptionsPanel: JPanel = new JPanel( new java.awt.GridLayout(0, 3) ) commonOptionsPanel.setBorder( - BorderFactory.createTitledBorder("Select Common Options") + BorderFactory.createTitledBorder("Select CommonOptions for console") ) CommonOptionsUtils.BooleanCommonOptions.foreach { case Some(option) => @@ -159,6 +208,8 @@ class RiddlIdeaSettingsComponent(private val numToolWindow: Int) { private def createComponent(): Unit = { val riddlFormBuilder: FormBuilder = FormBuilder.createFormBuilder + .addComponentFillVertically(new JPanel(), 0) + .addComponent(topLevelFileTextField) .addComponent( commandPicker ) @@ -319,6 +370,7 @@ class RiddlIdeaSettingsComponent(private val numToolWindow: Int) { def getPanel: JPanel = riddlSettingsPanel + def getTopLevelFieldText: String = topLevelFileTextField.getText def getConfFieldText: String = confFileTextField.getText def isModified: Boolean = diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsConfigurable.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsConfigurable.scala index ab03930..2d00577 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsConfigurable.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/settings/RiddlIdeaSettingsConfigurable.scala @@ -6,6 +6,7 @@ import com.ossuminc.riddl.plugins.idea.settings.CommonOptionsUtils.{ IntegerCommonOption } import com.ossuminc.riddl.plugins.idea.utils.ManagerBasedGetterUtils.* +import com.ossuminc.riddl.plugins.idea.utils.ParsingUtils.runCommandForEditor import com.ossuminc.riddl.plugins.idea.utils.ToolWindowUtils.* import org.codehaus.groovy.control.ConfigurationException @@ -25,13 +26,28 @@ class RiddlIdeaSettingsConfigurable(numWindow: Int) extends Configurable { override def isModified: Boolean = component.isModified override def apply(): Unit = { + println(numWindow) val windowState = getRiddlIdeaState(numWindow) windowState.setCommand(component.getPickedCommand) - val fileForPath = File(component.getConfFieldText) + if component.getTopLevelFieldText.endsWith(".riddl") then + val topLevelFile = File(component.getTopLevelFieldText) + if topLevelFile.exists() && topLevelFile.isFile + then { + windowState.setTopLevelPath(component.getTopLevelFieldText) + + runCommandForEditor(numWindow) + + updateToolWindowRunPane(numWindow, fromReload = true) + } else + windowState.appendRunOutput( + "The provided top-level file is invalid - cannot run on edit" + ) + + val confPath = File(component.getConfFieldText) if component.getPickedCommand == "from" && - (fileForPath.exists() && fileForPath.isFile) + (confPath.exists() && confPath.isFile) then { windowState.setConfPath(Some(component.getConfFieldText)) diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/ui/RiddlTerminalConsole.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/ui/RiddlTerminalConsole.scala index 4b800d7..0c865f3 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/ui/RiddlTerminalConsole.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/ui/RiddlTerminalConsole.scala @@ -6,7 +6,7 @@ import com.intellij.execution.ui.ConsoleViewContentType import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.project.Project import com.intellij.ui.awt.RelativePoint -import com.ossuminc.riddl.plugins.idea.riddlErrorRegex +import com.ossuminc.riddl.plugins.idea.utils.riddlErrorRegex import com.ossuminc.riddl.plugins.idea.utils.editorForError import scala.util.matching.Regex @@ -66,6 +66,7 @@ class RiddlTerminalConsole( } } } + this.printHyperlink(textLine + "\n", hyperlinkInfo) } } diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ParsingUtils.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ParsingUtils.scala index 3a887ad..2c08f90 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ParsingUtils.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ParsingUtils.scala @@ -1,9 +1,22 @@ package com.ossuminc.riddl.plugins.idea.utils import com.ossuminc.riddl.commands.Commands -import com.ossuminc.riddl.passes.PassesResult +import com.ossuminc.riddl.language.AST.Root +import com.ossuminc.riddl.language.parsing.{RiddlParserInput, TopLevelParser} +import com.ossuminc.riddl.passes.{Pass, PassesResult} import com.ossuminc.riddl.plugins.idea.settings.RiddlIdeaSettings -import com.ossuminc.riddl.utils.{Logger, Logging, PlatformContext, pc} +import com.ossuminc.riddl.utils.{ + Await, + Logger, + Logging, + PlatformContext, + StringLogger, + pc +} + +import java.nio.file.Paths +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration case class RiddlIdeaPluginLogger( numWindow: Int @@ -24,12 +37,12 @@ object ParsingUtils { def runCommandForWindow( numWindow: Int, - confFile: Option[String] = None + confFile: Option[String] ): Unit = { val windowState: RiddlIdeaSettings.State = getRiddlIdeaState(numWindow) - if windowState.getCommand.nonEmpty || - (windowState.getCommand == "from" & (confFile.isDefined | windowState.getFromOption.isDefined)) + if !windowState.getCommand.isBlank || + (windowState.getCommand == "from" && confFile.isDefined && windowState.getFromOption.isDefined) then pc.withLogger(RiddlIdeaPluginLogger(numWindow)) { _ => pc.withOptions(getRiddlIdeaState(numWindow).getCommonOptions) { _ => @@ -54,4 +67,33 @@ object ParsingUtils { } } + def runCommandForEditor( + numWindow: Int + ): Unit = { + val windowState: RiddlIdeaSettings.State = getRiddlIdeaState(numWindow) + windowState.getTopLevelPath.foreach { path => + println(path) + val rpi: RiddlParserInput = Await.result( + RiddlParserInput.fromPath(path), + FiniteDuration(5, TimeUnit.SECONDS) + ) + + pc.withLogger(StringLogger()) { _ => + pc.withOptions(getRiddlIdeaState(numWindow).getCommonOptions) { _ => + TopLevelParser(rpi, false).parseRootWithURLs match { + case Right((root, _)) => + val passesResult = Pass.runStandardPasses(root) + if passesResult.messages.hasErrors then + println("passesresult") + windowState.setMessagesForEditor( + passesResult.messages.justErrors + ) + case Left((msgs, _)) => + windowState.setMessagesForEditor(msgs) + println() + } + } + } + } + } } diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ToolWindowUtils.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ToolWindowUtils.scala index b50e570..88e7bd2 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ToolWindowUtils.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/ToolWindowUtils.scala @@ -169,15 +169,23 @@ object ToolWindowUtils { else if state.getFromOption.isEmpty then writeToConsole("From command chosen, but no option has been chosen") else (if state.getRunOutput.nonEmpty - then writeStateOutputToConsole() - else if state.getCommand == "from" then + then { + writeStateOutputToConsole() + FileEditorManager + .getInstance(project) + .getSelectedFiles + .toSeq + .foreach(file => highlightErrorForFile(state, file.getName)) + + } else if state.getCommand == "from" then if confFile.exists() && confFile.isFile then runCommandForWindow(numWindow, Some(statePath)) else writeToConsole( s"This window's configuration file:\n " + statePath + "\nwas not found, please configure it in settings" ) - else if fromReload then runCommandForWindow(numWindow)) + else if fromReload then + runCommandForWindow(numWindow, state.getConfPath)) val fileEditorManager = FileEditorManager .getInstance(project) diff --git a/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/utils.scala b/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/utils.scala index 07f58da..f6640fe 100644 --- a/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/utils.scala +++ b/src/main/scala/com/ossuminc/riddl/plugins/idea/utils/utils.scala @@ -2,24 +2,30 @@ package com.ossuminc.riddl.plugins.idea import com.intellij.notification.{Notification, NotificationType, Notifications} import com.intellij.openapi.application.{Application, ApplicationManager} +import com.intellij.openapi.editor.markup.{ + HighlighterLayer, + MarkupModel, + TextAttributes +} import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.markup.{HighlighterLayer, TextAttributes} import com.intellij.openapi.fileEditor.{FileEditorManager, OpenFileDescriptor} import com.intellij.openapi.project.{Project, ProjectManager} import com.intellij.openapi.util.IconLoader import com.intellij.openapi.vfs.{LocalFileSystem, VirtualFile} import com.intellij.util.ui.UIUtil +import com.ossuminc.riddl.language.Messages import com.ossuminc.riddl.plugins.idea.settings.RiddlIdeaSettings +import com.ossuminc.riddl.plugins.idea.utils.ToolWindowUtils.updateToolWindowRunPane import com.ossuminc.riddl.plugins.idea.utils.ManagerBasedGetterUtils.{ getProject, getRiddlIdeaState } -import com.ossuminc.riddl.plugins.idea.riddlErrorRegex import com.typesafe.config.ConfigObject import pureconfig.ConfigSource -import scala.jdk.CollectionConverters.* +import scala.jdk.CollectionConverters.* import java.awt.GridBagConstraints +import java.nio.file.Path import javax.swing.Icon import scala.util.matching.Regex @@ -28,7 +34,7 @@ package object utils { IconLoader.getIcon("images/RIDDL-icon.jpg", classType) object ManagerBasedGetterUtils { - val application: Application = ApplicationManager.getApplication + private val application: Application = ApplicationManager.getApplication def getProject: Project = ProjectManager.getInstance().getOpenProjects.head @@ -39,8 +45,8 @@ package object utils { ) .getState - def getRiddlIdeaState(numToolWindow: Int): RiddlIdeaSettings.State = - getRiddlIdeaStates.getState(numToolWindow) + def getRiddlIdeaState(numWindow: Int): RiddlIdeaSettings.State = + getRiddlIdeaStates.getState(numWindow) } object CreationUtils { @@ -85,7 +91,6 @@ package object utils { LocalFileSystem.getInstance.findFileByPath( s"$pathToConf/$fileName" ) - FileEditorManager .getInstance(getProject) .openTextEditor( @@ -108,41 +113,106 @@ package object utils { .filter(outputBlock => outputBlock.contains(fileName)) .map(_.split("\n").toSeq) ) - .foreach { outputBlock => - riddlErrorRegex.findFirstMatchIn(outputBlock.head) match + .foreach(block => + riddlErrorRegex.findFirstMatchIn(block.head) match case Some(resultMatch: Regex.Match) => - val editor = editorForError( - state.getWindowNum, - resultMatch.group(2), - resultMatch.group(3).toInt, - resultMatch.group(4).toInt - ) - - editor.getMarkupModel.removeAllHighlighters() - Thread.sleep(500) - - if resultMatch.group(1) == "[ERROR]" then - val highlighter = editor.getMarkupModel.addLineHighlighter( - resultMatch.group(3).toInt, - HighlighterLayer.ERROR, - new TextAttributes() - ) - highlighter.setErrorStripeMarkColor( - UIUtil.getErrorForeground - ) - highlighter.setErrorStripeTooltip(outputBlock.tail.mkString("\n")) - else if resultMatch.group(1) == "[WARN]" then - val highlighter = editor.getMarkupModel.addLineHighlighter( - resultMatch.group(3).toInt, - HighlighterLayer.WARNING, - new TextAttributes() - ) - highlighter.setErrorStripeMarkColor( - UIUtil.getToolTipForeground - ) - highlighter.setErrorStripeTooltip(outputBlock.tail.mkString("\n")) + highlightForErrorMessage(state, block, Left(resultMatch)) case _ => () + ) + + def highlightForErrorMessage( + state: RiddlIdeaSettings.State, + outputBlock: Seq[String], + matchOrAtLine: Either[Regex.Match, Messages.Message] + ): Unit = { + val severity = matchOrAtLine match { + case Left(m) => m.group(1) + case Right(m) => + m.kind.severity match { + case s if s < 4 => "[warn]" + case _ => "[error]" + } + } + val fileNameOrPath: String = matchOrAtLine match { + case Left(m) => m.group(2) + case Right(msg) => + if state.getTopLevelPath.isEmpty then "" + else + Path + .of(state.getTopLevelPath.get) + .getParent + .toString + "/" + msg.loc.source.origin } + if fileNameOrPath == "" then + displayNotification( + ".riddl file for editor parsing was not set" + ) + updateToolWindowRunPane(state.getWindowNum) + return + + val lineNumber = matchOrAtLine match { + case Left(m) => m.group(3).toInt - 1 + case Right(msg) => msg.loc.line + } + val col = matchOrAtLine match { + case Left(m) => m.group(3).toInt - 1 + case Right(msg) => msg.loc.col + } + val editor: Editor = matchOrAtLine match { + case Left(_) => + editorForError( + state.getWindowNum, + fileNameOrPath, + lineNumber, + col + ) + case Right(_) => + FileEditorManager + .getInstance(getProject) + .getSelectedTextEditor() + } + val markupModel: MarkupModel = editor.getMarkupModel + state.setMarkupModel(markupModel) + + Thread.sleep(500) + + val msg = outputBlock match { + case outBlock if outBlock.nonEmpty => outBlock.tail.mkString("\n") + case Seq() => + matchOrAtLine match { + case Right(msg) => msg.format + case Left(_) => "" + } + } + + if severity == "[error]" + then + val highlighter = markupModel.addLineHighlighter( + lineNumber, + HighlighterLayer.ERROR, + new TextAttributes() + ) + highlighter.setErrorStripeMarkColor( + UIUtil.getErrorForeground + ) + highlighter.setErrorStripeTooltip(msg) + state.appendErrorHighlighter(highlighter) + else if severity == "[warn]" + then + val highlighter = markupModel.addLineHighlighter( + lineNumber, + HighlighterLayer.WARNING, + new TextAttributes() + ) + highlighter.setErrorStripeMarkColor( + UIUtil.getToolTipForeground + ) + highlighter.setErrorStripeTooltip(msg) + state.appendErrorHighlighter(highlighter) + } + + def riddlErrorRegex: Regex = + """(\[\w+\]) ([\w/_-]+\.riddl)\((\d+):(\d+)\)\:""".r def readFromOptionsFromConf(path: String): Seq[String] = ConfigSource.file(path).load[ConfigObject] match { @@ -152,5 +222,3 @@ package object utils { Seq() } } - -def riddlErrorRegex = """(\[\w+\]) ([\w/_-]+\.riddl)\((\d+):(\d+)\)\:""".r