diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java index a7bd1773b72a..a57142e7729c 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/IntentionHintComponent.java @@ -7,6 +7,7 @@ import com.intellij.codeInsight.hint.*; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInsight.intention.IntentionActionDelegate; import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings; +import com.intellij.codeInsight.intention.impl.preview.IntentionPreviewPopupUpdateProcessor; import com.intellij.codeInsight.unwrap.ScopeHighlighter; import com.intellij.codeInspection.SuppressIntentionActionFromFix; import com.intellij.icons.AllIcons; @@ -40,10 +41,12 @@ import com.intellij.ui.PopupMenuListenerAdapter; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.icons.RowIcon; import com.intellij.ui.popup.WizardPopup; +import com.intellij.ui.popup.list.ListPopupImpl; import com.intellij.util.Alarm; import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.EmptyIcon; +import kotlin.Unit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -77,6 +80,9 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { private static final Border INACTIVE_BORDER = BorderFactory.createEmptyBorder(NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE, NORMAL_BORDER_SIZE); private static final Border INACTIVE_BORDER_SMALL = BorderFactory.createEmptyBorder(SMALL_BORDER_SIZE, SMALL_BORDER_SIZE, SMALL_BORDER_SIZE, SMALL_BORDER_SIZE); + public static final KeyboardShortcut ALT_SPACE_SHORTCUT = KeyboardShortcut.fromString(("alt SPACE")); + public static final String ALT_SPACE_SHORTCUT_TEXT = KeymapUtil.getShortcutText(ALT_SPACE_SHORTCUT); + private final IntentionPreviewPopupUpdateProcessor myPreviewPopupUpdateProcessor; @TestOnly public CachedIntentions getCachedIntentions() { @@ -314,6 +320,7 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { ApplicationManager.getApplication().assertIsDispatchThread(); myFile = file; myEditor = editor; + myPreviewPopupUpdateProcessor = new IntentionPreviewPopupUpdateProcessor(project, myFile, myEditor); myCachedIntentions = cachedIntentions; myPanel.setLayout(new BorderLayout()); myPanel.setOpaque(false); @@ -438,6 +445,7 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { } } } + registerShowPreviewAction(); } boolean committed = PsiDocumentManager.getInstance(myFile.getProject()).isCommitted(myEditor.getDocument()); @@ -464,6 +472,9 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { final Object selectedItem = PlatformDataKeys.SELECTED_ITEM.getData((DataProvider)source); if (selectedItem instanceof IntentionActionWithTextCaching) { IntentionAction action = IntentionActionDelegate.unwrap(((IntentionActionWithTextCaching)selectedItem).getAction()); + if (myPopup instanceof ListPopupImpl) { + updatePreviewPopup(action, ((ListPopupImpl)myPopup).getList().getSelectedIndex()); + } if (action instanceof SuppressIntentionActionFromFix) { if (injectedFile != null && ((SuppressIntentionActionFromFix)action).isShouldBeAppliedToInjectionHost() == ThreeState.NO) { final PsiElement at = injectedFile.findElementAt(injectedEditor.getCaretModel().getOffset()); @@ -504,6 +515,32 @@ public class IntentionHintComponent implements Disposable, ScrollAwareHint { Disposer.register(myPopup, ApplicationManager.getApplication()::assertIsDispatchThread); } + private void updatePreviewPopup(@NotNull IntentionAction action, int index) { + myPreviewPopupUpdateProcessor.setup(text -> { + myPopup.setAdText(text, SwingConstants.LEFT); + return Unit.INSTANCE; + }, index); + myPreviewPopupUpdateProcessor.updatePopup(action); + } + + private void registerShowPreviewAction() { + ((WizardPopup)myPopup).registerAction("showIntentionPreview", ALT_SPACE_SHORTCUT.getFirstKeyStroke(), new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + myPreviewPopupUpdateProcessor.toggleShow(); + if (myPopup instanceof ListPopupImpl) { + JList list = ((ListPopupImpl)myPopup).getList(); + int selectedIndex = list.getSelectedIndex(); + Object selectedValue = list.getSelectedValue(); + if (selectedValue instanceof IntentionActionWithTextCaching) { + updatePreviewPopup(((IntentionActionWithTextCaching)selectedValue).getAction(), selectedIndex); + } + } + } + }); + myPopup.setAdText(CodeInsightBundle.message("intention.preview.adv.show.text", ALT_SPACE_SHORTCUT_TEXT), SwingConstants.LEFT); + } + void canceled(@NotNull ListPopupStep intentionListStep) { if (myPopup.getListStep() != intentionListStep || myDisposed) { return; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/ShowIntentionActionsHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/ShowIntentionActionsHandler.java index 99da52a5debf..803dbf88a9bf 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/ShowIntentionActionsHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/ShowIntentionActionsHandler.java @@ -216,7 +216,7 @@ public class ShowIntentionActionsHandler implements CodeInsightActionHandler { } } - private static void invokeIntention(@NotNull IntentionAction action, @Nullable Editor editor, @NotNull PsiFile file) { + public static void invokeIntention(@NotNull IntentionAction action, @Nullable Editor editor, @NotNull PsiFile file) { IntentionsCollector.getInstance().record(file.getProject(), action, file.getLanguage()); PsiElement elementToMakeWritable = action.getElementToMakeWritable(file); if (elementToMakeWritable != null && !FileModificationService.getInstance().preparePsiElementsForWrite(elementToMakeWritable)) { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewComponent.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewComponent.kt new file mode 100644 index 000000000000..088ea6259614 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewComponent.kt @@ -0,0 +1,50 @@ +// 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.codeInsight.intention.impl.preview + +import com.intellij.codeInsight.CodeInsightBundle +import com.intellij.ide.plugins.MultiPanel +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.project.Project +import com.intellij.ui.components.JBLoadingPanel +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import javax.swing.JComponent +import javax.swing.JLabel + +class IntentionPreviewComponent(project: Project) : JBLoadingPanel(BorderLayout(), + { panel -> IntentionPreviewLoadingDecorator(panel, project) }) { + private var NO_PREVIEW_LABEL = JLabel(CodeInsightBundle.message("intention.preview.no.available.text")).also { setupLabel(it) } + private var LOADING_LABEL = JLabel(CodeInsightBundle.message("intention.preview.initial.loading.preview")).also { setupLabel(it) } + + var editors: List = emptyList() + + val multiPanel: MultiPanel = object : MultiPanel() { + override fun create(key: Int): JComponent { + return when (key) { + NO_PREVIEW -> NO_PREVIEW_LABEL + LOADING_PREVIEW -> LOADING_LABEL + else -> { + if (editors.isEmpty()) return NO_PREVIEW_LABEL + + IntentionPreviewEditorsPanel(mutableListOf().apply { addAll(editors) }) + } + } + } + } + + init { + add(multiPanel) + setLoadingText(CodeInsightBundle.message("intention.preview.loading.preview")) + } + + companion object { + const val NO_PREVIEW = -1 + const val LOADING_PREVIEW = -2 + + private fun setupLabel(label: JLabel) { + label.border = JBUI.Borders.empty(3) + label.background = EditorColorsManager.getInstance().globalScheme.defaultBackground + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewComputable.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewComputable.kt new file mode 100644 index 000000000000..3b86475b48a1 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewComputable.kt @@ -0,0 +1,101 @@ +// 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.codeInsight.intention.impl.preview + +import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.impl.CachedIntentions +import com.intellij.codeInsight.intention.impl.IntentionActionWithTextCaching +import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler +import com.intellij.diff.comparison.ComparisonManager +import com.intellij.diff.comparison.ComparisonPolicy +import com.intellij.diff.fragments.LineFragment +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.progress.DumbProgressIndicator +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Computable +import com.intellij.openapi.util.ThrowableComputable +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory + +class IntentionPreviewComputable(private val originalFile: PsiFile, + private val project: Project, + private val originalEditor: Editor, + private val action: IntentionAction) : Computable>?> { + private val LOG = Logger.getInstance(IntentionPreviewComputable::class.java) + + override fun compute(): Pair>? { + ProgressManager.checkCanceled() + val psiFileCopy = ApplicationManager.getApplication().runReadAction(Computable { nonPhysicalPsiCopy(originalFile, project) }) + ProgressManager.checkCanceled() + val documentCopy = ApplicationManager.getApplication().runReadAction( + Computable { FileDocumentManager.getInstance().getDocument(psiFileCopy.viewProvider.virtualFile) }) + if (documentCopy == null) return null + + CommandProcessor.getInstance().runUndoTransparentAction(Runnable { + try { + ApplicationManager.getApplication().runReadAction(ThrowableComputable { + val editorCopy = EditorFactory.getInstance().createEditor(documentCopy, project) + .also { it.caretModel.moveToOffset(originalEditor.caretModel.offset) } + + val actionWithTextCachingCopy = intentionActionWithTextCaching(editorCopy, psiFileCopy) + if (actionWithTextCachingCopy == null) return@ThrowableComputable + + try { + originalEditor.document.setReadOnly(true) + val fileEditorPair = ShowIntentionActionsHandler.chooseFileForAction(psiFileCopy, editorCopy, actionWithTextCachingCopy.action) + ?: return@ThrowableComputable + ProgressManager.checkCanceled() + actionWithTextCachingCopy.action.invoke(project, fileEditorPair.second, fileEditorPair.first) + ProgressManager.checkCanceled() + } + finally { + originalEditor.document.setReadOnly(false) + ApplicationManager.getApplication().invokeLater(Runnable { EditorFactory.getInstance().releaseEditor(editorCopy) }) + } + }) + } + catch (e: Exception) { + LOG.warn("There are exceptions on invocation the intention: '${action.text}' on a copy of the file.", e) + throw ProcessCanceledException() + } + }) + + return Pair>( + psiFileCopy, + PsiDocumentManager.getInstance(project).commitAndRunReadAction(Computable { + ComparisonManager.getInstance().compareLines(originalFile.text, psiFileCopy.text, ComparisonPolicy.TRIM_WHITESPACES, + DumbProgressIndicator.INSTANCE) + })) + } + + private fun intentionActionWithTextCaching(editorCopy: Editor, + psiFileCopy: PsiFile): IntentionActionWithTextCaching? { + val actionsToShow = ShowIntentionsPass.getActionsToShow(editorCopy, psiFileCopy, false) + val cachedIntentions = CachedIntentions.createAndUpdateActions(project, psiFileCopy, editorCopy, actionsToShow) + return getFixes(cachedIntentions).find { it.text == action.text } + } + + private fun nonPhysicalPsiCopy(psiFile: PsiFile, project: Project): PsiFile { + ProgressManager.checkCanceled() + return PsiFileFactory.getInstance(project).createFileFromText(psiFile.name, + psiFile.language, + psiFile.text, false, true, false, + psiFile.virtualFile) + } + + + fun getFixes(cachedIntentions: CachedIntentions): Sequence = + sequenceOf() + .plus(cachedIntentions.intentions) + .plus(cachedIntentions.inspectionFixes) + .plus(cachedIntentions.errorFixes) +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewEditorsPanel.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewEditorsPanel.kt new file mode 100644 index 000000000000..7fe8c69a75e8 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewEditorsPanel.kt @@ -0,0 +1,31 @@ +// 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.codeInsight.intention.impl.preview + +import com.intellij.openapi.editor.colors.CodeInsightColors +import com.intellij.openapi.editor.colors.EditorColorsScheme +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.ui.VerticalFlowLayout +import com.intellij.ui.Gray +import com.intellij.ui.JBColor +import com.intellij.util.ui.JBUI +import javax.swing.JPanel + +class IntentionPreviewEditorsPanel(val editors: List) : JPanel(VerticalFlowLayout(0, 0)) { + init { + editors.forEachIndexed { index, editor -> + add(editor.component) + if (index < editors.size - 1) { + add(createSeparatorLine(editor.colorsScheme)) + } + } + } + + private fun createSeparatorLine(colorsScheme: EditorColorsScheme): JPanel { + var color = colorsScheme.getColor(CodeInsightColors.METHOD_SEPARATORS_COLOR) + if (color == null) { + color = JBColor.namedColor("Group.separatorColor", JBColor(Gray.xCD, Gray.x51)) + } + + return JBUI.Panels.simplePanel().withBorder(JBUI.Borders.customLine(color, 1, 0, 0, 0)) + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewLoadingDecorator.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewLoadingDecorator.kt new file mode 100644 index 000000000000..a91a43e5027d --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewLoadingDecorator.kt @@ -0,0 +1,34 @@ +// 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.codeInsight.intention.impl.preview + +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.EditorFontType +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.LoadingDecorator +import com.intellij.ui.ColorUtil +import com.intellij.ui.components.panels.NonOpaquePanel +import com.intellij.util.ui.AsyncProcessIcon +import java.awt.BorderLayout +import javax.swing.JLabel +import javax.swing.JPanel + +class IntentionPreviewLoadingDecorator(panel: JPanel, project: Project) : + LoadingDecorator(panel, project, 500, false, AsyncProcessIcon("IntentionPreviewProcessLoading")) { + override fun customizeLoadingLayer(parent: JPanel, text: JLabel, icon: AsyncProcessIcon): NonOpaquePanel { + val opaquePanel = + NonOpaquePanel(BorderLayout(100, 100)).also { + it.isOpaque = true + it.background = ColorUtil.withAlpha(EditorColorsManager.getInstance().globalScheme.defaultBackground, 0.6) + it.add(icon) + it.add(text) + } + + parent.layout = BorderLayout() + parent.add(opaquePanel) + + val font = EditorColorsManager.getInstance().globalScheme.getFont(EditorFontType.PLAIN) + text.font = font.deriveFont(font.style, (font.size + 2).toFloat()); + + return opaquePanel + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewModel.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewModel.kt new file mode 100644 index 000000000000..4deb80e213a0 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewModel.kt @@ -0,0 +1,87 @@ +// 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.codeInsight.intention.impl.preview + +import com.intellij.diff.comparison.ComparisonManager +import com.intellij.diff.comparison.ComparisonPolicy +import com.intellij.diff.fragments.LineFragment +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.progress.DumbProgressIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.util.ui.JBUI + +class IntentionPreviewModel { + companion object { + fun reformatRange(project: Project, psiFileCopy: PsiFile, lineFragment: LineFragment) { + val start = lineFragment.startOffset2 + val end = lineFragment.endOffset2 + + if (start >= end) return + + val document = FileDocumentManager.getInstance().getDocument(psiFileCopy.viewProvider.virtualFile) + if (document != null) PsiDocumentManager.getInstance(project).commitDocument(document) + + WriteCommandAction.runWriteCommandAction(project, Runnable { + CodeStyleManager.getInstance(project).reformatRange(psiFileCopy, start, end, true) + }) + } + + fun createEditors(project: Project, originalFile: PsiFile, pair: Pair>?): List { + if (pair == null) return emptyList() + + val psiFileCopy: PsiFile? = pair.first + val lines: List = pair.second + + if (psiFileCopy == null) return emptyList() + + lines.forEach { lineFragment -> reformatRange(project, psiFileCopy, lineFragment) } + + return ComparisonManager.getInstance().compareLines(originalFile.text, psiFileCopy.text, + ComparisonPolicy.TRIM_WHITESPACES, DumbProgressIndicator.INSTANCE) + .mapNotNull { + val start = StringUtil.lineColToOffset(psiFileCopy.text, it.startLine2, 0) + val end = StringUtil.lineColToOffset(psiFileCopy.text, it.endLine2, 0) + + if (start >= end) return@mapNotNull null + + val text = psiFileCopy.text.substring(start, end).trimStart('\n').trimEnd('\n').trimIndent() + if (text.isBlank()) return@mapNotNull null + + return@mapNotNull createEditor(project, originalFile.fileType, text, it.startLine1) + } + } + + private fun createEditor(project: Project, fileType: FileType, text: String, lineShift: Int): EditorEx { + val editorFactory = EditorFactory.getInstance() + val document = editorFactory.createDocument(text) + val editor = (editorFactory.createEditor(document, project, fileType, false) as EditorEx) + .also { it.setBorder(JBUI.Borders.empty(2, 0, 2, 0)) } + + editor.settings.also { + it.isLineNumbersShown = true + it.isCaretRowShown = false + it.isLineMarkerAreaShown = false + it.isFoldingOutlineShown = false + it.additionalColumnsCount = 0 + it.additionalLinesCount = 0 + it.isRightMarginShown = false + it.isUseSoftWraps = false + it.isAdditionalPageAtBottom = false + } + + editor.gutterComponentEx.also { + it.setPaintBackground(false) + it.setLineNumberConvertor { line -> line + lineShift } + } + + return editor + } + } +} \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewPopupUpdateProcessor.kt b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewPopupUpdateProcessor.kt new file mode 100644 index 000000000000..2e72419ab030 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/intention/impl/preview/IntentionPreviewPopupUpdateProcessor.kt @@ -0,0 +1,148 @@ +// 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.codeInsight.intention.impl.preview + +import com.intellij.codeInsight.CodeInsightBundle +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.impl.IntentionHintComponent +import com.intellij.codeInsight.intention.impl.preview.IntentionPreviewComponent.Companion.LOADING_PREVIEW +import com.intellij.codeInsight.intention.impl.preview.IntentionPreviewComponent.Companion.NO_PREVIEW +import com.intellij.diff.fragments.LineFragment +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.psi.PsiFile +import com.intellij.ui.popup.PopupPositionManager +import com.intellij.ui.popup.PopupUpdateProcessor +import com.intellij.util.Alarm +import java.util.concurrent.Callable +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +class IntentionPreviewPopupUpdateProcessor(private val project: Project, + private val originalFile: PsiFile, + private val originalEditor: Editor) : PopupUpdateProcessor(project) { + private var index: Int = LOADING_PREVIEW + private var show = false + + private var progressIndicator: ProgressIndicator = EmptyProgressIndicator() + private val alarm = Alarm() + + private lateinit var popup: JBPopup + private lateinit var component: IntentionPreviewComponent + private lateinit var updateAdvText: (String) -> Unit + + private var editorsToRelease = mutableListOf() + + override fun updatePopup(lookupItemObject: Any?) { + if (!show) return + + alarm.cancelAllRequests() + progressIndicator.cancel() + + if (!::popup.isInitialized || popup.isDisposed) { + component = IntentionPreviewComponent(project) + component.multiPanel.select(LOADING_PREVIEW, true) + + popup = JBPopupFactory.getInstance().createComponentPopupBuilder(component, null) + .setCancelCallback { cancel() } + .createPopup() + + PopupPositionManager.positionPopupInBestPosition(popup, originalEditor, null) + + updateAdvText.invoke(CodeInsightBundle.message("intention.preview.adv.hide.text")) + } + + val value = component.multiPanel.getValue(index, false) + if (value != null) { + select(index) + return + } + + val action = lookupItemObject as IntentionAction + if (!action.startInWriteAction() || action.getElementToMakeWritable(originalFile) !== originalFile) { + select(NO_PREVIEW) + return + } + + UpdatePopup(project, action, originalFile, originalEditor).start() + } + + fun setup(updateAdvConsumer: (String) -> Unit, parentIndex: Int) { + index = parentIndex + updateAdvText = updateAdvConsumer + } + + private fun cancel(): Boolean { + editorsToRelease.forEach { EditorFactory.getInstance().releaseEditor(it) } + editorsToRelease.clear() + component.removeAll() + alarm.cancelAllRequests() + show = false + updateAdvText.invoke(CodeInsightBundle.message("intention.preview.adv.show.text", IntentionHintComponent.ALT_SPACE_SHORTCUT_TEXT)) + return true + } + + inner class UpdatePopup(private val project: Project, + private val action: IntentionAction, + private val originalFile: PsiFile, + private val originalEditor: Editor) : Runnable { + lateinit var computation: Future>?> + + fun start() { + component.startLoading() + computation = ApplicationManager.getApplication().executeOnPooledThread( + Callable { + try { + ProgressManager.getInstance().runProcess(IntentionPreviewComputable(originalFile, project, originalEditor, action), + progressIndicator) + } + catch (e: ProcessCanceledException) { + Pair>(null, emptyList()) + } + }) + + alarm.addRequest(this, 100) + } + + override fun run() { + if (!computation.isCancelled && !computation.isDone) { + alarm.addRequest(this, 200) + return + } + + try { + val editors = IntentionPreviewModel.createEditors(project, originalFile, computation.get(3, TimeUnit.SECONDS)) + if (editors.isEmpty()) { + select(NO_PREVIEW) + return + } + + editorsToRelease.addAll(editors) + select(index, editors) + } + catch (e: Exception) { + select(NO_PREVIEW) + } + } + } + + fun toggleShow() { + show = !show + } + + private fun select(index: Int, editors: List = emptyList()) { + component.stopLoading() + component.editors = editors + component.multiPanel.select(index, true) + + popup.pack(true, true) + } +} \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties index 46e9d5494c6f..2d9769ba62f7 100644 --- a/platform/platform-resources-en/src/messages/CodeInsightBundle.properties +++ b/platform/platform-resources-en/src/messages/CodeInsightBundle.properties @@ -268,6 +268,11 @@ intention.wrap.with.unmodifiable.list=Wrap with unmodifiable list intention.wrap.with.unmodifiable.set=Wrap with unmodifiable set intention.wrap.with.unmodifiable.map=Wrap with unmodifiable map +intention.preview.adv.show.text=Press {0} to open preview +intention.preview.adv.hide.text=Press Escape to hide preview +intention.preview.no.available.text=Preview isn't available +intention.preview.loading.preview=Loading... +intention.preview.initial.loading.preview=Intention Preview lightbulb.tooltip=Click or press {0} dialog.intention.settings.intention.list.title=Intention List dialog.intention.settings.description.panel.title=Description: