From 76db4f66dc44c1d30ddd6996e712d210f578fb29 Mon Sep 17 00:00:00 2001 From: Konstantin Hudyakov Date: Fri, 25 Dec 2020 13:16:29 +0300 Subject: [PATCH] [IFT] Add Find and Replace in files lesson for java, python and ruby IDEA-CR-69975 GitOrigin-RevId: 0db188d67ae1a5c294047e7e4eb95fe01d5f5e85 --- .../java/LearnProject/src/util/Utils.java | 5 + .../src/warehouse/FindInFilesSample.java | 18 ++ .../LearnProject/src/warehouse/Warehouse.java | 49 ++++ .../intellij/java/ift/JavaLearningCourse.kt | 2 + .../res/messages/LessonsBundle.properties | 17 ++ .../general/navigation/FindInFilesLesson.kt | 231 ++++++++++++++++++ .../training/learn/lesson/kimpl/LessonUtil.kt | 8 +- .../PyCharmLearningProject/src/util/util.py | 1 + .../src/warehouse/find_in_files_sample.py | 13 + .../src/warehouse/warehouse.py | 31 +++ .../python/ift/PythonLearningCourse.kt | 2 + 11 files changed, 374 insertions(+), 3 deletions(-) create mode 100644 java/java-features-trainer/resources/learnProjects/java/LearnProject/src/util/Utils.java create mode 100644 java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/FindInFilesSample.java create mode 100644 java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/Warehouse.java create mode 100644 plugins/ide-features-trainer/src/training/learn/lesson/general/navigation/FindInFilesLesson.kt create mode 100644 python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/util/util.py create mode 100644 python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/find_in_files_sample.py create mode 100644 python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/warehouse.py diff --git a/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/util/Utils.java b/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/util/Utils.java new file mode 100644 index 000000000000..4d50e832f413 --- /dev/null +++ b/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/util/Utils.java @@ -0,0 +1,5 @@ +package util; + +public final class Utils { + public static final String[] FRUITS = new String[]{"pineapple", "banana", "apple", "grapes", "mango", "melon", "peach", "orange"}; +} \ No newline at end of file diff --git a/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/FindInFilesSample.java b/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/FindInFilesSample.java new file mode 100644 index 000000000000..b72c207973f1 --- /dev/null +++ b/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/FindInFilesSample.java @@ -0,0 +1,18 @@ +package warehouse; + +public final class FindInFilesSample { + public static void main(String[] args) { + Warehouse warehouse = new Warehouse(); + warehouse.addFruits("peach", 3); + warehouse.addFruits("pineapple", 5); + warehouse.addFruits("mango", 1); + warehouse.addFruits("apple", 5); + + boolean result = warehouse.takeFruit("apple"); + if (result) { + System.out.println("This apple was delicious!"); + } + + warehouse.printAllFruits(); + } +} diff --git a/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/Warehouse.java b/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/Warehouse.java new file mode 100644 index 000000000000..3b739ac21f3d --- /dev/null +++ b/java/java-features-trainer/resources/learnProjects/java/LearnProject/src/warehouse/Warehouse.java @@ -0,0 +1,49 @@ +package warehouse; + +import java.util.HashMap; +import java.util.Map; + +import util.Utils; + +public final class Warehouse { + // Fruit name to amount of it in warehouse + private final Map entry = new HashMap<>(); // Apple, banana, etc... + + public Warehouse() { + String[] availableFruits = Utils.FRUITS; + for (String fruit : availableFruits) { + entry.put(fruit, 0); + } + } + + /** + * @param fruitName some fruit name from Utils.FRUITS (mango, apple...) + */ + public void addFruits(String fruitName, int quantity) { + Integer curQuantity = entry.get(fruitName); + if (curQuantity != null) { + entry.put(fruitName, curQuantity + quantity); + } + else { + throw new IllegalArgumentException("Not found fruit with name: " + fruitName); + } + } + + public boolean takeFruit(String fruitName) { + Integer curQuantity = entry.get(fruitName); + if (curQuantity == null) { + throw new IllegalArgumentException("Not found fruit with name: " + fruitName); + } + else if (curQuantity > 0) { + entry.put(fruitName, curQuantity - 1); + return true; + } + return false; + } + + public void printAllFruits() { + for (Map.Entry pair : entry.entrySet()) { + System.out.println(pair.getKey() + ": " + pair.getValue()); + } + } +} \ No newline at end of file diff --git a/java/java-features-trainer/src/com/intellij/java/ift/JavaLearningCourse.kt b/java/java-features-trainer/src/com/intellij/java/ift/JavaLearningCourse.kt index 92c035af82f0..6bfa9bb71307 100644 --- a/java/java-features-trainer/src/com/intellij/java/ift/JavaLearningCourse.kt +++ b/java/java-features-trainer/src/com/intellij/java/ift/JavaLearningCourse.kt @@ -21,6 +21,7 @@ import training.learn.lesson.general.* import training.learn.lesson.general.assistance.CodeFormatLesson import training.learn.lesson.general.assistance.ParameterInfoLesson import training.learn.lesson.general.assistance.QuickPopupsLesson +import training.learn.lesson.general.navigation.FindInFilesLesson import training.learn.lesson.general.refactorings.ExtractVariableFromBubbleLesson import training.learn.lesson.kimpl.LessonUtil @@ -100,6 +101,7 @@ class JavaLearningCourse : LearningCourseBase(JavaLanguage.INSTANCE.id) { JavaInheritanceHierarchyLesson(it), JavaRecentFilesLesson(it), JavaOccurrencesLesson(it), + FindInFilesLesson(it, lang, "src/warehouse/FindInFilesSample.java") ) }, LearningModule(name = LessonsBundle.message("run.debug.module.name"), diff --git a/plugins/ide-features-trainer/res/messages/LessonsBundle.properties b/plugins/ide-features-trainer/res/messages/LessonsBundle.properties index 750d145b065d..411072812ae7 100644 --- a/plugins/ide-features-trainer/res/messages/LessonsBundle.properties +++ b/plugins/ide-features-trainer/res/messages/LessonsBundle.properties @@ -247,6 +247,23 @@ search.everywhere.finish=Done! Similarly, you can use {0} to lo to look for a file. search.everywhere.navigation.promotion=You will find other navigation actions and workflow use cases in the {0} module. +find.in.files.lesson.name=Find and replace in files +find.in.files.show.find.popup=Suppose you want to find all occurrences of some string in the project. Press {0} to open the {1} window. +find.in.files.type.to.find=Let''s type {0} to look for all textual occurrences in the Learning project. +find.in.files.whole.words=You were supposed to find the apple string but also got some pineapple entries. \ + Let''s narrow down the search to a whole word. Click on {0} or press {1}. +find.in.files.select.row=You can see the context of the found string in the embedded editor. \ + Click the highlighted row or navigate to it by {0} and {1} buttons. +find.in.files.go.to.file=Also, you can open the selected file in the main editor. Press {0} or double click the row to move to this file. +find.in.files.show.replace.popup=Suppose you want to replace all found occurrences with other string. \ + It may be very boring to perform replacing in each file consistently. will help you with this task. Press {0} to open {1} window. +find.in.files.type.to.replace=Let''s replace all occurrences of {0} with {1}. Type {1} in the highlighted field. +find.in.files.select.directory=Also you can search or replace in the specified scope. \ + Click on the {0} button or press {1} to narrow the replacement only to current directory. +find.in.files.press.replace.all=Press {0} to start refactoring. +find.in.files.confirm.replace=And now press {0} to confirm refactoring. +find.in.files.popup.closed.warning.message=Press {0} to open the {1} window again. + extract.method.lesson.name=Extract method extract.method.invoke.action=Press {0} to extract the selected code block into a method. extract.method.start.refactoring=Click {0} to start refactoring. diff --git a/plugins/ide-features-trainer/src/training/learn/lesson/general/navigation/FindInFilesLesson.kt b/plugins/ide-features-trainer/src/training/learn/lesson/general/navigation/FindInFilesLesson.kt new file mode 100644 index 000000000000..b757ce1daeeb --- /dev/null +++ b/plugins/ide-features-trainer/src/training/learn/lesson/general/navigation/FindInFilesLesson.kt @@ -0,0 +1,231 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package training.learn.lesson.general.navigation + +import com.intellij.find.FindBundle +import com.intellij.find.FindInProjectSettings +import com.intellij.find.FindManager +import com.intellij.find.SearchTextArea +import com.intellij.find.impl.FindInProjectSettingsBase +import com.intellij.find.impl.FindPopupPanel +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.impl.ActionButton +import com.intellij.testGuiFramework.fixtures.extended.ExtendedTableFixture +import com.intellij.testGuiFramework.framework.GuiTestUtil +import com.intellij.testGuiFramework.impl.actionButton +import com.intellij.testGuiFramework.impl.button +import com.intellij.testGuiFramework.impl.findComponentWithTimeout +import com.intellij.testGuiFramework.util.Key +import com.intellij.usages.UsagePresentation +import com.intellij.util.ui.UIUtil +import org.fest.swing.core.MouseClickInfo +import org.fest.swing.data.TableCell +import org.fest.swing.fixture.JTextComponentFixture +import training.commands.kotlin.TaskContext +import training.commands.kotlin.TaskRuntimeContext +import training.commands.kotlin.TaskTestContext +import training.learn.LessonsBundle +import training.learn.interfaces.Module +import training.learn.lesson.kimpl.* +import java.awt.event.InputEvent +import java.awt.event.KeyEvent +import javax.swing.* + +class FindInFilesLesson(module: Module, lang: String, override val existedFile: String) + : KLesson("Find in files", LessonsBundle.message("find.in.files.lesson.name"), module, lang) { + + override val lessonContent: LessonContext.() -> Unit = { + resetFindSettings() + + lateinit var showPopupTaskId: TaskContext.TaskId + task("FindInPath") { + showPopupTaskId = taskId + text(LessonsBundle.message("find.in.files.show.find.popup", + action(it), LessonUtil.actionName(it))) + triggerByUiComponentAndHighlight(false, false) { popup: FindPopupPanel -> + !popup.helper.isReplaceState + } + test { + Thread.sleep(300) + actions(it) + } + } + + task("apple") { + text(LessonsBundle.message("find.in.files.type.to.find", code(it))) + stateCheck { getFindPopup()?.stringToFind?.toLowerCase() == it } + restoreByUi() + test { type(it) } + } + + task { + val wholeWordsButtonText = FindBundle.message("find.whole.words").dropMnemonic() + text(LessonsBundle.message("find.in.files.whole.words", + icon(AllIcons.Actions.Words), + LessonUtil.rawKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.ALT_DOWN_MASK)))) + highlightAndTriggerWhenButtonSelected(wholeWordsButtonText) + showWarningIfPopupClosed(false) + test { + ideFrame { + actionButton(wholeWordsButtonText).click() + } + } + } + + task("apple...") { + text(LessonsBundle.message("find.in.files.select.row", + action("EditorUp"), action("EditorDown"))) + triggerByPartOfComponent { table: JTable -> + val rowIndex = table.findLastRowIndexOfItemWithText(it) + if (rowIndex >= 0) { + table.getCellRect(rowIndex, 0, false) + } + else null + } + triggerByUiComponentAndHighlight(false, false) { table: JTable -> + table.selectedRow != -1 && table.selectedRow == table.findLastRowIndexOfItemWithText(it) + } + restoreByUi(restoreId = showPopupTaskId) + test { + ideFrame { + Thread.sleep(300) + val table = findComponentWithTimeout { table: JTable -> table.findLastRowIndexOfItemWithText(it) != -1 } + val tableFixture = ExtendedTableFixture(robot(), table) + val rowIndex = table.findLastRowIndexOfItemWithText(it) + tableFixture.click(TableCell.row(rowIndex).column(0), MouseClickInfo.leftButton()) + } + } + } + + task { + text(LessonsBundle.message("find.in.files.go.to.file", LessonUtil.rawEnter())) + stateCheck { virtualFile.name != existedFile.substringAfterLast('/') } + restoreByUi(restoreId = showPopupTaskId) + test { GuiTestUtil.shortcut(Key.ENTER) } + } + + task("ReplaceInPath") { + text(LessonsBundle.message("find.in.files.show.replace.popup", + action(it), LessonUtil.actionName(it))) + triggerByUiComponentAndHighlight(false, false) { popup: FindPopupPanel -> + popup.helper.isReplaceState + } + test { actions(it) } + } + + task("orange") { + text(LessonsBundle.message("find.in.files.type.to.replace", + code("apple"), code(it))) + triggerByUiComponentAndHighlight(highlightInside = false) { ui: SearchTextArea -> + it.startsWith(ui.textArea.text) + } + stateCheck { + getFindPopup()?.helper?.model?.let { model -> + model.stringToReplace == it && model.stringToFind == "apple" + } ?: false + } + restoreByUi() + test { + ideFrame { + val textArea = findComponentWithTimeout { textArea: JTextArea -> textArea.text == "" } + JTextComponentFixture(robot(), textArea).click() + type(it) + } + } + } + + task { + val directoryScopeText = FindBundle.message("find.popup.scope.directory").dropMnemonic() + text(LessonsBundle.message("find.in.files.select.directory", + strong(directoryScopeText), + LessonUtil.rawKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.ALT_DOWN_MASK)))) + highlightAndTriggerWhenButtonSelected(directoryScopeText) + showWarningIfPopupClosed(true) + test { + ideFrame { + actionButton(directoryScopeText).click() + } + } + } + + val replaceAllDialogTitle = FindBundle.message("find.replace.all.confirmation.title") + task { + val replaceAllButtonText = FindBundle.message("find.popup.replace.all.button").dropMnemonic() + text(LessonsBundle.message("find.in.files.press.replace.all", strong(replaceAllButtonText))) + triggerByUiComponentAndHighlight { button: JButton -> + button.text == replaceAllButtonText + } + triggerByUiComponentAndHighlight(false, false) { dialog: JDialog -> + dialog.title == replaceAllDialogTitle + } + showWarningIfPopupClosed(true) + test { + ideFrame { + Thread.sleep(300) + button(replaceAllButtonText).click() + } + } + } + + task { + val replaceButtonText = FindBundle.message("find.replace.command") + text(LessonsBundle.message("find.in.files.confirm.replace", strong(replaceButtonText))) + stateCheck { editor.document.charsSequence.contains("orange") } + restoreByUi(delayMillis = defaultRestoreDelay) + test { + ideFrame { + Thread.sleep(300) + findMessageDialog(replaceAllDialogTitle).click(replaceButtonText) + } + } + } + } + + private fun TaskRuntimeContext.getFindPopup(): FindPopupPanel? { + return UIUtil.getParentOfType(FindPopupPanel::class.java, focusOwner) + } + + private fun TaskContext.highlightAndTriggerWhenButtonSelected(buttonText: String) { + triggerByUiComponentAndHighlight { button: ActionButton -> + button.action.templateText == buttonText + } + triggerByUiComponentAndHighlight(false, false) { button: ActionButton -> + button.action.templateText == buttonText && button.isSelected + } + } + + private fun JTable.findLastRowIndexOfItemWithText(textToFind: String): Int { + for (ind in (rowCount - 1) downTo 0) { + val item = getValueAt(ind, 0) as? UsagePresentation + if (item?.plainText?.contains(textToFind, true) == true) { + return ind + } + } + return -1 + } + + private fun TaskContext.showWarningIfPopupClosed(isReplacePopup: Boolean) { + val actionId = if (isReplacePopup) "ReplaceInPath" else "FindInPath" + showWarning(LessonsBundle.message("find.in.files.popup.closed.warning.message", action(actionId), LessonUtil.actionName(actionId))) { + getFindPopup()?.helper?.isReplaceState != isReplacePopup + } + } + + private fun LessonContext.resetFindSettings() { + prepareRuntimeTask { + FindManager.getInstance(project).findInProjectModel.apply { + isWholeWordsOnly = false + stringToFind = "" + stringToReplace = "" + directoryName = null + } + val settings = FindInProjectSettings.getInstance(project) as? FindInProjectSettingsBase + settings?.apply { + findStrings.clear() + replaceStrings.clear() + recentDirectories.clear() + } + } + } + + override val testScriptProperties = TaskTestContext.TestScriptProperties(10) +} \ No newline at end of file diff --git a/plugins/ide-features-trainer/src/training/learn/lesson/kimpl/LessonUtil.kt b/plugins/ide-features-trainer/src/training/learn/lesson/kimpl/LessonUtil.kt index 69a6cabb6289..ae6328e31e73 100644 --- a/plugins/ide-features-trainer/src/training/learn/lesson/kimpl/LessonUtil.kt +++ b/plugins/ide-features-trainer/src/training/learn/lesson/kimpl/LessonUtil.kt @@ -1,9 +1,7 @@ // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package training.learn.lesson.kimpl -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.actionSystem.* import com.intellij.openapi.actionSystem.impl.ActionButton import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationNamesInfo @@ -173,6 +171,10 @@ object LessonUtil { return "$keyStroke" } + fun rawKeyStroke(keyStroke: KeyStroke): String { + return "${KeymapUtil.getKeyStrokeText(keyStroke)}" + } + fun rawEnter(): String = rawKeyStroke(KeyEvent.VK_ENTER) fun rawCtrlEnter(): String { diff --git a/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/util/util.py b/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/util/util.py new file mode 100644 index 000000000000..b46e56ee4fe6 --- /dev/null +++ b/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/util/util.py @@ -0,0 +1 @@ +FRUITS = ['pineapple', 'banana', 'apple', 'grapes', 'mango', 'melon', 'peach', 'orange'] diff --git a/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/find_in_files_sample.py b/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/find_in_files_sample.py new file mode 100644 index 000000000000..b41e70d400b2 --- /dev/null +++ b/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/find_in_files_sample.py @@ -0,0 +1,13 @@ +from warehouse.warehouse import Warehouse + +warehouse = Warehouse() +warehouse.add_fruits('peach', 3) +warehouse.add_fruits('pineapple', 5) +warehouse.add_fruits('mango', 1) +warehouse.add_fruits('apple', 5) + +result = warehouse.take_fruit('apple') +if result: + print('This apple was delicious!') + +warehouse.print_all_fruits() diff --git a/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/warehouse.py b/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/warehouse.py new file mode 100644 index 000000000000..01398f415c8a --- /dev/null +++ b/python/python-features-trainer/resources/learnProjects/python/PyCharmLearningProject/src/warehouse/warehouse.py @@ -0,0 +1,31 @@ +from util.util import FRUITS + + +class Warehouse: + # Fruit name to amount of it in warehouse + entry = {} # Apple, banana, etc... + + def __init__(self) -> None: + for fruit in FRUITS: + self.entry[fruit] = 0 + + # fruit name from util.FRUITS (mango, apple...) + def add_fruits(self, fruit_name, quantity) -> None: + cur_quantity = self.entry.get(fruit_name) + if cur_quantity is not None: + self.entry[fruit_name] = cur_quantity + quantity + else: + raise KeyError(f"Not found fruit with name: {fruit_name}") + + def take_fruit(self, fruit_name) -> bool: + cur_quantity = self.entry.get(fruit_name) + if cur_quantity is None: + raise KeyError(f"Not found fruit with name: {fruit_name}") + elif cur_quantity > 0: + self.entry[fruit_name] = cur_quantity - 1 + return True + return False + + def print_all_fruits(self) -> None: + for fruit, quantity in self.entry.items(): + print(f"{fruit}: {quantity}") diff --git a/python/python-features-trainer/src/com/jetbrains/python/ift/PythonLearningCourse.kt b/python/python-features-trainer/src/com/jetbrains/python/ift/PythonLearningCourse.kt index b2bc723a4e1f..14fbdb01f696 100644 --- a/python/python-features-trainer/src/com/jetbrains/python/ift/PythonLearningCourse.kt +++ b/python/python-features-trainer/src/com/jetbrains/python/ift/PythonLearningCourse.kt @@ -26,6 +26,7 @@ import training.learn.lesson.general.* import training.learn.lesson.general.assistance.CodeFormatLesson import training.learn.lesson.general.assistance.ParameterInfoLesson import training.learn.lesson.general.assistance.QuickPopupsLesson +import training.learn.lesson.general.navigation.FindInFilesLesson import training.learn.lesson.general.refactorings.ExtractMethodCocktailSortLesson import training.learn.lesson.general.refactorings.ExtractVariableFromBubbleLesson import training.learn.lesson.kimpl.LessonUtil @@ -111,6 +112,7 @@ class PythonLearningCourse : LearningCourseBase(PythonLanguage.INSTANCE.id) { PythonFileStructureLesson(it), PythonRecentFilesLesson(it), PythonSearchEverywhereLesson(it), + FindInFilesLesson(it, lang, "src/warehouse/find_in_files_sample.py") ) }, LearningModule(name = LessonsBundle.message("run.debug.module.name"),