mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IFT-265 Add lesson about Local History feature
GitOrigin-RevId: 4438df6ef2c2fa728b604969d13d07a4f6e49808
This commit is contained in:
committed by
intellij-monorepo-bot
parent
458f9d8ce9
commit
8e91f22b20
@@ -21,6 +21,7 @@ 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
|
||||
@@ -87,6 +88,7 @@ class JavaLearningCourse : LearningCourseBase(JavaLanguage.INSTANCE.id) {
|
||||
moduleType = LessonType.SINGLE_EDITOR) {
|
||||
fun ls(sampleName: String) = loadSample("CodeAssistance/$sampleName")
|
||||
listOf(
|
||||
LocalHistoryLesson(),
|
||||
CodeFormatLesson(ls("CodeFormat.java.sample"), true),
|
||||
ParameterInfoLesson(ls("ParameterInfo.java.sample")),
|
||||
QuickPopupsLesson(ls("QuickPopups.java.sample")),
|
||||
|
||||
@@ -175,6 +175,20 @@ refactoring.menu.finish.refactoring=To complete refactoring, you need to choose
|
||||
code.assistance.module.name=Code assistance
|
||||
code.assistance.module.description=Learn how to format your code, obtain parameter info, and preview quick popups.
|
||||
|
||||
local.history.lesson.name=Restore removed code
|
||||
local.history.remove.code=In this lesson, you will learn how to restore code using {0}. Press {1} to remove the selected code fragment.
|
||||
local.history.file.modification.progress=Modifying file
|
||||
local.history.editor.hint=File modification is disabled for the current step.
|
||||
local.history.dialog.message=The IDE simulates the process of editing a file. Please wait until it completes.
|
||||
local.history.imagine.restore=Let''s imagine that you need to restore the code that you deleted in the beginning. The {0} action cannot help you because there were a lot of other changes since that, and you might lose them.
|
||||
local.history.invoke.context.menu=With {0} you can restore the deleted code with a couple of clicks. Right-click the editor to invoke the context menu.
|
||||
local.history.show.history=Choose {0} \u2192 {1}.
|
||||
local.history.select.revision=The left pane of the {0} dialog displays a list of saved revisions of the current file. The project state is automatically saved to {1} when you edit code, run tests, and perform other actions. For example, select the highlighted revision.
|
||||
local.history.restore.code=The right-hand pane shows a diff viewer that displays the differences between each revision and the current one. You can see the code in this revision that has been deleted at the beginning of the lesson. Click the chevron button {0} to restore the code fragment.
|
||||
local.history.restore.code.balloon=Click to restore the code fragment
|
||||
local.history.close.window=Close the dialog to return to the changed code. Press {0}.
|
||||
local.history.congratulations=Great! You have restored your code fragment!
|
||||
|
||||
code.format.lesson.name=Code format
|
||||
code.format.reformat.selection=<ide/> can help you correct code formatting with just one action. \
|
||||
to reformat the selected code fragment with {0}.
|
||||
|
||||
@@ -15,10 +15,7 @@ import org.fest.swing.driver.BasicJListCellReader
|
||||
import org.fest.swing.driver.ComponentDriver
|
||||
import org.fest.swing.exception.ComponentLookupException
|
||||
import org.fest.swing.exception.WaitTimedOutError
|
||||
import org.fest.swing.fixture.AbstractComponentFixture
|
||||
import org.fest.swing.fixture.ContainerFixture
|
||||
import org.fest.swing.fixture.JButtonFixture
|
||||
import org.fest.swing.fixture.JListFixture
|
||||
import org.fest.swing.fixture.*
|
||||
import org.fest.swing.timing.Condition
|
||||
import org.fest.swing.timing.Pause
|
||||
import org.fest.swing.timing.Timeout
|
||||
@@ -29,10 +26,7 @@ import java.awt.Component
|
||||
import java.awt.Container
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JDialog
|
||||
import javax.swing.JLabel
|
||||
import javax.swing.JList
|
||||
import javax.swing.*
|
||||
|
||||
@LearningDsl
|
||||
class TaskTestContext(rt: TaskRuntimeContext) : TaskRuntimeContext(rt) {
|
||||
@@ -101,6 +95,12 @@ class TaskTestContext(rt: TaskRuntimeContext) : TaskRuntimeContext(rt) {
|
||||
return ActionButtonFixture(robot(), actionButton)
|
||||
}
|
||||
|
||||
fun <C : Container> ContainerFixture<C>.jMenuItem(timeout: Timeout = defaultTimeout,
|
||||
finderFunction: (JMenuItem) -> Boolean): JMenuItemFixture {
|
||||
val item = findComponentWithTimeout(timeout) { item: JMenuItem -> finderFunction(item) }
|
||||
return JMenuItemFixture(robot(), item)
|
||||
}
|
||||
|
||||
// Modified copy-paste
|
||||
fun <C : Container> ContainerFixture<C>.jListContains(partOfItem: String? = null, timeout: Timeout = defaultTimeout): JListFixture {
|
||||
return generalListFinder(timeout, partOfItem) { element, p -> element.contains(p) }
|
||||
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
// Copyright 2000-2021 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 training.learn.lesson.general.assistance
|
||||
|
||||
import com.intellij.CommonBundle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.idea.ActionsBundle
|
||||
import com.intellij.openapi.actionSystem.impl.ActionMenu
|
||||
import com.intellij.openapi.actionSystem.impl.ActionMenuItem
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.invokeAndWaitIfNeeded
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.EditorModificationUtil
|
||||
import com.intellij.openapi.editor.LogicalPosition
|
||||
import com.intellij.openapi.editor.ex.EditorEx
|
||||
import com.intellij.openapi.editor.ex.EditorGutterComponentEx
|
||||
import com.intellij.openapi.editor.impl.EditorComponentImpl
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.progress.runBackgroundableTask
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.ui.popup.Balloon
|
||||
import com.intellij.ui.components.JBLoadingPanel
|
||||
import com.intellij.ui.components.JBLoadingPanelListener
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.table.JBTable
|
||||
import com.intellij.util.DocumentUtil
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import org.fest.swing.core.MouseButton
|
||||
import org.fest.swing.data.TableCell
|
||||
import org.fest.swing.fixture.JTableFixture
|
||||
import org.jetbrains.annotations.Nls
|
||||
import training.FeaturesTrainerIcons
|
||||
import training.dsl.*
|
||||
import training.dsl.LessonUtil.restoreIfModifiedOrMoved
|
||||
import training.learn.LearnBundle
|
||||
import training.learn.LessonsBundle
|
||||
import training.learn.course.KLesson
|
||||
import training.learn.course.LessonProperties
|
||||
import training.learn.course.LessonType
|
||||
import training.learn.lesson.LessonManager
|
||||
import training.ui.LearningUiHighlightingManager
|
||||
import training.ui.LearningUiHighlightingManager.HighlightingOptions
|
||||
import training.ui.LearningUiUtil
|
||||
import java.awt.Component
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.swing.JFrame
|
||||
|
||||
class LocalHistoryLesson : KLesson("CodeAssistance.LocalHistory", LessonsBundle.message("local.history.lesson.name")) {
|
||||
override val languageId = "yaml"
|
||||
override val lessonType = LessonType.SCRATCH
|
||||
override val properties = LessonProperties(availableSince = "212.5284")
|
||||
|
||||
private val lineToDelete = 14
|
||||
private val revisionInd = 2
|
||||
|
||||
private val sample = parseLessonSample("""
|
||||
cat:
|
||||
name: Pelmen
|
||||
gender: male
|
||||
breed: sphinx
|
||||
fur_type: hairless
|
||||
fur_pattern: solid
|
||||
fur_colors: [ white ]
|
||||
tail_length: long
|
||||
eyes_colors: [ green ]
|
||||
|
||||
favourite_things:
|
||||
- three plaids
|
||||
- pile of clothes
|
||||
- castle of boxes
|
||||
- toys scattered all over the place
|
||||
|
||||
behavior:
|
||||
- play:
|
||||
condition: boring
|
||||
actions:
|
||||
- bring one of the favourite toys to the human
|
||||
- run all over the house
|
||||
- eat:
|
||||
condition: want to eat
|
||||
actions:
|
||||
- shout to the whole house
|
||||
- sharpen claws by the sofa
|
||||
- wake up a human in the middle of the night""".trimIndent())
|
||||
|
||||
private val textToDelete = """
|
||||
| - play:
|
||||
| condition: boring
|
||||
| actions:
|
||||
| - bring one of the favourite toys to the human
|
||||
| - run all over the house
|
||||
""".trimMargin()
|
||||
|
||||
private val textAfterDelete = """
|
||||
| behavior:
|
||||
|
|
||||
| - eat:
|
||||
""".trimMargin()
|
||||
|
||||
private val textToAppend = """
|
||||
| - sleep:
|
||||
| condition: want to sleep
|
||||
| action:
|
||||
| - bury himself in a human's blanket
|
||||
| - bury himself in a favourite plaid
|
||||
""".trimMargin()
|
||||
|
||||
override val lessonContent: LessonContext.() -> Unit = {
|
||||
prepareSample(sample)
|
||||
|
||||
caret(textToDelete, select = true)
|
||||
|
||||
prepareRuntimeTask(ModalityState.NON_MODAL) {
|
||||
FileDocumentManager.getInstance().saveDocument(editor.document)
|
||||
}
|
||||
|
||||
val localHistoryActionText = ActionsBundle.groupText("LocalHistory").dropMnemonic()
|
||||
task {
|
||||
text(LessonsBundle.message("local.history.remove.code", strong(localHistoryActionText), action("EditorDelete")))
|
||||
stateCheck {
|
||||
editor.document.charsSequence.contains(textAfterDelete)
|
||||
}
|
||||
restoreIfModifiedOrMoved()
|
||||
test { invokeActionViaShortcut("DELETE") }
|
||||
}
|
||||
|
||||
setEditorHint(LessonsBundle.message("local.history.editor.hint"))
|
||||
|
||||
waitBeforeContinue(500)
|
||||
|
||||
prepareRuntimeTask {
|
||||
if (!TaskTestContext.inTestMode) {
|
||||
val userDecision = Messages.showOkCancelDialog(
|
||||
LessonsBundle.message("local.history.dialog.message"),
|
||||
LessonsBundle.message("recent.files.dialog.title"),
|
||||
CommonBundle.message("button.ok"),
|
||||
LearnBundle.message("learn.stop.lesson"),
|
||||
FeaturesTrainerIcons.Img.PluginIcon
|
||||
)
|
||||
if (userDecision != Messages.OK) {
|
||||
LessonManager.instance.stopLesson()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifyFile()
|
||||
|
||||
lateinit var invokeMenuTaskId: TaskContext.TaskId
|
||||
task {
|
||||
invokeMenuTaskId = taskId
|
||||
text(LessonsBundle.message("local.history.imagine.restore", strong(ActionsBundle.message("action.\$Undo.text"))))
|
||||
text(LessonsBundle.message("local.history.invoke.context.menu", strong(localHistoryActionText)))
|
||||
triggerByUiComponentAndHighlight { ui: ActionMenu ->
|
||||
ui.text?.contains(localHistoryActionText) == true
|
||||
}
|
||||
test {
|
||||
ideFrame { robot().rightClick(editor.component) }
|
||||
}
|
||||
}
|
||||
|
||||
task("LocalHistory.ShowHistory") {
|
||||
val showHistoryActionText = ActionsBundle.actionText(it).dropMnemonic()
|
||||
text(LessonsBundle.message("local.history.show.history", strong(localHistoryActionText), strong(showHistoryActionText)))
|
||||
triggerByUiComponentAndHighlight(clearPreviousHighlights = false) { ui: ActionMenuItem ->
|
||||
ui.text == showHistoryActionText
|
||||
}
|
||||
trigger(it)
|
||||
restoreByUi()
|
||||
test {
|
||||
ideFrame {
|
||||
jMenuItem { item -> item.text?.contains(localHistoryActionText) == true }.click()
|
||||
jMenuItem { item -> item.text == showHistoryActionText }.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var revisionsTable: JBTable? = null
|
||||
task {
|
||||
triggerByPartOfComponent { ui: JBTable ->
|
||||
if (checkInsideLocalHistoryFrame(ui)) {
|
||||
revisionsTable = ui
|
||||
ui.getCellRect(revisionInd, 0, false)
|
||||
}
|
||||
else null
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var selectRevisionTaskId: TaskContext.TaskId
|
||||
task {
|
||||
selectRevisionTaskId = taskId
|
||||
text(LessonsBundle.message("local.history.select.revision", strong(localHistoryActionText), strong(localHistoryActionText)))
|
||||
val step = CompletableFuture<Boolean>()
|
||||
addStep(step)
|
||||
triggerByUiComponentAndHighlight(false, false, clearPreviousHighlights = false) l@{ ui: JBLoadingPanel ->
|
||||
if (!checkInsideLocalHistoryFrame(ui)) return@l false
|
||||
ui.addListener(object : JBLoadingPanelListener {
|
||||
override fun onLoadingStart() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
override fun onLoadingFinish() {
|
||||
val revisions = revisionsTable ?: return
|
||||
if (revisions.selectionModel.selectedIndices.let { it.size == 1 && it[0] == revisionInd }) {
|
||||
ui.removeListener(this)
|
||||
step.complete(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
true
|
||||
}
|
||||
restoreByUi(invokeMenuTaskId, delayMillis = defaultRestoreDelay)
|
||||
test {
|
||||
ideFrame {
|
||||
Thread.sleep(1000)
|
||||
val table = revisionsTable ?: error("revisionsTable is not initialized")
|
||||
JTableFixture(robot(), table).click(TableCell.row(revisionInd).column(0), MouseButton.LEFT_BUTTON)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task {
|
||||
triggerByPartOfComponent { ui: EditorGutterComponentEx -> findDiffGutterRect(ui) }
|
||||
}
|
||||
|
||||
task {
|
||||
text(LessonsBundle.message("local.history.restore.code", icon(AllIcons.Diff.ArrowRight)))
|
||||
text(LessonsBundle.message("local.history.restore.code.balloon"),
|
||||
LearningBalloonConfig(Balloon.Position.below, 0, cornerToPointerDistance = 50))
|
||||
stateCheck {
|
||||
editor.document.charsSequence.contains(textToDelete)
|
||||
}
|
||||
restoreByUi(invokeMenuTaskId)
|
||||
restoreState(selectRevisionTaskId) l@{
|
||||
val revisions = revisionsTable ?: return@l false
|
||||
revisions.selectionModel.selectedIndices.let { it.size != 1 || it[0] != revisionInd }
|
||||
}
|
||||
test {
|
||||
ideFrame {
|
||||
val gutterComponent = previous.ui as? EditorGutterComponentEx ?: error("Failed to find gutter component")
|
||||
val gutterRect = findDiffGutterRect(gutterComponent) ?: error("Failed to find required gutter")
|
||||
robot().click(gutterComponent, Point(gutterRect.x + gutterRect.width / 2, gutterRect.y + gutterRect.height / 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task {
|
||||
before { LearningUiHighlightingManager.clearHighlights() }
|
||||
text(LessonsBundle.message("local.history.close.window", action("EditorEscape")))
|
||||
stateCheck {
|
||||
previous.ui?.isShowing != true
|
||||
}
|
||||
test {
|
||||
Thread.sleep(500)
|
||||
invokeActionViaShortcut("ESCAPE")
|
||||
}
|
||||
}
|
||||
|
||||
setEditorHint(null)
|
||||
|
||||
text(LessonsBundle.message("local.history.congratulations"))
|
||||
}
|
||||
|
||||
override fun onLessonEnd(project: Project, lessonPassed: Boolean) {
|
||||
if (!lessonPassed) return
|
||||
val editorComponent = LearningUiUtil.findComponentOrNull(EditorComponentImpl::class.java) ?: error("Failed to find editor component")
|
||||
val lines = textToDelete.lines()
|
||||
val rightColumn = lines.maxOf { it.length }
|
||||
LearningUiHighlightingManager.highlightPartOfComponent(editorComponent, HighlightingOptions(highlightInside = false)) {
|
||||
val editor = editorComponent.editor
|
||||
val textToFind = lines[0].trim()
|
||||
val offset = editor.document.charsSequence.indexOf(textToFind)
|
||||
if (offset == -1) error("Failed to find '$textToFind' in the editor")
|
||||
val leftPosition = editor.offsetToLogicalPosition(offset)
|
||||
val leftPoint = editor.logicalPositionToXY(leftPosition)
|
||||
val rightPoint = editor.logicalPositionToXY(LogicalPosition(leftPosition.line, rightColumn))
|
||||
Rectangle(leftPoint.x - 3, leftPoint.y, rightPoint.x - leftPoint.x + 6, editor.lineHeight * lines.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findDiffGutterRect(ui: EditorGutterComponentEx): Rectangle? {
|
||||
val editor = findEditorForGutter(ui) ?: return null
|
||||
val offset = editor.document.charsSequence.indexOf(textToDelete)
|
||||
return if (offset != -1) {
|
||||
val lineIndex = editor.document.getLineNumber(offset)
|
||||
invokeAndWaitIfNeeded {
|
||||
val y = editor.visualLineToY(lineIndex)
|
||||
Rectangle(ui.width - ui.whitespaceSeparatorOffset, y, ui.width - 26, editor.lineHeight)
|
||||
}
|
||||
}
|
||||
else null
|
||||
}
|
||||
|
||||
private fun findEditorForGutter(component: EditorGutterComponentEx): Editor? {
|
||||
val scrollPane = UIUtil.getParentOfType(JBScrollPane::class.java, component) ?: return null
|
||||
return UIUtil.findComponentOfType(scrollPane, EditorComponentImpl::class.java)?.editor
|
||||
}
|
||||
|
||||
private fun TaskRuntimeContext.checkInsideLocalHistoryFrame(component: Component): Boolean {
|
||||
val frame = UIUtil.getParentOfType(JFrame::class.java, component)
|
||||
return frame?.title == virtualFile.path
|
||||
}
|
||||
|
||||
// If message is null it will remove the existing hint and allow file modification
|
||||
private fun LessonContext.setEditorHint(@Nls message: String?) {
|
||||
prepareRuntimeTask {
|
||||
EditorModificationUtil.setReadOnlyHint(editor, message)
|
||||
(editor as EditorEx).isViewer = message != null
|
||||
}
|
||||
}
|
||||
|
||||
private fun LessonContext.modifyFile() {
|
||||
task {
|
||||
addFutureStep {
|
||||
val editor = this.editor
|
||||
runBackgroundableTask(LessonsBundle.message("local.history.file.modification.progress"), project, cancellable = false) {
|
||||
val document = editor.document
|
||||
invokeAndWaitIfNeeded { FileDocumentManager.getInstance().saveDocument(document) }
|
||||
removeLineWithAnimation(editor)
|
||||
invokeAndWaitIfNeeded { FileDocumentManager.getInstance().saveDocument(document) }
|
||||
Thread.sleep(50)
|
||||
insertStringWithAnimation(editor, textToAppend, editor.document.textLength)
|
||||
taskInvokeLater {
|
||||
editor.caretModel.moveToOffset(document.textLength)
|
||||
FileDocumentManager.getInstance().saveDocument(document)
|
||||
completeStep()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
private fun removeLineWithAnimation(editor: Editor) {
|
||||
val document = editor.document
|
||||
val startOffset = document.getLineStartOffset(lineToDelete)
|
||||
val endOffset = document.getLineEndOffset(lineToDelete)
|
||||
for (ind in endOffset downTo startOffset) {
|
||||
invokeAndWaitIfNeeded {
|
||||
DocumentUtil.writeInRunUndoTransparentAction {
|
||||
editor.caretModel.moveToOffset(ind)
|
||||
document.deleteString(ind - 1, ind)
|
||||
}
|
||||
}
|
||||
Thread.sleep(10)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresBackgroundThread
|
||||
private fun insertStringWithAnimation(editor: Editor, text: String, offset: Int) {
|
||||
val document = editor.document
|
||||
for (ind in text.indices) {
|
||||
invokeAndWaitIfNeeded {
|
||||
DocumentUtil.writeInRunUndoTransparentAction {
|
||||
document.insertString(offset + ind, text[ind].toString())
|
||||
editor.caretModel.moveToOffset(offset + ind)
|
||||
}
|
||||
}
|
||||
Thread.sleep(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ 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
|
||||
@@ -98,6 +99,7 @@ class PythonLearningCourse : LearningCourseBase(PythonLanguage.INSTANCE.id) {
|
||||
moduleType = LessonType.SINGLE_EDITOR) {
|
||||
fun ls(sampleName: String) = loadSample("CodeAssistance/$sampleName")
|
||||
listOf(
|
||||
LocalHistoryLesson(),
|
||||
CodeFormatLesson(ls("CodeFormat.py.sample"), true),
|
||||
ParameterInfoLesson(ls("ParameterInfo.py.sample")),
|
||||
QuickPopupsLesson(ls("QuickPopups.py.sample")),
|
||||
|
||||
Reference in New Issue
Block a user