[IFT] Rework Python and Java run lessons and adjust them for the new UI

IJ-CR-103403

GitOrigin-RevId: 88218a3ef100af6592eea13a2ea3ee4c3a8070c6
This commit is contained in:
Alexey Merkulov
2023-02-22 14:23:27 +01:00
committed by intellij-monorepo-bot
parent bc6abbde05
commit 0291a4ce92
11 changed files with 247 additions and 66 deletions

View File

@@ -22,5 +22,6 @@
<orderEntry type="library" name="kotlinx-serialization-core" level="project" />
<orderEntry type="library" name="kotlinx-serialization-json" level="project" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.java.execution.impl" />
</component>
</module>

View File

@@ -80,8 +80,9 @@ java.basic.completion.deeper.level=Sometimes, you need to see suggestions for st
Press {0} twice to get them in the lookup.
java.basic.completion.module.promotion=You will find more about refactorings in the {0} module.
java.run.configuration.lets.run=Any code marked with {0} can be run. Let''s run our simple example with {1}. \
Alternatively, you can click {0} and select the {2} item.
java.run.configuration.lets.run=There are more than one way to run your code. \
Let''s try to run it from gutter this time by clicking {0} and selecting the {1} item. \
Alternatively, you can press {2}.
java.postfix.completion.type=Type {0} after the parenthesis to see the list of postfix completion suggestions.
java.postfix.completion.complete=Select {0} from the list or type the same value in the editor. Press {1} to complete the statement.

View File

