Skip to content

Commit

Permalink
Merged branch idea243.release into idea243.x
Browse files Browse the repository at this point in the history
  • Loading branch information
builduser committed Jan 8, 2025
2 parents 36edc07 + 1d53299 commit be47611
Show file tree
Hide file tree
Showing 40 changed files with 977 additions and 204 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ The "fast tests" can take over an hour. To get a quick feedback on project healt

> runTypeInferenceTests

### Running a single test / test class with testOnly
Scala Plugin project configuration is different from the standard sbt project.
One of the differences is that you don't need to add the sbt project name before the test name.
Just run `testOnly <test name>` without the project prefix. \
For example:

> testOnly org.jetbrains.plugins.scala.annotator.Scala3HighlightingTestsMix

### GitHub Actions build

The project is configured to build and run the typeInference tests and fast tests with Github Actions. \
Expand Down
5 changes: 3 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ lazy val scalaCommunity: sbt.Project =
scalaLanguageUtils % "test->test;compile->compile",
scalaLanguageUtilsRt % "test->test;compile->compile",
pluginXml,
scalaCli % "test->test;compile->compile"
scalaCli % "test->test;compile->compile",
javaDecompilerIntegration % "test->test" //add only test dependency to run tests from this module
)
.settings(MainProjectSettings)
.settings(
Expand Down Expand Up @@ -750,7 +751,7 @@ lazy val propertiesIntegration =

lazy val javaDecompilerIntegration =
newProject("java-decompiler", file("scala/integration/java-decompiler"))
.dependsOn(scalaImpl)
.dependsOn(scalaImpl % "compile->compile;test->test")
.settings(
intellijPlugins += "org.jetbrains.java.decompiler".toPlugin,
packageMethod := PackagingMethod.MergeIntoOther(scalaCommunity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ import scala.util.control.NonFatal
/**
* The class is responsible for pasting Scala/Worksheet/Sbt files in the Project View.
* If the content which is pasted to Project View is a valid Scala code,
* this class tries to calculate the best file name for the newly created Scala file which can be one of the follows:
* this class tries to calculate the best file name for the newly created Scala file which can be one of these:
* - regular file (*.scala)
* - worksheet file (*.sc), created when the code contains top-level expressions
* - plugins.sbt file when the content contains addSbtPlugin and is pasted to the build module root
*
* For a similar Java implementation see [[com.intellij.ide.JavaFilePasteProvider]]
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
package org.jetbrains.plugins.scala.conversion.copy.plainText

import com.intellij.ide.IdeView
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.actionSystem.{LangDataKeys, PlatformCoreDataKeys}
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.vfs.{VfsUtil, VirtualFile}
import com.intellij.psi.PsiManager
import com.intellij.psi.{PsiDirectory, PsiManager}
import com.intellij.util.ui.TextTransferable
import org.jetbrains.plugins.scala.SlowTests
import org.jetbrains.plugins.scala.extensions.{StringExt, inWriteAction}
import org.jetbrains.plugins.scala.util.assertions.CollectionsAssertions.assertCollectionEquals
import org.jetbrains.sbt.project.SbtExternalSystemImportingTestLike
import org.junit.Assert.{assertEquals, assertNotNull, fail}
import org.junit.Assert.{assertEquals, assertNotNull, assertTrue}
import org.junit.experimental.categories.Category

@Category(Array(classOf[SlowTests]))
class ScalaFilePasteProviderInSbtProjectTest extends SbtExternalSystemImportingTestLike {

override protected def getTestProjectPath: String =
s"scala/conversion/testdata/sbt_projects_for_paste/${getTestName(true)}"

override protected def copyTestProjectToTemporaryDir: Boolean = true

override def setUp(): Unit = {
super.setUp()

Expand Down Expand Up @@ -55,33 +68,85 @@ class ScalaFilePasteProviderInSbtProjectTest extends SbtExternalSystemImportingT
|""".stripMargin

def testAutoCreatePluginSbtFile(): Unit = {
assertExpectedFileName(PastedCodeWithAddSbtPlugin, "project", "plugins.sbt")
doPasteToDirectoryTest(PastedCodeWithAddSbtPlugin, "project", "plugins.sbt")

val SomeOtherName = "worksheet.sc"
assertExpectedFileName(PastedCodeWithoutAddSbtPlugin, "project", SomeOtherName)
assertExpectedFileName(PastedCodeWithAddSbtPlugin, "project/inner", SomeOtherName)
assertExpectedFileName(PastedCodeWithAddSbtPlugin, "src/main/scala", SomeOtherName)
assertExpectedFileName(PastedCodeWithAddSbtPlugin, "", SomeOtherName)
doPasteToDirectoryTest(PastedCodeWithoutAddSbtPlugin, "project", SomeOtherName)
doPasteToDirectoryTest(PastedCodeWithAddSbtPlugin, "project/inner", SomeOtherName)
doPasteToDirectoryTest(PastedCodeWithAddSbtPlugin, "src/main/scala", SomeOtherName)
doPasteToDirectoryTest(PastedCodeWithAddSbtPlugin, "", SomeOtherName)
}

def testAutoCreatePluginSbtFileWithAlreadyExistingPluginsSbt(): Unit = {
doPasteToDirectoryTest(PastedCodeWithAddSbtPlugin, "project", "plugins_1.sbt")
}

private def assertExpectedFileName(
private def doPasteToDirectoryTest(
pastedCode: String,
relativeDirPath: String,
expectedFileName: String
): Unit = {
val psiDirectory = findPsiDirectory(relativeDirPath)
val module = ModuleUtilCore.findModuleForPsiElement(psiDirectory)

// Prepare context before invoking paste action
val dataContext = SimpleDataContext.builder()
.add(PlatformCoreDataKeys.MODULE, module)
.add(LangDataKeys.IDE_VIEW, new IdeView {
override def getDirectories: Array[PsiDirectory] = Array(psiDirectory)

override def getOrChooseDirectory(): PsiDirectory = psiDirectory
})
.build()
CopyPasteManager.getInstance.setContents(new TextTransferable(pastedCode))

// Invoke paste action
val pasteProvider = new ScalaFilePasteProvider()
assertTrue(
"Paste action is not enabled in the context",
pasteProvider.isPasteEnabled(dataContext)
)

val filesBeforePaste = psiDirectory.getVirtualFile.getChildren
inWriteAction {
pasteProvider.performPaste(dataContext)
}
val filesAfterPaste = psiDirectory.getVirtualFile.getChildren
val newFiles = (filesAfterPaste.toSet -- filesBeforePaste.toSet).toSeq

//We need to save documents to files to test their contents
inWriteAction {
saveDocumentContentsToDisk(newFiles)
}

val newFileNames = newFiles.map(_.getName)
assertCollectionEquals(
"Wrong file names are created after pasting to directory",
Seq(expectedFileName),
newFileNames
)

val fileContent = new String(newFiles.head.contentsToByteArray())
assertEquals(
"Newly created file content should equal to the pasted content",
pastedCode.withNormalizedSeparator.trim,
fileContent.withNormalizedSeparator.trim
)
}

private def saveDocumentContentsToDisk(newFiles: Seq[VirtualFile]): Unit = {
val fileDocumentManager = FileDocumentManager.getInstance
val newDocuments = newFiles.map(FileDocumentManager.getInstance().getDocument)
newDocuments.foreach(fileDocumentManager.saveDocument)
}

private def findPsiDirectory(relativeDirPath: String): PsiDirectory = {
val pathParts = relativeDirPath.split('/').filter(_.nonEmpty) // findRelativeFile accepts varargs
val directory: VirtualFile = VfsUtil.findRelativeFile(myProjectRoot, pathParts: _*)
assertNotNull(s"Can't find directory `$relativeDirPath` in `$myProjectRoot`", directory)

val psiDirectory = PsiManager.getInstance(getProject).findDirectory(directory)
assertNotNull(s"Can't find psi directory for directory ${directory.getPath}", psiDirectory)

val module = ModuleUtilCore.findModuleForPsiElement(psiDirectory)

val provider = new ScalaFilePasteProvider()
val nameWithExtension = provider.suggestedScalaFileNameForText(pastedCode, module, Some(psiDirectory)).getOrElse {
fail("Can't create scala file for pasted code").asInstanceOf[Nothing]
}
assertEquals("Suggested file name", expectedFileName, nameWithExtension.fullName)
psiDirectory
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.10.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ThisBuild / version := "0.1.0-SNAPSHOT"

ThisBuild / scalaVersion := "3.3.4"

lazy val root = (project in file("."))
.settings(
name := "root"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.10.3
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
<!--Leaving the action in RunContextPopupGroup as well for easy access from Project Tree view -->
<add-to-group group-id="RunContextPopupGroup" anchor="last"/>
</action>
<action id="Scala.DecompileTasty"
class="org.jetbrains.plugins.scala.decompileToJava.ShowDecompiledTastyAction">
<add-to-group group-id="idea.java.decompiler.action.group" anchor="after" relative-to-action="Scala.DecompileToJava"/>
</action>
</actions>
</idea-plugin>
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ cannot.decompile.filename.colon.message=Cannot decompile {0}: {1}

### org/jetbrains/plugins/scala/decompileToJava/ShowDecompiledClassAsJavaAction.scala
show.decompiled.class.as.java=Show Decompiled Class As Java

### org/jetbrains/plugins/scala/decompileToJava/ShowDecompiledTastyAction.scala
show.decompiled.tasty=Show Decompiled Tasty
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.jetbrains.plugins.scala.decompileToJava

import com.intellij.ide.util.PsiNavigationSupport
import com.intellij.openapi.actionSystem.{ActionUpdateThread, AnAction, AnActionEvent, CommonDataKeys}
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.util.{PsiTreeUtil, PsiUtilBase}
import com.intellij.psi.{PsiClass, PsiClassOwner, PsiElement}
import org.jetbrains.plugins.scala.tasty.TastyFileType

/**
* The action shows the decompiled version of .tasty files in a format of readable Scala code
*
* This class was copied from [[org.jetbrains.java.decompiler.ShowDecompiledClassAction]]
* with the exception that it handles .tasty files instead of .class files
*
* @see [[ShowDecompiledClassAsJavaAction]]
* @see [[org.jetbrains.java.decompiler.ShowDecompiledClassAction]]
*/
class ShowDecompiledTastyAction extends AnAction(ScalaJavaDecompilerBundle.message("show.decompiled.tasty")) {

override def getActionUpdateThread: ActionUpdateThread = ActionUpdateThread.BGT

override def update(e: AnActionEvent): Unit = {
val psiElement = getPsiElement(e)
val visible = psiElement.exists(_.getContainingFile.isInstanceOf[PsiClassOwner])
val enabled = visible && getOriginalFile(psiElement.orNull) != null
e.getPresentation.setVisible(visible)
e.getPresentation.setEnabled(enabled)
}

override def actionPerformed(e: AnActionEvent): Unit = {
val project = e.getProject
if (project == null) return

val file = getOriginalFile(getPsiElement(e).orNull)
if (file == null) return

PsiNavigationSupport.getInstance().createNavigatable(project, file, -1).navigate(true)
}

private def getPsiElement(e: AnActionEvent): Option[PsiElement] = {
val project = e.getProject
if (project == null) return None

val editor = e.getData(CommonDataKeys.EDITOR)
if (editor != null) {
val file = Option(PsiUtilBase.getPsiFileInEditor(editor, project))
file.flatMap(file => Option(file.findElementAt(editor.getCaretModel.getOffset)))
}
else {
Option(e.getData(CommonDataKeys.PSI_ELEMENT))
}
}

private def getOriginalFile(psiElement: PsiElement): VirtualFile = {
val psiClass = PsiTreeUtil.getParentOfType(psiElement, classOf[PsiClass], false)
val file = Option(psiClass).flatMap(cls => Option(cls.getOriginalElement.getContainingFile.getVirtualFile))
file.filter(FileTypeRegistry.getInstance.isFileOfType(_, TastyFileType)).orNull
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.jetbrains.plugins.scala.decompileToJava

import com.intellij.ide.util.PsiNavigationSupport
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.actionSystem.{ActionUiKind, AnAction, AnActionEvent, CommonDataKeys, DataContext, Presentation}
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.psi.search.{FileTypeIndex, GlobalSearchScope}
import com.intellij.psi.{PsiFile, PsiManager}
import junit.framework.TestCase.assertEquals
import org.jetbrains.plugins.scala.base.ScalaLightCodeInsightFixtureTestCase
import org.jetbrains.plugins.scala.extensions.invokeAndWait
import org.jetbrains.plugins.scala.lang.psi.api.ScalaFile
import org.jetbrains.plugins.scala.{ScalaFileType, ScalaVersion}
import org.junit.Assert.{assertFalse, assertNotNull, assertTrue}

import scala.jdk.CollectionConverters.CollectionHasAsScala

class ShowDecompiledTastyActionTest extends ScalaLightCodeInsightFixtureTestCase {

override protected def supportedIn(version: ScalaVersion): Boolean = version.isScala3

private val ActionId = "Scala.DecompileTasty"

private def getAction: AnAction = {
val actionManager = com.intellij.openapi.actionSystem.ActionManager.getInstance()
val registeredAction = actionManager.getAction(ActionId)
assertNotNull(s"Action '$ActionId' should be registered in the ActionManager", registeredAction)
registeredAction
}

def testActionAvailableForScala3LibraryFile(): Unit = {
val action = getAction
val actionPresentation = openFileEditorAndUpdateAction(getProject, action, "CanEqual.scala")
assertTrue(
s"Action '$ActionId' should be available in the given context",
actionPresentation.isEnabledAndVisible
)

val event = createActionEvent(getProject, action)
action.actionPerformed(event)

val editor = FileEditorManager.getInstance(getProject).getSelectedTextEditor
assertEquals("Opened file name", "CanEqual.tasty", editor.getVirtualFile.getName)
}

def testActionNotAvailableForOtherFiles(): Unit = {
val actionPresentation = openFileEditorAndUpdateAction(getProject, getAction, "Option.scala")
assertFalse(
s"Action '$ActionId' should not be enabled in the given context",
actionPresentation.isEnabled
)
assertTrue(
s"Action '$ActionId' should be visible in the given context",
actionPresentation.isVisible
)
}

private def openFileEditorAndUpdateAction(project: Project, action: AnAction, scala2SourceFile: String): Presentation = {
findFileAndOpenEditor(project, scala2SourceFile)
updateActionWithCurrentlySelectedClass(project, action)
}

private def updateActionWithCurrentlySelectedClass(project: Project, action: AnAction): Presentation = {
val event = createActionEvent(project, action)
action.update(event)
event.getPresentation
}

private def createActionEvent(project: Project, action: AnAction) = {
val editor = FileEditorManager.getInstance(project).getSelectedTextEditor
assertNotNull("There should be a selected editor", editor)

val psiFile = PsiManager.getInstance(project).findFile(editor.getVirtualFile)
.ensuring(_ != null, "PsiFile could not be retrieved from the editor's virtual file")
.asInstanceOf[ScalaFile]

val firstDefinitionInFile = psiFile.members.head

val dataContext: DataContext = SimpleDataContext.builder()
.add(CommonDataKeys.PSI_ELEMENT, firstDefinitionInFile)
.add(CommonDataKeys.PROJECT, project)
.build()

val event = AnActionEvent.createEvent(action, dataContext, null, "dummy place", ActionUiKind.TOOLBAR, null)
event
}

private def findFileAndOpenEditor(project: Project, Scala3LibrarySourceFile: String): Unit = {
val scala3PsiFile = findFile(project, Scala3LibrarySourceFile)
openFileInEditor(project, scala3PsiFile)
assertSelectedEditorFileName(project, Scala3LibrarySourceFile)
}

private def assertSelectedEditorFileName(project: Project, ScalaLibrarySourceFile: String): Unit = {
val editor = FileEditorManager.getInstance(project).getSelectedTextEditor
assertNotNull("There should be an open editor", editor)
assertEquals(ScalaLibrarySourceFile, editor.getVirtualFile.getName)
}

private def openFileInEditor(project: Project, psiFile: PsiFile): Unit = {
// Open CanEqual.scala file
invokeAndWait {
PsiNavigationSupport.getInstance
.createNavigatable(project, psiFile.getVirtualFile, psiFile.getTextOffset)
.navigate(true)
}
}

private def closeAllOpenEditors(project: Project): Unit = {
FileEditorManager.getInstance(project).getSelectedEditors.foreach { fileEditor =>
FileEditorManager.getInstance(project).closeFile(fileEditor.getFile)
}
}

private def findFile(project: Project, ScalaLibrarySourceFile: String) = {
FileTypeIndex.getFiles(ScalaFileType.INSTANCE, GlobalSearchScope.everythingScope(project)).asScala
.find(_.getName == ScalaLibrarySourceFile)
.map(vf => PsiManager.getInstance(project).findFile(vf))
.getOrElse(throw new RuntimeException(s"File '$ScalaLibrarySourceFile' not found in the library scope"))
}
}
Loading

0 comments on commit be47611

Please sign in to comment.