diff --git a/java/java-features-trainer/src/com/intellij/java/ift/lesson/completion/JavaSmartTypeCompletionLesson.kt b/java/java-features-trainer/src/com/intellij/java/ift/lesson/completion/JavaSmartTypeCompletionLesson.kt index 66cb65dd8a5b..11e5e74d6593 100644 --- a/java/java-features-trainer/src/com/intellij/java/ift/lesson/completion/JavaSmartTypeCompletionLesson.kt +++ b/java/java-features-trainer/src/com/intellij/java/ift/lesson/completion/JavaSmartTypeCompletionLesson.kt @@ -1,14 +1,10 @@ // Copyright 2000-2019 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 com.intellij.java.ift.lesson.completion -import com.intellij.java.ift.JavaLessonsBundle import training.dsl.* -import training.dsl.LessonUtil.restoreIfModifiedOrMoved -import training.learn.LessonsBundle -import training.learn.course.KLesson -class JavaSmartTypeCompletionLesson : KLesson("Smart type completion", LessonsBundle.message("smart.completion.lesson.name")) { - val sample: LessonSample = parseLessonSample(""" +class JavaSmartTypeCompletionLesson : SmartTypeCompletionLessonBase() { + override val sample: LessonSample = parseLessonSample(""" import java.lang.String; import java.util.HashSet; import java.util.LinkedList; @@ -34,43 +30,13 @@ class JavaSmartTypeCompletionLesson : KLesson("Smart type completion", LessonsBu } """.trimIndent()) - override val lessonContent: LessonContext.() -> Unit = { - prepareSample(sample) - task { - text(JavaLessonsBundle.message("java.smart.type.completion.apply", action("SmartTypeCompletion"), action("EditorChooseLookupItem"))) - trigger("SmartTypeCompletion") - stateCheck { - val text = editor.document.text - text.contains("strings = arrayBlockingQueue;") || text.contains("strings = linkedList;") - } - restoreIfModifiedOrMoved(sample) - testSmartCompletion() - } + override val firstCompletionItem: String = "linkedList" + override val firstCompletionCheck: String = "strings = linkedList;" + + override val secondCompletionItem: String = "arrayBlockingQueue" + override val secondCompletionCheck: String = "return arrayBlockingQueue.toArray(new String[0]);" + + override fun LessonContext.setCaretForSecondItem() { caret(20, 16) - task { - text(JavaLessonsBundle.message("java.smart.type.completion.return", action("SmartTypeCompletion"), action("EditorChooseLookupItem"))) - trigger("SmartTypeCompletion") - stateCheck { - val text = editor.document.text - text.contains("return arrayBlockingQueue.toArray(new String[0]);") - || text.contains("return strings.toArray(new String[0]);") - } - restoreIfModifiedOrMoved() - testSmartCompletion() - } } - - private fun TaskContext.testSmartCompletion() { - test { - invokeActionViaShortcut("CTRL SHIFT SPACE") - ideFrame { - jListContains("arrayBlockingQueue").item(0).doubleClick() - } - } - } - - override val helpLinks: Map get() = mapOf( - Pair(LessonsBundle.message("help.code.completion"), - LessonUtil.getHelpLink("auto-completing-code.html")), - ) } \ No newline at end of file diff --git a/java/java-features-trainer/src/com/intellij/java/ift/lesson/completion/SmartTypeCompletionLessonBase.kt b/java/java-features-trainer/src/com/intellij/java/ift/lesson/completion/SmartTypeCompletionLessonBase.kt new file mode 100644 index 000000000000..48113d8e9e34 --- /dev/null +++ b/java/java-features-trainer/src/com/intellij/java/ift/lesson/completion/SmartTypeCompletionLessonBase.kt @@ -0,0 +1,69 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.ift.lesson.completion + +import com.intellij.java.ift.JavaLessonsBundle +import training.dsl.LessonContext +import training.dsl.LessonSample +import training.dsl.LessonUtil +import training.dsl.LessonUtil.restoreIfModifiedOrMoved +import training.dsl.TaskContext +import training.learn.LessonsBundle +import training.learn.course.KLesson +import training.util.isToStringContains + +abstract class SmartTypeCompletionLessonBase : KLesson("Smart type completion", LessonsBundle.message("smart.completion.lesson.name")) { + abstract val sample: LessonSample + + abstract val firstCompletionItem: String + abstract val firstCompletionCheck: String + + abstract val secondCompletionItem: String + abstract val secondCompletionCheck: String + + abstract fun LessonContext.setCaretForSecondItem() + + override val lessonContent: LessonContext.() -> Unit = { + prepareSample(sample) + task { + text(JavaLessonsBundle.message("java.smart.type.completion.apply", action("SmartTypeCompletion"), action("EditorChooseLookupItem"))) + trigger("SmartTypeCompletion") + triggerAndBorderHighlight().listItem { + it.isToStringContains(firstCompletionItem) + } + stateCheck { + val text = editor.document.text + text.contains(firstCompletionCheck) + } + restoreIfModifiedOrMoved(sample) + testSmartCompletion(firstCompletionItem) + } + setCaretForSecondItem() + task { + text(JavaLessonsBundle.message("java.smart.type.completion.return", action("SmartTypeCompletion"), action("EditorChooseLookupItem"))) + trigger("SmartTypeCompletion") + triggerAndBorderHighlight().listItem { + it.isToStringContains(secondCompletionItem) + } + stateCheck { + val text = editor.document.text + text.contains(secondCompletionCheck) + } + restoreIfModifiedOrMoved() + testSmartCompletion(secondCompletionItem) + } + } + + private fun TaskContext.testSmartCompletion(item: String) { + test { + invokeActionViaShortcut("CTRL SHIFT SPACE") + ideFrame { + jListContains(item).item(0).doubleClick() + } + } + } + + override val helpLinks: Map get() = mapOf( + Pair(LessonsBundle.message("help.code.completion"), + LessonUtil.getHelpLink("auto-completing-code.html")), + ) +} \ No newline at end of file diff --git a/java/java-features-trainer/src/com/intellij/java/ift/lesson/essential/JavaOnboardingTourLesson.kt b/java/java-features-trainer/src/com/intellij/java/ift/lesson/essential/JavaOnboardingTourLesson.kt index b76a4374dcff..a4c881b3f673 100644 --- a/java/java-features-trainer/src/com/intellij/java/ift/lesson/essential/JavaOnboardingTourLesson.kt +++ b/java/java-features-trainer/src/com/intellij/java/ift/lesson/essential/JavaOnboardingTourLesson.kt @@ -2,113 +2,22 @@ package com.intellij.java.ift.lesson.essential import com.intellij.codeInsight.daemon.impl.analysis.HighlightingFeature -import com.intellij.execution.RunManager -import com.intellij.execution.ui.UIExperiment -import com.intellij.icons.AllIcons -import com.intellij.ide.DataManager -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereManagerImpl -import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUI -import com.intellij.ide.ui.UISettings -import com.intellij.ide.util.PropertiesComponent -import com.intellij.ide.util.gotoByName.GotoActionModel -import com.intellij.idea.ActionsBundle import com.intellij.java.ift.JavaLessonsBundle -import com.intellij.java.ift.JavaProjectUtil -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.ActionPlaces -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.ex.ActionUtil -import com.intellij.openapi.actionSystem.impl.ActionButton -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.application.runWriteAction -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.editor.LogicalPosition -import com.intellij.openapi.editor.actions.ToggleCaseAction -import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.module.LanguageLevelUtil import com.intellij.openapi.module.ModuleManager -import com.intellij.openapi.progress.runBackgroundableTask import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectBundle -import com.intellij.openapi.projectRoots.JavaSdk -import com.intellij.openapi.projectRoots.SdkType -import com.intellij.openapi.roots.ui.configuration.SdkDetector -import com.intellij.openapi.ui.MessageDialogBuilder -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.ex.MultiLineLabel import com.intellij.openapi.ui.popup.Balloon -import com.intellij.openapi.util.NlsSafe -import com.intellij.openapi.util.WindowStateService -import com.intellij.openapi.wm.ToolWindowManager -import com.intellij.openapi.wm.impl.FocusManagerImpl -import com.intellij.ui.IdeUICustomization -import com.intellij.ui.UIBundle -import com.intellij.ui.components.fields.ExtendableTextField -import com.intellij.ui.components.panels.NonOpaquePanel -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.tree.TreeVisitor -import com.intellij.util.PlatformUtils -import com.intellij.util.ui.UIUtil -import com.intellij.util.ui.tree.TreeUtil -import com.intellij.xdebugger.XDebuggerManager import com.siyeh.InspectionGadgetsBundle import com.siyeh.IntentionPowerPackBundle -import kotlinx.serialization.json.JsonObjectBuilder -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonArray -import kotlinx.serialization.json.put import org.jetbrains.annotations.Nls -import training.FeaturesTrainerIcons import training.dsl.* -import training.dsl.LessonUtil.adjustSearchEverywherePosition -import training.dsl.LessonUtil.checkEditorModification -import training.dsl.LessonUtil.restoreIfModified import training.dsl.LessonUtil.restoreIfModifiedOrMoved -import training.dsl.LessonUtil.restorePopupPosition -import training.learn.LearnBundle -import training.learn.LessonsBundle -import training.learn.course.KLesson -import training.learn.course.LessonProperties -import training.learn.lesson.LessonManager -import training.learn.lesson.general.run.clearBreakpoints -import training.learn.lesson.general.run.toggleBreakpointTask -import training.project.ProjectUtils -import training.ui.LearningUiHighlightingManager -import training.ui.LearningUiManager -import training.ui.getFeedbackProposedPropertyName import training.util.* -import java.awt.Point -import java.awt.event.KeyEvent -import java.util.concurrent.CompletableFuture -import javax.swing.JTree -import javax.swing.JWindow -import javax.swing.tree.TreePath -class JavaOnboardingTourLesson : KLesson("java.onboarding", JavaLessonsBundle.message("java.onboarding.lesson.name")) { - private lateinit var openLearnTaskId: TaskContext.TaskId - private var useDelay: Boolean = false - - private val demoFileDirectory: String = "src" - private val demoFileNameWithoutExtension: String = "Welcome" - private val demoFileName: String = "$demoFileNameWithoutExtension.java" - - private val uiSettings get() = UISettings.getInstance() - - override val properties: LessonProperties = LessonProperties( - canStartInDumbMode = true, - openFileAtStart = false - ) - - override val testScriptProperties: TaskTestContext.TestScriptProperties = TaskTestContext.TestScriptProperties(skipTesting = true) - - private var backupPopupLocation: Point? = null - private var hideToolStripesPreference = false - private var showNavigationBarPreference = true - - @NlsSafe - private var jdkAtStart: String = "undefined" - - val sample: LessonSample = parseLessonSample(""" +class JavaOnboardingTourLesson : OnboardingTourLessonBase("java.onboarding") { + override val demoFileExtension: String = "java" + override val learningProjectName: String = "IdeaLearningProject" + override val sample: LessonSample = parseLessonSample(""" import java.util.Arrays; import java.util.List; @@ -128,459 +37,9 @@ class JavaOnboardingTourLesson : KLesson("java.onboarding", JavaLessonsBundle.me } """.trimIndent()) - override val lessonContent: LessonContext.() -> Unit = { - prepareRuntimeTask { - jdkAtStart = getCurrentJdkVersionString(project) - useDelay = true - invokeActionForFocusContext(getActionById("Stop")) - val runManager = RunManager.getInstance(project) - runManager.allSettings.forEach(runManager::removeConfiguration) + override val completionStepExpectedCompletion: String = "length" - val root = ProjectUtils.getCurrentLearningProjectRoot() - val srcDir = root.findChild(demoFileDirectory) ?: error("'src' directory not found.") - if (srcDir.findChild(demoFileName) == null) invokeLater { - runWriteAction { - srcDir.createChildData(this, demoFileName) - // todo: This file shows with .java extension in the Project view and this extension disappears when user open it - // (because we fill the file after the user open it) Fill the file immediately in this place? - } - } - } - clearBreakpoints() - - checkUiSettings() - - projectTasks() - - prepareSample(sample, checkSdkConfiguration = false) - - openLearnToolwindow() - - sdkConfigurationTasks() - - waitIndexingTasks() - - runTasks() - - debugTasks() - - completionSteps() - - waitBeforeContinue(500) - - contextActions() - - waitBeforeContinue(500) - - searchEverywhereTasks() - - task { - text(JavaLessonsBundle.message("java.onboarding.epilog", - getCallBackActionId("CloseProject"), - LessonUtil.returnToWelcomeScreenRemark(), - LearningUiManager.addCallback { LearningUiManager.resetModulesView() })) - } - } - - override fun onLessonEnd(project: Project, lessonEndInfo: LessonEndInfo) { - prepareFeedbackData(project, lessonEndInfo) - restorePopupPosition(project, SearchEverywhereManagerImpl.LOCATION_SETTINGS_KEY, backupPopupLocation) - backupPopupLocation = null - - uiSettings.hideToolStripes = hideToolStripesPreference - uiSettings.showNavigationBar = showNavigationBarPreference - uiSettings.fireUISettingsChanged() - - if (!lessonEndInfo.lessonPassed) { - LessonUtil.showFeedbackNotification(this, project) - return - } - val dataContextPromise = DataManager.getInstance().dataContextFromFocusAsync - invokeLater { - val result = MessageDialogBuilder.yesNoCancel(JavaLessonsBundle.message("java.onboarding.finish.title"), - JavaLessonsBundle.message("java.onboarding.finish.text", - LessonUtil.returnToWelcomeScreenRemark())) - .yesText(JavaLessonsBundle.message("java.onboarding.finish.exit")) - .noText(JavaLessonsBundle.message("java.onboarding.finish.modules")) - .icon(FeaturesTrainerIcons.PluginIcon) - .show(project) - - when (result) { - Messages.YES -> invokeLater { - LessonManager.instance.stopLesson() - val closeAction = getActionById("CloseProject") - dataContextPromise.onSuccess { context -> - invokeLater { - val event = AnActionEvent.createFromAnAction(closeAction, null, ActionPlaces.LEARN_TOOLWINDOW, context) - ActionUtil.performActionDumbAwareWithCallbacks(closeAction, event) - } - } - } - Messages.NO -> invokeLater { - LearningUiManager.resetModulesView() - } - } - if (result != Messages.YES) { - LessonUtil.showFeedbackNotification(this, project) - } - } - } - - private fun prepareFeedbackData(project: Project, lessonEndInfo: LessonEndInfo) { - val primaryLanguage = module.primaryLanguage - if (primaryLanguage == null) { - thisLogger().error("Onboarding lesson has no language support for some magical reason") - return - } - val configPropertyName = getFeedbackProposedPropertyName(primaryLanguage) - if (PropertiesComponent.getInstance().getBoolean(configPropertyName, false)) { - return - } - - val jdkVersionsFuture = CompletableFuture>() - runBackgroundableTask(ProjectBundle.message("progress.title.detecting.sdks"), project, false) { indicator -> - val jdkVersions = mutableListOf() - SdkDetector.getInstance().detectSdks(JavaSdk.getInstance(), indicator, object : SdkDetector.DetectedSdkListener { - override fun onSdkDetected(type: SdkType, version: String, home: String) { - jdkVersions.add(version) - } - - override fun onSearchCompleted() { - jdkVersionsFuture.complete(jdkVersions) - } - }) - } - - @Suppress("HardCodedStringLiteral") - val currentJdkVersion: @NlsSafe String = getCurrentJdkVersionString(project) - - val module = ModuleManager.getInstance(project).modules.first() - - @Suppress("HardCodedStringLiteral") - val currentLanguageLevel: @NlsSafe String = LanguageLevelUtil.getEffectiveLanguageLevel(module).name - - primaryLanguage.onboardingFeedbackData = object : OnboardingFeedbackData("IDEA Onboarding Tour Feedback", lessonEndInfo) { - override val feedbackReportId = "idea_onboarding_tour" - - override val additionalFeedbackFormatVersion: Int = 1 - - private val jdkVersions: List? by lazy { - if (jdkVersionsFuture.isDone) jdkVersionsFuture.get() else null - } - - override val addAdditionalSystemData: JsonObjectBuilder.() -> Unit = { - put("jdk_at_start", jdkAtStart) - put("current_jdk", currentJdkVersion) - put("language_level", currentLanguageLevel) - put("found_jdk", buildJsonArray { - for (version in jdkVersions ?: emptyList()) { - add(JsonPrimitive(version)) - } - }) - } - - override val addRowsForUserAgreement: Panel.() -> Unit = { - row(JavaLessonsBundle.message("java.onboarding.feedback.system.found.jdks")) { - val versions: @NlsSafe String = jdkVersions?.joinToString("\n") ?: "none" - cell(MultiLineLabel(versions)) - } - row(JavaLessonsBundle.message("java.onboarding.feedback.system.jdk.at.start")) { - label(jdkAtStart) - } - row(JavaLessonsBundle.message("java.onboarding.feedback.system.current.jdk")) { - label(currentJdkVersion) - } - row(JavaLessonsBundle.message("java.onboarding.feedback.system.lang.level")) { - label(currentLanguageLevel) - } - } - - override fun feedbackHasBeenProposed() { - PropertiesComponent.getInstance().setValue(configPropertyName, true, false) - } - } - } - - private fun getCurrentJdkVersionString(project: Project): String { - return JavaProjectUtil.getEffectiveJdk(project)?.let { JavaSdk.getInstance().getVersionString(it) } ?: "none" - } - - private fun LessonContext.debugTasks() { - clearBreakpoints() - - var logicalPosition = LogicalPosition(0, 0) - prepareRuntimeTask { - logicalPosition = editor.offsetToLogicalPosition(sample.startOffset) - } - caret(sample.startOffset) - - toggleBreakpointTask(sample, { logicalPosition }, checkLine = false) { - text(JavaLessonsBundle.message("java.onboarding.balloon.click.here"), - LearningBalloonConfig(Balloon.Position.below, width = 0, cornerToPointerDistance = 20)) - text(JavaLessonsBundle.message("java.onboarding.toggle.breakpoint.1", - code("6.5"), code("findAverage"), code("26"))) - text(JavaLessonsBundle.message("java.onboarding.toggle.breakpoint.2")) - } - - highlightButtonById("Debug", highlightInside = false, usePulsation = false) - - actionTask("Debug") { - showBalloonOnHighlightingComponent(JavaLessonsBundle.message("java.onboarding.balloon.start.debugging")) - restoreState { - lineWithBreakpoints() != setOf(logicalPosition.line) - } - restoreIfModified(sample) - JavaLessonsBundle.message("java.onboarding.start.debugging", icon(AllIcons.Actions.StartDebugger)) - } - - lateinit var debuggerGotItTaskId: TaskContext.TaskId - task { - debuggerGotItTaskId = taskId - } - - highlightDebugActionsToolbar() - - task { - rehighlightPreviousUi = true - gotItStep(Balloon.Position.above, width = 0, - JavaLessonsBundle.message("java.onboarding.balloon.about.debug.panel", - strong(UIBundle.message("tool.window.name.debug")), - strong(LessonsBundle.message("debug.workflow.lesson.name")))) - restoreByUi(debuggerGotItTaskId) - } - - highlightButtonById("Stop", highlightInside = false, usePulsation = false) - task { - val position = if (UIExperiment.isNewDebuggerUIEnabled()) Balloon.Position.above else Balloon.Position.atRight - showBalloonOnHighlightingComponent(JavaLessonsBundle.message("java.onboarding.balloon.stop.debugging"), - position) { list -> list.maxByOrNull { it.locationOnScreen.y } } - text(JavaLessonsBundle.message("java.onboarding.stop.debugging", icon(AllIcons.Actions.Suspend))) - restoreIfModified(sample) - stateCheck { - XDebuggerManager.getInstance(project).currentSession == null - } - } - - prepareRuntimeTask { - LearningUiHighlightingManager.clearHighlights() - } - } - - private fun LessonContext.waitIndexingTasks() { - task { - triggerAndBorderHighlight().component { progress: NonOpaquePanel -> - progress.javaClass.name.contains("InlineProgressPanel") - } - } - - task { - text(JavaLessonsBundle.message("java.onboarding.indexing.description")) - text(JavaLessonsBundle.message("java.onboarding.wait.indexing"), LearningBalloonConfig(Balloon.Position.above, 0)) - waitSmartModeStep() - } - - waitBeforeContinue(300) - - prepareRuntimeTask { - LearningUiHighlightingManager.clearHighlights() - } - } - - private fun LessonContext.runTasks() { - highlightRunToolbar(highlightInside = false, usePulsation = false) - - task { - triggerUI { - clearPreviousHighlights = false - }.component { ui: ActionButton -> ActionManager.getInstance().getId(ui.action) == "Run" } - } - - task { - val introductionText = JavaLessonsBundle.message("java.onboarding.temporary.configuration.description", - strong(ActionsBundle.actionText("NewUiRunWidget")), - icon(AllIcons.Actions.Execute), - icon(AllIcons.Actions.StartDebugger)) - val runOptionsText = if (PlatformUtils.isIdeaUltimate()) { - JavaLessonsBundle.message("java.onboarding.run.options.ultimate", - icon(AllIcons.Actions.Profile), - icon(AllIcons.General.RunWithCoverage), - icon(AllIcons.Actions.More)) - } - else { - JavaLessonsBundle.message("java.onboarding.run.options.community", - icon(AllIcons.General.RunWithCoverage), - icon(AllIcons.Actions.More)) - } - - text("$introductionText $runOptionsText") - text(JavaLessonsBundle.message("java.onboarding.run.sample", icon(AllIcons.Actions.Execute), action("Run"))) - text(JavaLessonsBundle.message("java.onboarding.run.sample.balloon", icon(AllIcons.Actions.Execute), action("Run")), - LearningBalloonConfig(Balloon.Position.below, 0)) - checkToolWindowState("Run", true) - restoreIfModified(sample) - } - } - - private fun LessonContext.openLearnToolwindow() { - task { - triggerAndBorderHighlight().component { stripe: ActionButton -> - stripe.action.templateText == LearnBundle.message("toolwindow.stripe.Learn") - } - } - - task { - openLearnTaskId = taskId - text(JavaLessonsBundle.message("java.onboarding.balloon.open.learn.toolbar", strong(LearnBundle.message("toolwindow.stripe.Learn"))), - LearningBalloonConfig(Balloon.Position.atRight, width = 0, duplicateMessage = true)) - stateCheck { - ToolWindowManager.getInstance(project).getToolWindow("Learn")?.isVisible == true - } - restoreIfModified(sample) - } - - prepareRuntimeTask { - LearningUiHighlightingManager.clearHighlights() - requestEditorFocus() - } - } - - - private fun LessonContext.checkUiSettings() { - hideToolStripesPreference = uiSettings.hideToolStripes - showNavigationBarPreference = uiSettings.showNavigationBar - - showInvalidDebugLayoutWarning() - - if (!hideToolStripesPreference && (showNavigationBarPreference || uiSettings.showMainToolbar)) { - // a small hack to have same tasks count. It is needed to track statistics result. - task { } - task { } - return - } - - task { - text(JavaLessonsBundle.message("java.onboarding.change.ui.settings")) - proceedLink() - } - - prepareRuntimeTask { - uiSettings.hideToolStripes = false - uiSettings.showNavigationBar = true - uiSettings.fireUISettingsChanged() - } - } - - private fun LessonContext.projectTasks() { - prepareRuntimeTask { - LessonUtil.hideStandardToolwindows(project) - } - - task { - triggerAndBorderHighlight().component { stripe: ActionButton -> - stripe.action.templateText == IdeUICustomization.getInstance().getProjectViewTitle(project) - } - } - - lateinit var openProjectViewTask: TaskContext.TaskId - task { - openProjectViewTask = taskId - var projectDirExpanded = false - - text(JavaLessonsBundle.message("java.onboarding.project.view.description", - action("ActivateProjectToolWindow"))) - text(JavaLessonsBundle.message("java.onboarding.balloon.project.view"), - LearningBalloonConfig(Balloon.Position.atRight, width = 0, cornerToPointerDistance = 8)) - triggerUI().treeItem { tree: JTree, path: TreePath -> - val result = path.pathCount >= 2 && path.getPathComponent(1).isToStringContains("IdeaLearningProject") - if (result) { - if (!projectDirExpanded) { - invokeLater { tree.expandPath(path) } - } - projectDirExpanded = true - } - result - } - } - - task { - var srcDirCollapsed = false - triggerAndBorderHighlight().treeItem { tree: JTree, path: TreePath -> - val result = path.pathCount >= 3 - && path.getPathComponent(1).isToStringContains("IdeaLearningProject") - && path.getPathComponent(2).isToStringContains(demoFileDirectory) - if (result) { - if (!srcDirCollapsed) { - invokeLater { tree.collapsePath(path) } - } - srcDirCollapsed = true - } - result - } - } - - fun isDemoFilePath(path: TreePath) = - path.pathCount >= 4 && path.getPathComponent(3).isToStringContains(demoFileNameWithoutExtension) - - task { - text(JavaLessonsBundle.message("java.onboarding.balloon.source.directory", strong(demoFileDirectory)), - LearningBalloonConfig(Balloon.Position.atRight, duplicateMessage = true, width = 0)) - triggerAndBorderHighlight().treeItem { _: JTree, path: TreePath -> - isDemoFilePath(path) - } - restoreByUi(openProjectViewTask) - } - - task { - text(JavaLessonsBundle.message("java.onboarding.balloon.open.file", strong(demoFileName)), - LearningBalloonConfig(Balloon.Position.atRight, duplicateMessage = true, width = 0)) - stateCheck l@{ - if (FileEditorManager.getInstance(project).selectedTextEditor == null) return@l false - virtualFile.name == demoFileName - } - restoreState { - (previous.ui as? JTree)?.takeIf { tree -> - TreeUtil.visitVisibleRows(tree, TreeVisitor { path -> - if (isDemoFilePath(path)) TreeVisitor.Action.INTERRUPT else TreeVisitor.Action.CONTINUE - }) != null - }?.isShowing?.not() ?: true - } - } - } - - private fun LessonContext.completionSteps() { - prepareRuntimeTask { - setSample(sample.insertAtPosition(2, " / values")) - FocusManagerImpl.getInstance(project).requestFocusInProject(editor.contentComponent, project) - } - - task { - val textToFind = "result / values" - triggerOnEditorText(textToFind, centerOffset = textToFind.length) - } - - task { - text(JavaLessonsBundle.message("java.onboarding.type.division", - code(" / values"))) - text(JavaLessonsBundle.message("java.onboarding.invoke.completion", code("."))) - text(JavaLessonsBundle.message("java.onboarding.invoke.completion.balloon", code(".")), - LearningBalloonConfig(Balloon.Position.below, width = 0)) - triggerAndBorderHighlight().listItem { // no highlighting - it.isToStringContains("length") - } - proposeRestoreForInvalidText(".") - } - - task { - text(JavaLessonsBundle.message("java.onboarding.choose.values.item", - code("length"), action("EditorChooseLookupItem"))) - text(JavaLessonsBundle.message("java.onboarding.invoke.completion.tip", action("CodeCompletion"))) - stateCheck { - checkEditorModification(sample, modificationPositionId = 2, needChange = "/values.length") - } - } - } - - private fun LessonContext.contextActions() { + override fun LessonContext.contextActions() { val quickFixMessage = InspectionGadgetsBundle.message("foreach.replace.quickfix") caret(sample.getPosition(3)) @@ -643,50 +102,4 @@ class JavaOnboardingTourLesson : KLesson("java.onboarding", JavaLessonsBundle.me restoreByUi(delayMillis = defaultRestoreDelay) } } - - private fun LessonContext.searchEverywhereTasks() { - val toggleCase = ActionsBundle.message("action.EditorToggleCase.text") - caret("AVERAGE", select = true) - task("SearchEverywhere") { - text(JavaLessonsBundle.message("java.onboarding.invoke.search.everywhere.1", - strong(toggleCase), code("AVERAGE"))) - text(JavaLessonsBundle.message("java.onboarding.invoke.search.everywhere.2", - LessonUtil.rawKeyStroke(KeyEvent.VK_SHIFT), LessonUtil.actionName(it))) - triggerAndBorderHighlight().component { ui: ExtendableTextField -> - UIUtil.getParentOfType(SearchEverywhereUI::class.java, ui) != null - } - restoreIfModifiedOrMoved() - } - - task { - transparentRestore = true - before { - if (backupPopupLocation != null) return@before - val ui = previous.ui ?: return@before - val popupWindow = UIUtil.getParentOfType(JWindow::class.java, ui) ?: return@before - val oldPopupLocation = WindowStateService.getInstance(project).getLocation(SearchEverywhereManagerImpl.LOCATION_SETTINGS_KEY) - if (adjustSearchEverywherePosition(popupWindow, "of array ") || LessonUtil.adjustPopupPosition(project, popupWindow)) { - backupPopupLocation = oldPopupLocation - } - } - text(JavaLessonsBundle.message("java.onboarding.search.everywhere.description", - code("AVERAGE"), code(JavaLessonsBundle.message("toggle.case.part")))) - triggerAndBorderHighlight().listItem { item -> - val value = (item as? GotoActionModel.MatchedValue)?.value - (value as? GotoActionModel.ActionWrapper)?.action is ToggleCaseAction - } - restoreByUi() - restoreIfModifiedOrMoved() - } - - task { - text(JavaLessonsBundle.message("java.onboarding.apply.action", strong(toggleCase), LessonUtil.rawEnter())) - stateCheck { - editor.document.text.contains("\"average") - } - restoreByUi(delayMillis = defaultRestoreDelay) - } - - text(JavaLessonsBundle.message("java.onboarding.case.changed")) - } } \ No newline at end of file diff --git a/java/java-features-trainer/src/com/intellij/java/ift/lesson/essential/OnboardingTourLessonBase.kt b/java/java-features-trainer/src/com/intellij/java/ift/lesson/essential/OnboardingTourLessonBase.kt new file mode 100644 index 000000000000..b3120d60ab7e --- /dev/null +++ b/java/java-features-trainer/src/com/intellij/java/ift/lesson/essential/OnboardingTourLessonBase.kt @@ -0,0 +1,615 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.ift.lesson.essential + +import com.intellij.execution.RunManager +import com.intellij.execution.ui.UIExperiment +import com.intellij.icons.AllIcons +import com.intellij.ide.DataManager +import com.intellij.ide.actions.searcheverywhere.SearchEverywhereManagerImpl +import com.intellij.ide.actions.searcheverywhere.SearchEverywhereUI +import com.intellij.ide.ui.UISettings +import com.intellij.ide.util.PropertiesComponent +import com.intellij.ide.util.gotoByName.GotoActionModel +import com.intellij.idea.ActionsBundle +import com.intellij.java.ift.JavaLessonsBundle +import com.intellij.java.ift.JavaProjectUtil +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.ex.ActionUtil +import com.intellij.openapi.actionSystem.impl.ActionButton +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.editor.actions.ToggleCaseAction +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.module.LanguageLevelUtil +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.progress.runBackgroundableTask +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectBundle +import com.intellij.openapi.projectRoots.JavaSdk +import com.intellij.openapi.projectRoots.SdkType +import com.intellij.openapi.roots.ui.configuration.SdkDetector +import com.intellij.openapi.ui.MessageDialogBuilder +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.ex.MultiLineLabel +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.util.NlsSafe +import com.intellij.openapi.util.WindowStateService +import com.intellij.openapi.wm.ToolWindowManager +import com.intellij.openapi.wm.impl.FocusManagerImpl +import com.intellij.ui.IdeUICustomization +import com.intellij.ui.UIBundle +import com.intellij.ui.components.fields.ExtendableTextField +import com.intellij.ui.components.panels.NonOpaquePanel +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.tree.TreeVisitor +import com.intellij.util.PlatformUtils +import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.tree.TreeUtil +import com.intellij.xdebugger.XDebuggerManager +import kotlinx.serialization.json.JsonObjectBuilder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.put +import training.FeaturesTrainerIcons +import training.dsl.* +import training.dsl.LessonUtil.adjustSearchEverywherePosition +import training.dsl.LessonUtil.checkEditorModification +import training.dsl.LessonUtil.restoreIfModified +import training.dsl.LessonUtil.restoreIfModifiedOrMoved +import training.dsl.LessonUtil.restorePopupPosition +import training.learn.LearnBundle +import training.learn.LessonsBundle +import training.learn.course.KLesson +import training.learn.course.LessonProperties +import training.learn.lesson.LessonManager +import training.learn.lesson.general.run.clearBreakpoints +import training.learn.lesson.general.run.toggleBreakpointTask +import training.project.ProjectUtils +import training.ui.LearningUiHighlightingManager +import training.ui.LearningUiManager +import training.ui.getFeedbackProposedPropertyName +import training.util.* +import java.awt.Point +import java.awt.event.KeyEvent +import java.util.concurrent.CompletableFuture +import javax.swing.JTree +import javax.swing.JWindow +import javax.swing.tree.TreePath + +abstract class OnboardingTourLessonBase(id: String) : KLesson(id, JavaLessonsBundle.message("java.onboarding.lesson.name")) { + private lateinit var openLearnTaskId: TaskContext.TaskId + private var useDelay: Boolean = false + + private val demoFileDirectory: String = "src" + private val demoFileNameWithoutExtension: String = "Welcome" + abstract val demoFileExtension: String + private val demoFileName: String + get() = "$demoFileNameWithoutExtension.$demoFileExtension" + + private val uiSettings get() = UISettings.getInstance() + + override val properties: LessonProperties = LessonProperties( + canStartInDumbMode = true, + openFileAtStart = false + ) + + override val testScriptProperties: TaskTestContext.TestScriptProperties = TaskTestContext.TestScriptProperties(skipTesting = true) + + private var backupPopupLocation: Point? = null + private var hideToolStripesPreference = false + private var showNavigationBarPreference = true + + @NlsSafe + private var jdkAtStart: String = "undefined" + + abstract val sample: LessonSample + + abstract val learningProjectName: String + + override val lessonContent: LessonContext.() -> Unit = { + prepareRuntimeTask { + jdkAtStart = getCurrentJdkVersionString(project) + useDelay = true + invokeActionForFocusContext(getActionById("Stop")) + val runManager = RunManager.getInstance(project) + runManager.allSettings.forEach(runManager::removeConfiguration) + + val root = ProjectUtils.getCurrentLearningProjectRoot() + val srcDir = root.findChild(demoFileDirectory) ?: error("'src' directory not found.") + if (srcDir.findChild(demoFileName) == null) invokeLater { + runWriteAction { + srcDir.createChildData(this, demoFileName) + // todo: This file shows with .java extension in the Project view and this extension disappears when user open it + // (because we fill the file after the user open it) Fill the file immediately in this place? + } + } + } + clearBreakpoints() + + checkUiSettings() + + projectTasks() + + prepareSample(sample, checkSdkConfiguration = false) + + openLearnToolwindow() + + sdkConfigurationTasks() + + waitIndexingTasks() + + runTasks() + + debugTasks() + + completionSteps() + + waitBeforeContinue(500) + + contextActions() + + waitBeforeContinue(500) + + searchEverywhereTasks() + + task { + text(JavaLessonsBundle.message("java.onboarding.epilog", + getCallBackActionId("CloseProject"), + LessonUtil.returnToWelcomeScreenRemark(), + LearningUiManager.addCallback { LearningUiManager.resetModulesView() })) + } + } + + override fun onLessonEnd(project: Project, lessonEndInfo: LessonEndInfo) { + prepareFeedbackData(project, lessonEndInfo) + restorePopupPosition(project, SearchEverywhereManagerImpl.LOCATION_SETTINGS_KEY, backupPopupLocation) + backupPopupLocation = null + + uiSettings.hideToolStripes = hideToolStripesPreference + uiSettings.showNavigationBar = showNavigationBarPreference + uiSettings.fireUISettingsChanged() + + if (!lessonEndInfo.lessonPassed) { + LessonUtil.showFeedbackNotification(this, project) + return + } + val dataContextPromise = DataManager.getInstance().dataContextFromFocusAsync + invokeLater { + val result = MessageDialogBuilder.yesNoCancel(JavaLessonsBundle.message("java.onboarding.finish.title"), + JavaLessonsBundle.message("java.onboarding.finish.text", + LessonUtil.returnToWelcomeScreenRemark())) + .yesText(JavaLessonsBundle.message("java.onboarding.finish.exit")) + .noText(JavaLessonsBundle.message("java.onboarding.finish.modules")) + .icon(FeaturesTrainerIcons.PluginIcon) + .show(project) + + when (result) { + Messages.YES -> invokeLater { + LessonManager.instance.stopLesson() + val closeAction = getActionById("CloseProject") + dataContextPromise.onSuccess { context -> + invokeLater { + val event = AnActionEvent.createFromAnAction(closeAction, null, ActionPlaces.LEARN_TOOLWINDOW, context) + ActionUtil.performActionDumbAwareWithCallbacks(closeAction, event) + } + } + } + Messages.NO -> invokeLater { + LearningUiManager.resetModulesView() + } + } + if (result != Messages.YES) { + LessonUtil.showFeedbackNotification(this, project) + } + } + } + + private fun getCurrentJdkVersionString(project: Project): String { + return JavaProjectUtil.getEffectiveJdk(project)?.let { JavaSdk.getInstance().getVersionString(it) } ?: "none" + } + + private fun prepareFeedbackData(project: Project, lessonEndInfo: LessonEndInfo) { + val primaryLanguage = module.primaryLanguage + if (primaryLanguage == null) { + thisLogger().error("Onboarding lesson has no language support for some magical reason") + return + } + val configPropertyName = getFeedbackProposedPropertyName(primaryLanguage) + if (PropertiesComponent.getInstance().getBoolean(configPropertyName, false)) { + return + } + + val jdkVersionsFuture = CompletableFuture>() + runBackgroundableTask(ProjectBundle.message("progress.title.detecting.sdks"), project, false) { indicator -> + val jdkVersions = mutableListOf() + SdkDetector.getInstance().detectSdks(JavaSdk.getInstance(), indicator, object : SdkDetector.DetectedSdkListener { + override fun onSdkDetected(type: SdkType, version: String, home: String) { + jdkVersions.add(version) + } + + override fun onSearchCompleted() { + jdkVersionsFuture.complete(jdkVersions) + } + }) + } + + @Suppress("HardCodedStringLiteral") + val currentJdkVersion: @NlsSafe String = getCurrentJdkVersionString(project) + + val module = ModuleManager.getInstance(project).modules.first() + + @Suppress("HardCodedStringLiteral") + val currentLanguageLevel: @NlsSafe String = LanguageLevelUtil.getEffectiveLanguageLevel(module).name + + primaryLanguage.onboardingFeedbackData = object : OnboardingFeedbackData("IDEA Onboarding Tour Feedback", lessonEndInfo) { + override val feedbackReportId = "idea_onboarding_tour" + + override val additionalFeedbackFormatVersion: Int = 1 + + private val jdkVersions: List? by lazy { + if (jdkVersionsFuture.isDone) jdkVersionsFuture.get() else null + } + + override val addAdditionalSystemData: JsonObjectBuilder.() -> Unit = { + put("jdk_at_start", jdkAtStart) + put("current_jdk", currentJdkVersion) + put("language_level", currentLanguageLevel) + put("found_jdk", buildJsonArray { + for (version in jdkVersions ?: emptyList()) { + add(JsonPrimitive(version)) + } + }) + } + + override val addRowsForUserAgreement: Panel.() -> Unit = { + row(JavaLessonsBundle.message("java.onboarding.feedback.system.found.jdks")) { + val versions: @NlsSafe String = jdkVersions?.joinToString("\n") ?: "none" + cell(MultiLineLabel(versions)) + } + row(JavaLessonsBundle.message("java.onboarding.feedback.system.jdk.at.start")) { + label(jdkAtStart) + } + row(JavaLessonsBundle.message("java.onboarding.feedback.system.current.jdk")) { + label(currentJdkVersion) + } + row(JavaLessonsBundle.message("java.onboarding.feedback.system.lang.level")) { + label(currentLanguageLevel) + } + } + + override fun feedbackHasBeenProposed() { + PropertiesComponent.getInstance().setValue(configPropertyName, true, false) + } + } + } + + private fun LessonContext.debugTasks() { + clearBreakpoints() + + var logicalPosition = LogicalPosition(0, 0) + prepareRuntimeTask { + logicalPosition = editor.offsetToLogicalPosition(sample.startOffset) + } + caret(sample.startOffset) + + toggleBreakpointTask(sample, { logicalPosition }, checkLine = false) { + text(JavaLessonsBundle.message("java.onboarding.balloon.click.here"), + LearningBalloonConfig(Balloon.Position.below, width = 0, cornerToPointerDistance = 20)) + text(JavaLessonsBundle.message("java.onboarding.toggle.breakpoint.1", + code("6.5"), code("findAverage"), code("26"))) + text(JavaLessonsBundle.message("java.onboarding.toggle.breakpoint.2")) + } + + highlightButtonById("Debug", highlightInside = false, usePulsation = false) + + actionTask("Debug") { + showBalloonOnHighlightingComponent(JavaLessonsBundle.message("java.onboarding.balloon.start.debugging")) + restoreState { + lineWithBreakpoints() != setOf(logicalPosition.line) + } + restoreIfModified(sample) + JavaLessonsBundle.message("java.onboarding.start.debugging", icon(AllIcons.Actions.StartDebugger)) + } + + lateinit var debuggerGotItTaskId: TaskContext.TaskId + task { + debuggerGotItTaskId = taskId + } + + highlightDebugActionsToolbar() + + task { + rehighlightPreviousUi = true + gotItStep(Balloon.Position.above, width = 0, + JavaLessonsBundle.message("java.onboarding.balloon.about.debug.panel", + strong(UIBundle.message("tool.window.name.debug")), + strong(LessonsBundle.message("debug.workflow.lesson.name")))) + restoreByUi(debuggerGotItTaskId) + } + + highlightButtonById("Stop", highlightInside = false, usePulsation = false) + task { + val position = if (UIExperiment.isNewDebuggerUIEnabled()) Balloon.Position.above else Balloon.Position.atRight + showBalloonOnHighlightingComponent(JavaLessonsBundle.message("java.onboarding.balloon.stop.debugging"), + position) { list -> list.maxByOrNull { it.locationOnScreen.y } } + text(JavaLessonsBundle.message("java.onboarding.stop.debugging", icon(AllIcons.Actions.Suspend))) + restoreIfModified(sample) + stateCheck { + XDebuggerManager.getInstance(project).currentSession == null + } + } + + prepareRuntimeTask { + LearningUiHighlightingManager.clearHighlights() + } + } + + private fun LessonContext.waitIndexingTasks() { + task { + triggerAndBorderHighlight().component { progress: NonOpaquePanel -> + progress.javaClass.name.contains("InlineProgressPanel") + } + } + + task { + text(JavaLessonsBundle.message("java.onboarding.indexing.description")) + text(JavaLessonsBundle.message("java.onboarding.wait.indexing"), LearningBalloonConfig(Balloon.Position.above, 0)) + waitSmartModeStep() + } + + waitBeforeContinue(300) + + prepareRuntimeTask { + LearningUiHighlightingManager.clearHighlights() + } + } + + private fun LessonContext.runTasks() { + highlightRunToolbar(highlightInside = false, usePulsation = false) + + task { + triggerUI { + clearPreviousHighlights = false + }.component { ui: ActionButton -> ActionManager.getInstance().getId(ui.action) == "Run" } + } + + task { + val introductionText = JavaLessonsBundle.message("java.onboarding.temporary.configuration.description", + strong(ActionsBundle.actionText("NewUiRunWidget")), + icon(AllIcons.Actions.Execute), + icon(AllIcons.Actions.StartDebugger)) + val runOptionsText = if (PlatformUtils.isIdeaUltimate()) { + JavaLessonsBundle.message("java.onboarding.run.options.ultimate", + icon(AllIcons.Actions.Profile), + icon(AllIcons.General.RunWithCoverage), + icon(AllIcons.Actions.More)) + } + else { + JavaLessonsBundle.message("java.onboarding.run.options.community", + icon(AllIcons.General.RunWithCoverage), + icon(AllIcons.Actions.More)) + } + + text("$introductionText $runOptionsText") + text(JavaLessonsBundle.message("java.onboarding.run.sample", icon(AllIcons.Actions.Execute), action("Run"))) + text(JavaLessonsBundle.message("java.onboarding.run.sample.balloon", icon(AllIcons.Actions.Execute), action("Run")), + LearningBalloonConfig(Balloon.Position.below, 0)) + checkToolWindowState("Run", true) + restoreIfModified(sample) + } + } + + private fun LessonContext.openLearnToolwindow() { + task { + triggerAndBorderHighlight().component { stripe: ActionButton -> + stripe.action.templateText == LearnBundle.message("toolwindow.stripe.Learn") + } + } + + task { + openLearnTaskId = taskId + text(JavaLessonsBundle.message("java.onboarding.balloon.open.learn.toolbar", strong(LearnBundle.message("toolwindow.stripe.Learn"))), + LearningBalloonConfig(Balloon.Position.atRight, width = 0, duplicateMessage = true)) + stateCheck { + ToolWindowManager.getInstance(project).getToolWindow("Learn")?.isVisible == true + } + restoreIfModified(sample) + } + + prepareRuntimeTask { + LearningUiHighlightingManager.clearHighlights() + requestEditorFocus() + } + } + + + private fun LessonContext.checkUiSettings() { + hideToolStripesPreference = uiSettings.hideToolStripes + showNavigationBarPreference = uiSettings.showNavigationBar + + showInvalidDebugLayoutWarning() + + if (!hideToolStripesPreference && (showNavigationBarPreference || uiSettings.showMainToolbar)) { + // a small hack to have same tasks count. It is needed to track statistics result. + task { } + task { } + return + } + + task { + text(JavaLessonsBundle.message("java.onboarding.change.ui.settings")) + proceedLink() + } + + prepareRuntimeTask { + uiSettings.hideToolStripes = false + uiSettings.showNavigationBar = true + uiSettings.fireUISettingsChanged() + } + } + + + private fun LessonContext.projectTasks() { + prepareRuntimeTask { + LessonUtil.hideStandardToolwindows(project) + } + + task { + triggerAndBorderHighlight().component { stripe: ActionButton -> + stripe.action.templateText == IdeUICustomization.getInstance().getProjectViewTitle(project) + } + } + + lateinit var openProjectViewTask: TaskContext.TaskId + task { + openProjectViewTask = taskId + var projectDirExpanded = false + + text(JavaLessonsBundle.message("java.onboarding.project.view.description", + action("ActivateProjectToolWindow"))) + text(JavaLessonsBundle.message("java.onboarding.balloon.project.view"), + LearningBalloonConfig(Balloon.Position.atRight, width = 0, cornerToPointerDistance = 8)) + triggerUI().treeItem { tree: JTree, path: TreePath -> + val result = path.pathCount >= 2 && path.getPathComponent(1).isToStringContains(learningProjectName) + if (result) { + if (!projectDirExpanded) { + invokeLater { tree.expandPath(path) } + } + projectDirExpanded = true + } + result + } + } + + task { + var srcDirCollapsed = false + triggerAndBorderHighlight().treeItem { tree: JTree, path: TreePath -> + val result = path.pathCount >= 3 + && path.getPathComponent(1).isToStringContains(learningProjectName) + && path.getPathComponent(2).isToStringContains(demoFileDirectory) + if (result) { + if (!srcDirCollapsed) { + invokeLater { tree.collapsePath(path) } + } + srcDirCollapsed = true + } + result + } + } + + fun isDemoFilePath(path: TreePath) = + path.pathCount >= 4 && path.getPathComponent(3).isToStringContains(demoFileNameWithoutExtension) + + task { + text(JavaLessonsBundle.message("java.onboarding.balloon.source.directory", strong(demoFileDirectory)), + LearningBalloonConfig(Balloon.Position.atRight, duplicateMessage = true, width = 0)) + triggerAndBorderHighlight().treeItem { _: JTree, path: TreePath -> + isDemoFilePath(path) + } + restoreByUi(openProjectViewTask) + } + + task { + text(JavaLessonsBundle.message("java.onboarding.balloon.open.file", strong(demoFileName)), + LearningBalloonConfig(Balloon.Position.atRight, duplicateMessage = true, width = 0)) + stateCheck l@{ + if (FileEditorManager.getInstance(project).selectedTextEditor == null) return@l false + virtualFile.name == demoFileName + } + restoreState { + (previous.ui as? JTree)?.takeIf { tree -> + TreeUtil.visitVisibleRows(tree, TreeVisitor { path -> + if (isDemoFilePath(path)) TreeVisitor.Action.INTERRUPT else TreeVisitor.Action.CONTINUE + }) != null + }?.isShowing?.not() ?: true + } + } + } + + abstract val completionStepExpectedCompletion: String + + private fun LessonContext.completionSteps() { + prepareRuntimeTask { + setSample(sample.insertAtPosition(2, " / values")) + FocusManagerImpl.getInstance(project).requestFocusInProject(editor.contentComponent, project) + } + + task { + val textToFind = "result / values" + triggerOnEditorText(textToFind, centerOffset = textToFind.length) + } + + task { + text(JavaLessonsBundle.message("java.onboarding.type.division", + code(" / values"))) + text(JavaLessonsBundle.message("java.onboarding.invoke.completion", code("."))) + text(JavaLessonsBundle.message("java.onboarding.invoke.completion.balloon", code(".")), + LearningBalloonConfig(Balloon.Position.below, width = 0)) + triggerAndBorderHighlight().listItem { // no highlighting + it.isToStringContains(completionStepExpectedCompletion) + } + proposeRestoreForInvalidText(".") + } + + task { + text(JavaLessonsBundle.message("java.onboarding.choose.values.item", + code(completionStepExpectedCompletion), action("EditorChooseLookupItem"))) + text(JavaLessonsBundle.message("java.onboarding.invoke.completion.tip", action("CodeCompletion"))) + stateCheck { + checkEditorModification(sample, modificationPositionId = 2, needChange = "/values.$completionStepExpectedCompletion") + } + } + } + + abstract fun LessonContext.contextActions() + + private fun LessonContext.searchEverywhereTasks() { + val toggleCase = ActionsBundle.message("action.EditorToggleCase.text") + caret("AVERAGE", select = true) + task("SearchEverywhere") { + text(JavaLessonsBundle.message("java.onboarding.invoke.search.everywhere.1", + strong(toggleCase), code("AVERAGE"))) + text(JavaLessonsBundle.message("java.onboarding.invoke.search.everywhere.2", + LessonUtil.rawKeyStroke(KeyEvent.VK_SHIFT), LessonUtil.actionName(it))) + triggerAndBorderHighlight().component { ui: ExtendableTextField -> + UIUtil.getParentOfType(SearchEverywhereUI::class.java, ui) != null + } + restoreIfModifiedOrMoved() + } + + task { + transparentRestore = true + before { + if (backupPopupLocation != null) return@before + val ui = previous.ui ?: return@before + val popupWindow = UIUtil.getParentOfType(JWindow::class.java, ui) ?: return@before + val oldPopupLocation = WindowStateService.getInstance(project).getLocation(SearchEverywhereManagerImpl.LOCATION_SETTINGS_KEY) + if (adjustSearchEverywherePosition(popupWindow, "of array ") || LessonUtil.adjustPopupPosition(project, popupWindow)) { + backupPopupLocation = oldPopupLocation + } + } + text(JavaLessonsBundle.message("java.onboarding.search.everywhere.description", + code("AVERAGE"), code(JavaLessonsBundle.message("toggle.case.part")))) + triggerAndBorderHighlight().listItem { item -> + val value = (item as? GotoActionModel.MatchedValue)?.value + (value as? GotoActionModel.ActionWrapper)?.action is ToggleCaseAction + } + restoreByUi() + restoreIfModifiedOrMoved() + } + + task { + text(JavaLessonsBundle.message("java.onboarding.apply.action", strong(toggleCase), LessonUtil.rawEnter())) + stateCheck { + editor.document.text.contains("\"average") + } + restoreByUi(delayMillis = defaultRestoreDelay) + } + + text(JavaLessonsBundle.message("java.onboarding.case.changed")) + } +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/kotlin.features-trainer.iml b/plugins/kotlin/features-trainer/kotlin.features-trainer.iml index 9cfdb73c7ea3..a99f3e5a3692 100644 --- a/plugins/kotlin/features-trainer/kotlin.features-trainer.iml +++ b/plugins/kotlin/features-trainer/kotlin.features-trainer.iml @@ -25,5 +25,11 @@ + + + + + + \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/CodeFormat.kt.sample b/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/CodeFormat.kt.sample new file mode 100644 index 000000000000..3ea5b9ed2f3e --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/CodeFormat.kt.sample @@ -0,0 +1,14 @@ + +const val A_CONST = 10000 +const val B_CONST = 500 + + + +class Processor{ + companion + object { + fun process(a: Int, b: Int): Int { return a * b + A_CONST + } + } +} + diff --git a/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/ParameterInfo.kt.sample b/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/ParameterInfo.kt.sample new file mode 100644 index 000000000000..37944209711d --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/ParameterInfo.kt.sample @@ -0,0 +1,9 @@ +import javax.swing.* + +fun main() { + val frame = JFrame("FrameDemo") + frame.setSize() + + frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) + frame.setVisible(true) +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/QuickPopups.kt.sample b/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/QuickPopups.kt.sample new file mode 100644 index 000000000000..e7a52646957d --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/data/modules/kotlin/CodeAssistance/QuickPopups.kt.sample @@ -0,0 +1,17 @@ +import java.text.DecimalFormat + +fun main() { + customFormat("###,###.###", 123456.789) + customFormat("###.##", 123456.789) + customFormat("000000.000", 123.78) + customFormat("$###,###.###", 12345.67) +} + +/** + * Prints a double value formatted according to a given pattern. + */ +fun customFormat(pattern: String, value: Double) { + val myFormatter = DecimalFormat(pattern); + val output = myFormatter.format(value); + System.out.println("$value $pattern $output"); +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/BufferedReader.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/BufferedReader.kt index 0d40e3edd146..d9dfbb11a3fe 100644 --- a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/BufferedReader.kt +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/BufferedReader.kt @@ -4,18 +4,10 @@ import java.io.InputStream * This is a sample class used in IDE Feature Trainer * for illustration properties */ -class BufferedReader { - private var inputStream: InputStream - private var maxBufferSize = 1024 - - constructor(inputStream: InputStream) { - this.inputStream = inputStream - } - - constructor(inputStream: InputStream, maxBufferSize: Int) { - this.inputStream = inputStream - this.maxBufferSize = maxBufferSize - } +class BufferedReader( + private var inputStream: InputStream, + private var maxBufferSize: Int = 1024 +) { /** * @return one byte from input stream diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/DerivedClass1.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/DerivedClass1.kt new file mode 100644 index 000000000000..cd4b56527526 --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/DerivedClass1.kt @@ -0,0 +1,13 @@ +open class DerivedClass1 : SomeInterface { + override fun foo(demo: FileStructureDemo) { + demo.boo() + } +} + +class SecondLevelClassA : DerivedClass1() { + override fun foo(demo: FileStructureDemo) { + demo.foo() + } +} + +class SecondLevelClassB : DerivedClass1() \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/DerivedClass2.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/DerivedClass2.kt new file mode 100644 index 000000000000..3a9f732cc00e --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/DerivedClass2.kt @@ -0,0 +1,5 @@ +class DerivedClass2 : SomeInterface { + override fun foo(demo: FileStructureDemo) { + demo.foo() + } +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/InheritanceHierarchySample.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/InheritanceHierarchySample.kt new file mode 100644 index 000000000000..1bd7403f2d35 --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/InheritanceHierarchySample.kt @@ -0,0 +1,7 @@ +class InheritanceHierarchySample { + +} + +fun someEntryPoint(base: SomeInterface, demo: FileStructureDemo) { + base.foo(demo) +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/QuadraticEquationsSolver.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/QuadraticEquationsSolver.kt index 3024c2015b70..afd2561fc171 100644 --- a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/QuadraticEquationsSolver.kt +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/QuadraticEquationsSolver.kt @@ -13,6 +13,7 @@ class QuadraticEquationsSolver { val x2 = (-b - sqrt(d)) / (2.0 * a) println("x1 = $x1, x2 = $x2") } + else -> println("x = ${-b / (2.0 * a)}") } } diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/RecentFilesDemo.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/RecentFilesDemo.kt index d1e25c23b942..a7a5e00036dc 100644 --- a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/RecentFilesDemo.kt +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/RecentFilesDemo.kt @@ -1,4 +1,3 @@ - fun main() { println("Hello World!") } \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/SomeInterface.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/SomeInterface.kt new file mode 100644 index 000000000000..f8bc7534c285 --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/SomeInterface.kt @@ -0,0 +1,5 @@ +interface SomeInterface { + fun foo(demo: FileStructureDemo) +} + +interface SomeDerivedInterface : SomeInterface \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/util/Utils.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/util/Utils.kt new file mode 100644 index 000000000000..239dea503002 --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/util/Utils.kt @@ -0,0 +1,5 @@ +package util + +object Utils { + val FRUITS = arrayOf("pineapple", "banana", "apple", "grapes", "mango", "melon", "peach", "orange") +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/warehouse/FindInFilesSample.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/warehouse/FindInFilesSample.kt new file mode 100644 index 000000000000..50888d0a6330 --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/warehouse/FindInFilesSample.kt @@ -0,0 +1,14 @@ +package warehouse + +fun main() { + val warehouse = Warehouse() + warehouse.addFruits("peach", 3) + warehouse.addFruits("pineapple", 5) + warehouse.addFruits("mango", 1) + warehouse.addFruits("apple", 5) + val result = warehouse.takeFruit("apple") + if (result) { + println("This apple was delicious!") + } + warehouse.printAllFruits() +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/warehouse/Warehouse.kt b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/warehouse/Warehouse.kt new file mode 100644 index 000000000000..f29ff0018c13 --- /dev/null +++ b/plugins/kotlin/features-trainer/resources/learnProjects/kotlin/KotlinLearningProject/src/warehouse/Warehouse.kt @@ -0,0 +1,43 @@ +package warehouse + +import util.Utils + +class Warehouse { + // Fruit name to amount of it in warehouse + private val entry: MutableMap = HashMap() // Apple, banana, etc... + + init { + val availableFruits: Array = Utils.FRUITS + for (fruit in availableFruits) { + entry[fruit] = 0 + } + } + + /** + * @param fruitName some fruit name from [Utils.FRUITS] (mango, apple...) + */ + fun addFruits(fruitName: String, quantity: Int) { + val curQuantity = entry[fruitName] + if (curQuantity != null) { + entry[fruitName] = curQuantity + quantity + } else { + throw IllegalArgumentException("Not found fruit with name: $fruitName") + } + } + + fun takeFruit(fruitName: String): Boolean { + val curQuantity = entry[fruitName] + requireNotNull(curQuantity) { "Not found fruit with name: $fruitName" } + if (curQuantity > 0) { + entry[fruitName] = curQuantity - 1 + return true + } + return false + } + + fun printAllFruits() { + for ((key, value) in entry) { + println("$key: $value") + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/resources/messages/KotlinLessonsBundle.properties b/plugins/kotlin/features-trainer/resources/messages/KotlinLessonsBundle.properties index 40292e5dc9ae..2b7a3d3d993e 100644 --- a/plugins/kotlin/features-trainer/resources/messages/KotlinLessonsBundle.properties +++ b/plugins/kotlin/features-trainer/resources/messages/KotlinLessonsBundle.properties @@ -1,3 +1,19 @@ configure.kotlin.progress.title=Configure Kotlin -kotlin.basic.completion.choose.item=Choose the highlighted {0} item to complete the function call and place the caret inside the parenthesis. \ No newline at end of file +kotlin.onboarding.invoke.intention.for.warning.1=You have just fixed a bug, but you can make this code look even better. \ + IDEA highlights and adds a yellow bulb to the code lines that can be improved. +kotlin.onboarding.invoke.intention.for.warning.2=Press {0} to preview the warnings and apply a quick-fix. +kotlin.onboarding.invoke.intention.for.warning.balloon=Press {0} to show available quick-fixes +kotlin.onboarding.select.fix=Apply the first item: {0}. In this case, the for-each loop will make code easier to understand. +kotlin.onboarding.invoke.intention.for.code=Intentions also save your time and make coding easier. Let''s use an intention to reformat string concatenation. \ + Press {0} to show possible options. +kotlin.onboarding.invoke.intention.for.code.balloon=Press {0} to show available intentions +kotlin.onboarding.apply.intention=Select {0} and press {1}. + +kotlin.basic.completion.choose.item=Choose the highlighted {0} item to complete the function call and place the caret inside the parenthesis. + +# Next string does not require translation +kotlin.refactoring.menu.inline.property.eng=Now let''s replace single usage of the {0} variable with an expression that defines it. \ + You can press {1} and filter the refactoring menu by ipr (inline property). \ + Choose this item or press {2}. +kotlin.refactoring.menu.confirm.constant=Choose and type any new name you want to use for the constant. Then press {0} to confirm. \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLangSupport.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLangSupport.kt index ef3a24c8cb5c..3e259fd3f0ab 100644 --- a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLangSupport.kt +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLangSupport.kt @@ -18,6 +18,7 @@ import java.nio.file.Path class KotlinLangSupport : JavaBasedLangSupport() { override val contentRootDirectoryName = "KotlinLearningProject" + override val defaultProductName: String = "IDEA" override val primaryLanguage: String = "kotlin" override val scratchFileName: String = "Learning.kt" diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLearningCourse.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLearningCourse.kt index ff626ed86a46..7d494a70b76c 100644 --- a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLearningCourse.kt +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/KotlinLearningCourse.kt @@ -1,22 +1,55 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package org.jetbrains.kotlin.training.ift +import com.intellij.java.ift.JavaLessonsBundle +import com.intellij.util.PlatformUtils import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.training.ift.lesson.basic.KotlinBasicCompletionLesson import org.jetbrains.kotlin.training.ift.lesson.basic.KotlinContextActionsLesson import org.jetbrains.kotlin.training.ift.lesson.basic.KotlinSelectLesson import org.jetbrains.kotlin.training.ift.lesson.basic.KotlinSurroundAndUnwrapLesson +import org.jetbrains.kotlin.training.ift.lesson.completion.KotlinCompletionWithTabLesson +import org.jetbrains.kotlin.training.ift.lesson.completion.KotlinPostfixCompletionLesson +import org.jetbrains.kotlin.training.ift.lesson.completion.KotlinSmartTypeCompletionLesson +import org.jetbrains.kotlin.training.ift.lesson.essential.KotlinOnboardingTourLesson +import org.jetbrains.kotlin.training.ift.lesson.navigation.KotlinDeclarationAndUsagesLesson import org.jetbrains.kotlin.training.ift.lesson.navigation.KotlinFileStructureLesson +import org.jetbrains.kotlin.training.ift.lesson.navigation.KotlinRecentFilesLesson import org.jetbrains.kotlin.training.ift.lesson.navigation.KotlinSearchEverywhereLesson +import org.jetbrains.kotlin.training.ift.lesson.refactorings.KotlinRefactoringMenuLesson import training.dsl.LessonUtil +import training.learn.CourseManager import training.learn.LessonsBundle import training.learn.course.LearningCourseBase import training.learn.course.LearningModule import training.learn.course.LessonType import training.learn.lesson.general.* +import training.learn.lesson.general.assistance.CodeFormatLesson +import training.learn.lesson.general.assistance.LocalHistoryLesson +import training.learn.lesson.general.assistance.ParameterInfoLesson +import training.learn.lesson.general.assistance.QuickPopupsLesson +import training.learn.lesson.general.navigation.FindInFilesLesson class KotlinLearningCourse : LearningCourseBase(KotlinLanguage.INSTANCE.id) { - override fun modules() = stableModules() + override fun modules() = onboardingTour() + stableModules() + CourseManager.instance.findCommonModules("Git") + + private val isOnboardingLessonEnabled: Boolean + get() = PlatformUtils.isIdeaCommunity() || PlatformUtils.isIdeaUltimate() + + private fun onboardingTour() = if (isOnboardingLessonEnabled) listOf( + LearningModule( + id = "Kotlin.Onboarding", + name = JavaLessonsBundle.message("java.onboarding.module.name"), + description = JavaLessonsBundle.message("java.onboarding.module.description", LessonUtil.productName), + primaryLanguage = langSupport, + moduleType = LessonType.PROJECT + ) { + listOf( + KotlinOnboardingTourLesson() + ) + } + ) + else emptyList() private fun stableModules() = listOf( LearningModule( @@ -59,7 +92,44 @@ class KotlinLearningCourse : LearningCourseBase(KotlinLanguage.INSTANCE.id) { primaryLanguage = langSupport, moduleType = LessonType.SINGLE_EDITOR // todo: change to SCRATCH when KTIJ-20742 will be resolved ) { - listOf(KotlinBasicCompletionLesson()) + listOf( + KotlinBasicCompletionLesson(), + KotlinSmartTypeCompletionLesson(), + KotlinPostfixCompletionLesson(), + // TODO: KotlinStatementCompletionLesson + KotlinCompletionWithTabLesson() + ) + }, + LearningModule( + id = "Kotlin.Refactorings", + name = LessonsBundle.message("refactorings.module.name"), + description = LessonsBundle.message("refactorings.module.description"), + primaryLanguage = langSupport, + moduleType = LessonType.SINGLE_EDITOR + ) { + fun ls(sampleName: String) = loadSample("Refactorings/$sampleName") + listOf( + // TODO: KotlinRenameLesson(), + // TODO: ExtractVariableFromBubbleLesson(ls("ExtractVariable.java.sample")), + // TODO: KotlinExtractMethodCocktailSortLesson(), + KotlinRefactoringMenuLesson(), + ) + }, + LearningModule( + id = "Kotlin.CodeAssistance", + name = LessonsBundle.message("code.assistance.module.name"), + description = LessonsBundle.message("code.assistance.module.description"), + primaryLanguage = langSupport, + moduleType = LessonType.SINGLE_EDITOR + ) { + fun ls(sampleName: String) = loadSample("CodeAssistance/$sampleName") + listOf( + LocalHistoryLesson(), + CodeFormatLesson(ls("CodeFormat.kt.sample"), true), + ParameterInfoLesson(ls("ParameterInfo.kt.sample")), + QuickPopupsLesson(ls("QuickPopups.kt.sample")), + // TODO: KotlinEditorCodingAssistanceLesson(ls("EditorCodingAssistance.java.sample")), + ) }, LearningModule( id = "Kotlin.Navigation", @@ -70,12 +140,27 @@ class KotlinLearningCourse : LearningCourseBase(KotlinLanguage.INSTANCE.id) { ) { listOf( KotlinSearchEverywhereLesson(), + FindInFilesLesson("src/warehouse/FindInFilesSample.kt"), KotlinFileStructureLesson(), + KotlinDeclarationAndUsagesLesson(), + // TODO: KotlinInheritanceHierarchyLesson + KotlinRecentFilesLesson() + // TODO: KotlinOccurrencesLesson ) - } + }, + /*LearningModule(id = "Kotlin.RunAndDebug", + name = LessonsBundle.message("run.debug.module.name"), + description = LessonsBundle.message("run.debug.module.description"), + primaryLanguage = langSupport, + moduleType = LessonType.SINGLE_EDITOR) { + listOf( + // TODO: KotlinRunConfigurationLesson(), + // TODO: KotlinDebugLesson(), + ) + },*/ ) - override fun getLessonIdToTipsMap(): Map> = mapOf( + override fun getLessonIdToTipsMap(): Map> = mutableMapOf( // Essential "context.actions" to listOf("ContextActions"), "Actions" to listOf("find_action", "GoToAction"), @@ -94,6 +179,12 @@ class KotlinLearningCourse : LearningCourseBase(KotlinLanguage.INSTANCE.id) { // Navigation "Search everywhere" to listOf("SearchEverywhere", "GoToClass", "search_everywhere_general"), + "Find in files" to listOf("FindReplaceToggle", "FindInPath"), "File structure" to listOf("FileStructurePopup"), - ) + ).also { map -> + val gitCourse = CourseManager.instance.findCommonCourseById("Git") + if (gitCourse != null) { + map.putAll(gitCourse.getLessonIdToTipsMap()) + } + } } \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinCompletionWithTabLesson.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinCompletionWithTabLesson.kt new file mode 100644 index 000000000000..d0080148da7d --- /dev/null +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinCompletionWithTabLesson.kt @@ -0,0 +1,19 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.training.ift.lesson.completion + +import training.dsl.LessonSample +import training.dsl.parseLessonSample +import training.learn.lesson.general.CompletionWithTabLesson + +class KotlinCompletionWithTabLesson : + CompletionWithTabLesson("DO_NOTHING_ON_CLOSE") { + override val sample: LessonSample = parseLessonSample("""import javax.swing.* + +fun main() { + val frame = JFrame("FrameDemo") + frame.setSize(175, 100) + + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE) + frame.isVisible = true +}""".trimIndent()) +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinPostfixCompletionLesson.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinPostfixCompletionLesson.kt new file mode 100644 index 000000000000..a57c86e4cacd --- /dev/null +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinPostfixCompletionLesson.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.training.ift.lesson.completion + +import com.intellij.java.ift.JavaLessonsBundle +import training.dsl.LearningDslBase +import training.dsl.LessonSample +import training.dsl.parseLessonSample +import training.learn.lesson.general.completion.PostfixCompletionLesson + +class KotlinPostfixCompletionLesson : PostfixCompletionLesson() { + override val sample: LessonSample = parseLessonSample( + """ + fun demonstrate(showTimes: Int) { + showTimes + } + """.trimIndent() + ) + + override val result: String = parseLessonSample( + """ + fun demonstrate(showTimes: Int) { + for (i in 0 until showTimes) { + + } + } + """.trimIndent() + ).text + + + override val completionSuffix: String = ".fo" + override val completionItem: String = "fori" + + override fun LearningDslBase.getTypeTaskText(): String { + return JavaLessonsBundle.message("java.postfix.completion.type", code(completionSuffix)) + } + + override fun LearningDslBase.getCompleteTaskText(): String { + return JavaLessonsBundle.message("java.postfix.completion.complete", code(completionItem), action("EditorChooseLookupItem")) + } +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinSmartTypeCompletionLesson.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinSmartTypeCompletionLesson.kt new file mode 100644 index 000000000000..36f2977bd020 --- /dev/null +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/completion/KotlinSmartTypeCompletionLesson.kt @@ -0,0 +1,33 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.training.ift.lesson.completion + +import com.intellij.java.ift.lesson.completion.SmartTypeCompletionLessonBase +import training.dsl.LessonContext +import training.dsl.LessonSample +import training.dsl.parseLessonSample + +class KotlinSmartTypeCompletionLesson : SmartTypeCompletionLessonBase() { + + override val sample: LessonSample = parseLessonSample(""" + object TestObject + + fun smartCompletionDemo(): TestObject { + val iterations = 5 + repeat() { + println("Hello") + } + + return + } +""".trimIndent()) + + override val firstCompletionItem: String = "iterations" + override val firstCompletionCheck: String = "repeat(iterations)" + + override val secondCompletionItem: String = "TestObject" + override val secondCompletionCheck: String = "return TestObject" + + override fun LessonContext.setCaretForSecondItem() { + caret(9, 12) + } +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/essential/KotlinOnboardingTourLesson.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/essential/KotlinOnboardingTourLesson.kt new file mode 100644 index 000000000000..4e229315eaab --- /dev/null +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/essential/KotlinOnboardingTourLesson.kt @@ -0,0 +1,92 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.training.ift.lesson.essential + + +import com.intellij.java.ift.lesson.essential.OnboardingTourLessonBase +import com.intellij.openapi.ui.popup.Balloon +import org.jetbrains.annotations.Nls +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.training.ift.KotlinLessonsBundle +import training.dsl.* +import training.dsl.LessonUtil.restoreIfModifiedOrMoved +import training.util.* + +class KotlinOnboardingTourLesson : OnboardingTourLessonBase("kotlin.onboarding") { + override val demoFileExtension: String = "kt" + override val learningProjectName: String = "KotlinLearningProject" + private val samplePrintln = "println(\"AVERAGE of array \" + array.joinToString() + \" is \" + findAverage(array))" + override val sample: LessonSample = parseLessonSample(""" + fun findAverage(values: IntArray): Double { + var result = 0.0 + for (i in 0 until values.size) { + result += values[i] + } + return result + } + + fun main() { + val array = intArrayOf(5, 6, 7, 8) + $samplePrintln + } + """.trimIndent()) + + override val completionStepExpectedCompletion: String = "size" + + override fun LessonContext.contextActions() { + val quickFixMessage = KotlinBundle.message("replace.index.loop.with.collection.loop.quick.fix.text") + caret(sample.getPosition(3)) + + task { + triggerOnEditorText("until", highlightBorder = true) + } + + task("ShowIntentionActions") { + text(KotlinLessonsBundle.message("kotlin.onboarding.invoke.intention.for.warning.1")) + text(KotlinLessonsBundle.message("kotlin.onboarding.invoke.intention.for.warning.2", action(it))) + text(KotlinLessonsBundle.message("kotlin.onboarding.invoke.intention.for.warning.balloon", action(it)), + LearningBalloonConfig(Balloon.Position.above, width = 0, cornerToPointerDistance = 80)) + triggerAndBorderHighlight().listItem { item -> + item.isToStringContains(quickFixMessage) + } + restoreIfModifiedOrMoved() + } + + task { + text(KotlinLessonsBundle.message("kotlin.onboarding.select.fix", strong(quickFixMessage))) + stateCheck { + editor.document.text.contains("for (element in values)") + } + restoreByUi(delayMillis = defaultRestoreDelay) + } + + fun getIntentionMessage(): @Nls String { + return KotlinBundle.message("convert.concatenation.to.template") + } + + caret("RAGE") + + task { + triggerOnEditorText("AVERAGE") + } + + task("ShowIntentionActions") { + text(KotlinLessonsBundle.message("kotlin.onboarding.invoke.intention.for.code", action(it))) + text(KotlinLessonsBundle.message("kotlin.onboarding.invoke.intention.for.code.balloon", action(it)), + LearningBalloonConfig(Balloon.Position.below, width = 0)) + val intentionMessage = getIntentionMessage() + triggerAndBorderHighlight().listItem { item -> + item.isToStringContains(intentionMessage) + } + restoreIfModifiedOrMoved() + } + + task { + text(KotlinLessonsBundle.message("kotlin.onboarding.apply.intention", strong(getIntentionMessage()), LessonUtil.rawEnter())) + stateCheck { + val text = editor.document.text + text.contains("\${array.joinToString()}") + } + restoreByUi(delayMillis = defaultRestoreDelay) + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/navigation/KotlinDeclarationAndUsagesLesson.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/navigation/KotlinDeclarationAndUsagesLesson.kt new file mode 100644 index 000000000000..2b3028c5c6a2 --- /dev/null +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/navigation/KotlinDeclarationAndUsagesLesson.kt @@ -0,0 +1,18 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.training.ift.lesson.navigation + +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import org.jetbrains.kotlin.psi.KtCallExpression +import training.dsl.LessonContext +import training.learn.lesson.general.navigation.DeclarationAndUsagesLesson + +class KotlinDeclarationAndUsagesLesson : DeclarationAndUsagesLesson() { + override fun LessonContext.setInitialPosition(): Unit = caret("foo()") + override val sampleFilePath: String get() = "src/DerivedClass2.kt" + override val entityName: String = "foo" + + override fun getParentExpression(element: PsiElement): PsiElement? { + return element.takeIf { element.parentOfType() != null } + } +} diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/navigation/KotlinRecentFilesLesson.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/navigation/KotlinRecentFilesLesson.kt new file mode 100644 index 000000000000..83e039c2de28 --- /dev/null +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/navigation/KotlinRecentFilesLesson.kt @@ -0,0 +1,25 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.training.ift.lesson.navigation + +import training.dsl.LessonContext +import training.dsl.LessonUtil +import training.learn.LessonsBundle +import training.learn.lesson.general.navigation.RecentFilesLesson + +class KotlinRecentFilesLesson : RecentFilesLesson() { + override val sampleFilePath: String = "src/RecentFilesDemo.kt" + + override val transitionMethodName: String = "println" + override val transitionFileName: String = "Console" + override val stringForRecentFilesSearch: String = "print" + + override fun LessonContext.setInitialPosition(): Unit = caret("println") + + override val helpLinks: Map + get() = mapOf( + Pair( + LessonsBundle.message("recent.files.locations.help.link"), + LessonUtil.getHelpLink("idea", "discover-intellij-idea.html#recent-files") + ), + ) +} \ No newline at end of file diff --git a/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/refactorings/KotlinRefactoringMenuLesson.kt b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/refactorings/KotlinRefactoringMenuLesson.kt new file mode 100644 index 000000000000..38cb00a3c35a --- /dev/null +++ b/plugins/kotlin/features-trainer/src/org/jetbrains/kotlin/training/ift/lesson/refactorings/KotlinRefactoringMenuLesson.kt @@ -0,0 +1,86 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.training.ift.lesson.refactorings + +import com.intellij.java.ift.JavaLessonsBundle +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.training.ift.KotlinLessonsBundle +import training.dsl.LessonContext +import training.dsl.LessonSample +import training.dsl.LessonUtil +import training.dsl.LessonUtil.restoreIfModifiedOrMoved +import training.dsl.dropMnemonic +import training.dsl.parseLessonSample +import training.dsl.restoreRefactoringOptionsInformer +import training.learn.lesson.general.refactorings.RefactoringMenuLessonBase +import training.util.adaptToNotNativeLocalization + +class KotlinRefactoringMenuLesson : RefactoringMenuLessonBase("java.refactoring.menu") { + override val sample: LessonSample = parseLessonSample( + """ +import java.io.FileReader +import java.io.BufferedReader + +fun main() { + val list = readStrings() + val filtered = list.filter { it.isNotEmpty() } + filtered.forEach { + println(it) + } +} + +fun readStrings(): List { + return BufferedReader().readLines() +}""".trimIndent()) + + override val lessonContent: LessonContext.() -> Unit = { + extractParameterTasks() + moreRefactoringsTasks() + restoreRefactoringOptionsInformer() + } + + private fun LessonContext.moreRefactoringsTasks() { + waitBeforeContinue(300) + + val inlineVariableName = "list" + + caret(inlineVariableName) + + actionTask("Inline") { + restoreIfModifiedOrMoved() + if (adaptToNotNativeLocalization) { + JavaLessonsBundle.message( + "java.refactoring.menu.inline.variable", code(inlineVariableName), + action("Refactorings.QuickListPopupAction"), strong(KotlinBundle.message("title.inline.property")), + action(it) + ) + } else KotlinLessonsBundle.message( + "kotlin.refactoring.menu.inline.property.eng", + code(inlineVariableName), action("Refactorings.QuickListPopupAction"), action(it) + ) + } + task { + stateCheck { + !editor.document.charsSequence.contains(inlineVariableName) + } + } + + caret("txt", true) + + actionTask("IntroduceConstant") { + restoreIfModifiedOrMoved() + if (adaptToNotNativeLocalization) { + JavaLessonsBundle.message( + "java.refactoring.menu.introduce.constant", action("Refactorings.QuickListPopupAction"), + strong(KotlinBundle.message("introduce.constant").dropMnemonic()), action(it) + ) + } else JavaLessonsBundle.message( + "java.refactoring.menu.introduce.constant.eng", + action("Refactorings.QuickListPopupAction"), action(it) + ) + } + + actionTask("NextTemplateVariable") { + KotlinLessonsBundle.message("kotlin.refactoring.menu.confirm.constant", LessonUtil.rawEnter()) + } + } +}