@@ -3,6 +3,7 @@ package com.intellij.java.ift.lesson.run
import com.intellij.codeInsight.daemon.LineMarkerInfo
import com.intellij.execution.ExecutionBundle
import com.intellij.execution.application.ApplicationConfiguration
import com.intellij.icons.AllIcons
import com.intellij.java.ift.JavaLessonsBundle
import com.intellij.openapi.actionSystem.CommonDataKeys
@@ -35,8 +36,10 @@ class JavaRunConfigurationLesson : CommonRunConfigurationLesson("java.run.config
}
task("RunClass") {
text(JavaLessonsBundle.message("java.run.configuration.lets.run", icon(AllIcons.Actions.Execute), action(it),
strong(ExecutionBundle.message("default.runner.start.action.text").dropMnemonic())))
text(JavaLessonsBundle.message("java.run.configuration.lets.run",
icon(AllIcons.RunConfigurations.TestState.Run),
strong(ExecutionBundle.message("default.runner.start.action.text").dropMnemonic()),
action(it)))
timerCheck { configurations().isNotEmpty() }
//Wait toolwindow
checkToolWindowState("Run", true)
@@ -46,7 +49,16 @@ class JavaRunConfigurationLesson : CommonRunConfigurationLesson("java.run.config
}
}
override val sampleFilePath: String = "src/${JavaRunLessonsUtils.demoClassName}.java"
override fun LessonContext.addAnotherRunConfiguration() {
prepareRuntimeTask {
addNewRunConfigurationFromContext { runConfiguration ->
runConfiguration.name = demoWithParametersName
(runConfiguration as ApplicationConfiguration).programParameters = "hello world"
}
}
}
override val sampleFilePath: String = "src/${demoClassName}.java"
}
internal fun TaskContext.highlightRunGutters(highlightInside: Boolean = false, usePulsation: Boolean = false) {
@@ -56,10 +68,8 @@ internal fun TaskContext.highlightRunGutters(highlightInside: Boolean = false, u
}.componentPart l@{ ui: EditorGutterComponentEx ->
if (CommonDataKeys.EDITOR.getData(ui as DataProvider) != editor) return@l null
val runGutterLines = (0 until editor.document.lineCount).mapNotNull { lineInd ->
val gutter = ui.getGutterRenderers(lineInd).singleOrNull() ?: return@mapNotNull null
if ((gutter as? LineMarkerInfo.LineMarkerGutterIconRenderer<*>)?.featureId == "run") {
if (ui.getGutterRenderers(lineInd).any { (it as? LineMarkerInfo.LineMarkerGutterIconRenderer<*>)?.featureId == "run" })
lineInd
}
else null
}
if (runGutterLines.size < 2) return@l null

View File

@@ -354,21 +354,28 @@ run.debug.module.name=Run and debug
run.debug.module.description=Run your code and fix errors with the IDE visual debugger.
run.configuration.lesson.name=Run configuration
run.configuration.hide.toolwindow=<ide/> automatically opened the {0} tool window. \
Tip: at the top of the {0} tool window, you can see the full run command. Now let''s hide the tool window with {1}.
run.configuration.list.not.shown.warning=Please select the {0} option in the {1} \u2192 {2} menu. It is required to enable the toolbar and \
complete the lesson. Alternatively <callback id="{3}">click</callback> to change the settings.
run.configuration.temporary.to.permanent=For each new run, <ide/> creates a temporary run configuration. \
run.configuration.run.current=Let''s run this simple demo program by the {0} button.
run.configuration.run.current.balloon=Let's run this sample
run.configuration.no.run.configuration=As you see running {0} will not lead to creating run configurations.
run.configuration.temporary.to.permanent=Now <ide/> created a temporary run configuration. \
Temporary configurations are automatically deleted when their number exceeds the default limit of five. \
Let''s convert the temporary configuration into a permanent one. Open the drop-down menu with run configurations.
Let''s convert the temporary configuration into a permanent one. Click the {0} near run/debug actions.
run.configuration.open.additional.menu.balloon=Open the additional actions menu
run.configuration.select.save.configuration=Select {0}.
run.configuration.open.run.configurations.popup=Now let's look at the run configurations popup. Click at the drop-down.
run.configuration.open.expand.all.configurations=Expand all to see all configurations.
run.configuration.hover.generated.configuration=For this lesson we added a configuration with some parameters. \
Run configurations can be run from the inline actions. Firstly, hover the configuration.
run.configuration.run.generated.configuration=Let's run it.
run.configuration.edit.configuration=Suppose you want to change a configuration or create another one manually. \
Then you need to open the drop-down menu again and click {0}. Alternatively, you can use the {1} action.
Then you can hold {0} and click the drop-down configuration button. Or you can open the drop-down menu again and click {1}.
run.configuration.settings.description=This is a place for managing run/debug configurations. \
Here you can set program parameters, JVM arguments, environment variables, and so on.
run.configuration.tip.about.save.configuration.into.file=Tip: Sometimes you might want to save a configuration to its own file. \
Such configurations will be easy to share between colleagues (usually, by using a version control system). \
Now close the settings dialog to finish this lesson.
Such configurations will be easy to share between colleagues (usually, by using a version control system).
run.configuration.close.settings=Now close the settings dialog to finish this lesson.
# Link name to the IDE help
run.configuration.help.link=Run/debug configurations

View File

@@ -1,6 +1,8 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package training.dsl
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.util.text.StringUtil
import training.ui.LearningUiManager
import training.util.replaceSpacesWithNonBreakSpace
@@ -30,6 +32,13 @@ interface LearningDslBase {
return "<icon_idx>$index</icon_idx>"
}
/** Show an icon from action widh [actionId] ID inside lesson step message */
fun actionIcon(actionId: String): String {
val icon = ActionManager.getInstance().getAction(actionId)?.templatePresentation?.icon ?: AllIcons.Toolbar.Unknown
val index = LearningUiManager.getIconIndex(icon)
return "<icon_idx>$index</icon_idx>"
}
fun shortcut(key: String): String {
return "<shortcut>${key}</shortcut>".surroundWithNonBreakSpaces()
}

View File

@@ -4,9 +4,15 @@ package training.dsl
import com.intellij.codeInsight.documentation.DocumentationComponent
import com.intellij.codeInsight.documentation.DocumentationEditorPane
import com.intellij.codeInsight.documentation.QuickDocUtil.isDocumentationV2Enabled
import com.intellij.execution.RunManager
import com.intellij.execution.actions.ConfigurationContext
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.impl.RunManagerImpl
import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl
import com.intellij.execution.ui.layout.impl.JBRunnerTabs
import com.intellij.execution.ui.layout.impl.RunnerLayoutSettings
import com.intellij.ide.IdeBundle
import com.intellij.ide.impl.DataManagerImpl
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.CommonDataKeys
@@ -248,6 +254,8 @@ object LessonUtil {
}
}
fun rawShift() = LessonUtil.rawKeyStroke(KeyStroke.getKeyStroke("SHIFT"))
fun checkToolbarIsShowing(ui: ActionButton): Boolean {
// Some buttons are duplicated to several tab-panels. It is a way to find an active one.
val parentOfType = UIUtil.getParentOfType(JBTabsImpl.Toolbar::class.java, ui)
@@ -395,6 +403,9 @@ object LessonUtil {
}
}
fun lastHighlightedUi(): JComponent? {
return LearningUiHighlightingManager.highlightingComponents.getOrNull(0) as? JComponent
}
}
fun LessonContext.firstLessonCompletedMessage() {
@@ -606,7 +617,8 @@ fun LessonContext.restoreChangedSettingsInformer(restoreSettings: () -> Unit) {
fun LessonContext.highlightButtonById(actionId: String,
highlightInside: Boolean = true,
usePulsation: Boolean = true,
clearHighlights: Boolean = true) {
clearHighlights: Boolean = true,
additionalContent: (TaskContext.() -> Unit)? = null) {
val needToFindButton = getActionById(actionId)
task {
@@ -639,6 +651,7 @@ fun LessonContext.highlightButtonById(actionId: String,
}
}
addStep(feature)
additionalContent?.invoke(this)
}
}
@@ -728,3 +741,14 @@ fun LessonContext.sdkConfigurationTasks() {
langSupport.sdkConfigurationTasks.invoke(this, lesson)
}
}
fun TaskRuntimeContext.addNewRunConfigurationFromContext(editConfiguration: (RunConfiguration) -> Unit = {}) {
val runManager = RunManager.getInstance(project) as RunManagerImpl
val dataContext = DataManagerImpl.getInstance().getDataContext(editor.component)
val configurationsFromContext = ConfigurationContext.getFromContext(dataContext, ActionPlaces.UNKNOWN).configurationsFromContext
val configurationSettings = configurationsFromContext?.singleOrNull() ?.configurationSettings ?: return
val runConfiguration = configurationSettings.configuration.clone()
editConfiguration(runConfiguration)
val newSettings = RunnerAndConfigurationSettingsImpl(runManager, runConfiguration)
runManager.addConfiguration(newSettings)
}

View File

@@ -22,12 +22,15 @@ import org.assertj.swing.timing.Condition
import org.assertj.swing.timing.Pause
import org.assertj.swing.timing.Timeout
import training.ui.IftTestContainerFixture
import training.ui.LearningUiHighlightingManager
import training.ui.LearningUiUtil
import training.ui.LearningUiUtil.findComponentWithTimeout
import training.util.getActionById
import training.util.invokeActionForFocusContext
import java.awt.Component
import java.awt.Container
import java.awt.Point
import java.awt.Rectangle
import java.util.*
import java.util.concurrent.TimeUnit
import javax.swing.*
@@ -44,6 +47,35 @@ class TaskTestContext(rt: TaskRuntimeContext) : TaskRuntimeContext(rt) {
val skipTesting: Boolean = false
)
class HighlightedArea internal constructor(private val fixture: IftTestContainerFixture<*>,
private val component: JComponent,
private val rectangle: Rectangle?) {
fun click() {
if (rectangle != null) {
fixture.robot().click(component, center(rectangle))
}
else {
fixture.robot().click(component)
}
}
fun hover() {
if (rectangle != null) {
fixture.robot().moveMouse(component, center(rectangle))
}
else {
fixture.robot().moveMouse(component)
}
}
private fun center(r: Rectangle) = Point(r.x + r.width / 2, r.y + r.height / 2)
}
val IftTestContainerFixture<*>.highlightedArea : HighlightedArea get() {
val component = LessonUtil.lastHighlightedUi() ?: error("No highlighted component")
return HighlightedArea(this, component, LearningUiHighlightingManager.getRectangle(component))
}
fun type(text: String) {
robot.waitForIdle()
for (element in text) {

View File

@@ -191,7 +191,7 @@ internal class TaskContextImpl(private val lessonExecutor: LessonExecutor,
if (useBalloon != null) {
val ui = useBalloon.highlightingComponent
?: runtimeContext.previous.ui as? JComponent
?: LearningUiHighlightingManager.highlightingComponents.getOrNull(0) as? JComponent
?: LessonUtil.lastHighlightedUi()
?: return
LessonExecutorUtil.showBalloonMessage(text,
ui,

View File

@@ -3,16 +3,23 @@ package training.learn.lesson.general.run
import com.intellij.execution.ExecutionBundle
import com.intellij.execution.RunManager
import com.intellij.execution.ui.RunConfigurationStartHistory
import com.intellij.icons.AllIcons
import com.intellij.ide.ui.UISettings
import com.intellij.idea.ActionsBundle
import com.intellij.openapi.editor.impl.EditorComponentImpl
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.popup.PopupFactoryImpl
import com.intellij.util.IconUtil
import com.intellij.util.ui.JBUI
import training.dsl.*
import training.learn.LessonsBundle
import training.learn.course.KLesson
import training.ui.LearningUiHighlightingManager
import training.ui.LearningUiManager
import javax.swing.JButton
import java.awt.Rectangle
import javax.swing.Icon
import javax.swing.JList
abstract class CommonRunConfigurationLesson(id: String) : KLesson(id, LessonsBundle.message("run.configuration.lesson.name")) {
protected abstract val sample: LessonSample
@@ -22,51 +29,65 @@ abstract class CommonRunConfigurationLesson(id: String) : KLesson(id, LessonsBun
protected fun TaskRuntimeContext.configurations() =
runManager().allSettings.filter { it.name.contains(demoConfigurationName) }
private fun TaskContext.runToolWindow() = strong(ExecutionBundle.message("tool.window.name.run"))
protected val demoWithParametersName: String get() = "$demoConfigurationName with parameters"
private val runIcon: Icon by lazy { IconUtil.toStrokeIcon(AllIcons.Actions.Execute, JBUI.CurrentTheme.RunWidget.RUN_MODE_ICON) }
override val lessonContent: LessonContext.() -> Unit
get() = {
prepareSample(sample)
prepareRuntimeTask {
configurations().forEach { runManager().removeConfiguration(it) }
val configurations = configurations()
for (it in configurations) {
runManager().removeConfiguration(it)
}
RunConfigurationStartHistory.getInstance(project).loadState(RunConfigurationStartHistory.State())
RunManager.getInstance(project).selectedConfiguration = null
LessonUtil.setEditorReadOnly(editor)
}
runTask()
task("HideActiveWindow") {
LearningUiHighlightingManager.clearHighlights()
text(LessonsBundle.message("run.configuration.hide.toolwindow", runToolWindow(), action(it)))
checkToolWindowState("Run", false)
test { actions(it) }
}
showWarningIfRunConfigurationsHidden()
highlightButtonById("Run", highlightInside = false, usePulsation = false)
task {
triggerAndFullHighlight().component { ui: JButton ->
ui.text == demoConfigurationName
text(LessonsBundle.message("run.configuration.run.current", icon(runIcon)))
text(LessonsBundle.message("run.configuration.run.current.balloon"), LearningBalloonConfig(Balloon.Position.below, 0))
checkToolWindowState("Run", true)
test {
ideFrame {
highlightedArea.click()
}
}
}
val saveConfigurationItemName = ExecutionBundle.message("save.temporary.run.configuration.action.name", demoConfigurationName)
.dropMnemonic()
text(LessonsBundle.message("run.configuration.no.run.configuration",
strong(ExecutionBundle.message("run.configurations.combo.run.current.file.selected"))))
runTask()
lateinit var restoreMoreTask: TaskContext.TaskId
highlightButtonById("MoreRunToolbarActions", highlightInside = false, usePulsation = false) {
restoreMoreTask = taskId
}
val saveConfigurationItemName = ExecutionBundle.message("choose.run.popup.save")
task {
text(LessonsBundle.message("run.configuration.temporary.to.permanent"))
text(LessonsBundle.message("run.configuration.temporary.to.permanent", actionIcon("MoreRunToolbarActions")))
text(LessonsBundle.message("run.configuration.open.additional.menu.balloon"),
LearningBalloonConfig(Balloon.Position.below, 0, highlightingComponent = LessonUtil.lastHighlightedUi()))
triggerAndBorderHighlight().listItem { item ->
item.toString() == saveConfigurationItemName
item is PopupFactoryImpl.ActionItem && item.text == saveConfigurationItemName
}
test {
ideFrame {
button(demoConfigurationName).click()
highlightedArea.click()
}
}
}
task {
text(LessonsBundle.message("run.configuration.select.save.configuration", strong(saveConfigurationItemName)))
restoreByUi()
restoreByUi(restoreId = restoreMoreTask, delayMillis = defaultRestoreDelay)
stateCheck {
val selectedConfiguration = RunManager.getInstance(project).selectedConfiguration ?: return@stateCheck false
!selectedConfiguration.isTemporary
@@ -78,11 +99,88 @@ abstract class CommonRunConfigurationLesson(id: String) : KLesson(id, LessonsBun
}
}
addAnotherRunConfiguration()
lateinit var dropDownTask: TaskContext.TaskId
highlightButtonById("RedesignedRunConfigurationSelector", usePulsation = false) {
dropDownTask = taskId
}
task {
text(LessonsBundle.message("run.configuration.open.run.configurations.popup"))
triggerAndBorderHighlight().listItem { item ->
item is PopupFactoryImpl.ActionItem && item.text.contains(ExecutionBundle.message("run.toolbar.widget.all.configurations", ""))
}
test {
ideFrame {
highlightedArea.click()
}
}
}
var foundItem = 0
task {
text(LessonsBundle.message("run.configuration.open.expand.all.configurations"))
triggerAndBorderHighlight().componentPart { jList: JList<*> ->
foundItem = LessonUtil.findItem(jList) { item ->
item is PopupFactoryImpl.ActionItem && item.text == demoWithParametersName
} ?: return@componentPart null
jList.getCellBounds(foundItem, foundItem)
}
restoreByUi(restoreId = dropDownTask)
test {
ideFrame {
highlightedArea.click()
}
}
}
task {
text(LessonsBundle.message("run.configuration.hover.generated.configuration"))
addFutureStep {
val jList = previous.ui as? JList<*> ?: return@addFutureStep
jList.addListSelectionListener { _ ->
if (jList.selectedIndex == foundItem) {
completeStep()
}
}
}
restoreByUi(restoreId = dropDownTask)
test {
ideFrame {
highlightedArea.hover()
}
}
}
task {
before {
val ui = previous.ui as? JList<*> ?: return@before
LearningUiHighlightingManager.highlightPartOfComponent(ui, LearningUiHighlightingManager.HighlightingOptions(highlightInside = false)) {
val itemRect = ui.getCellBounds(foundItem, foundItem)
Rectangle(itemRect.x + itemRect.width - JBUI.scale(110), itemRect.y, JBUI.scale(35), itemRect.height)
}
}
text(LessonsBundle.message("run.configuration.run.generated.configuration"))
stateCheck {
RunConfigurationStartHistory.getInstance(project).history().first().configuration.name == demoWithParametersName
}
restoreByUi(restoreId = dropDownTask)
test {
ideFrame {
highlightedArea.click()
}
}
}
highlightButtonById("RedesignedRunConfigurationSelector", usePulsation = false)
task("editRunConfigurations") {
LearningUiHighlightingManager.clearHighlights()
text(LessonsBundle.message("run.configuration.edit.configuration",
strong(ActionsBundle.message("action.editRunConfigurations.text").dropMnemonic()),
action(it)))
LessonUtil.rawShift(),
strong(ActionsBundle.message("action.editRunConfigurations.text").dropMnemonic())))
triggerAndBorderHighlight().component { ui: JBCheckBox ->
ui.text?.contains(ExecutionBundle.message("run.configuration.store.as.project.file").dropMnemonic()) == true
}
@@ -93,7 +191,14 @@ abstract class CommonRunConfigurationLesson(id: String) : KLesson(id, LessonsBun
task {
text(LessonsBundle.message("run.configuration.settings.description"))
text(LessonsBundle.message("run.configuration.tip.about.save.configuration.into.file"))
gotItStep(Balloon.Position.below, 300, LessonsBundle.message("run.configuration.tip.about.save.configuration.into.file"))
}
task {
before {
LearningUiHighlightingManager.clearHighlights()
}
text(LessonsBundle.message("run.configuration.close.settings"))
stateCheck {
focusOwner is EditorComponentImpl
}
@@ -107,29 +212,9 @@ abstract class CommonRunConfigurationLesson(id: String) : KLesson(id, LessonsBun
restoreUiInformer()
}
protected abstract fun LessonContext.runTask()
protected open fun LessonContext.addAnotherRunConfiguration() {}
private fun LessonContext.showWarningIfRunConfigurationsHidden() {
task {
val step = stateCheck {
UISettings.getInstance().run { showNavigationBar || showMainToolbar }
}
val callbackId = LearningUiManager.addCallback {
UISettings.getInstance().apply {
showNavigationBar = true
fireUISettingsChanged()
}
step.complete(true)
}
showWarning(LessonsBundle.message("run.configuration.list.not.shown.warning",
strong(ActionsBundle.message("action.ViewNavigationBar.text").dropMnemonic()),
strong(ActionsBundle.message("group.ViewMenu.text").dropMnemonic()),
strong(ActionsBundle.message("group.ViewAppearanceGroup.text").dropMnemonic()),
callbackId)) {
UISettings.getInstance().run { !showNavigationBar && !showMainToolbar }
}
}
}
protected abstract fun LessonContext.runTask()
private fun LessonContext.restoreUiInformer() {
if (UISettings.getInstance().run { showNavigationBar || showMainToolbar }) return

View File

@@ -32,7 +32,6 @@ import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.ExperimentalUI
import com.intellij.ui.components.labels.LinkLabel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import org.jetbrains.annotations.Nls
import training.lang.LangManager
import training.lang.LangSupport
@@ -146,6 +145,7 @@ fun invokeActionForFocusContext(action: AnAction) {
DataManager.getInstance().dataContextFromFocusAsync.onSuccess { dataContext ->
invokeLater {
val event = AnActionEvent.createFromAnAction(action, null, ActionPlaces.LEARN_TOOLWINDOW, dataContext)
event.presentation.isPerformGroup = true
performActionDumbAwareWithCallbacks(action, event)
}
}

View File

@@ -31,6 +31,18 @@ class PythonRunConfigurationLesson : CommonRunConfigurationLesson("python.run.co
}
}
override fun LessonContext.addAnotherRunConfiguration() {
prepareRuntimeTask {
addNewRunConfigurationFromContext { runConfiguration ->
runConfiguration.name = demoWithParametersName
if (runConfiguration is PythonRunConfiguration) {
runConfiguration.setNameChangedByUser(true)
runConfiguration.scriptParameters = "hello world"
}
}
}
}
// Redefine the base class links:
override val helpLinks: Map<String, String> get() = mapOf(
Pair(PythonLessonsBundle.message("python.run.configuration.help.link"),