UX-931 intentions preview: initial commit

GitOrigin-RevId: c66e5007f0a12668b7012c1d8072283ab8adbf43
This commit is contained in:
Dmitry.Krasilschikov
2019-12-03 16:04:11 +00:00
committed by intellij-monorepo-bot
parent 1f6eff05b9
commit 29196834cc
9 changed files with 494 additions and 1 deletions
@@ -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;
@@ -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)) {
@@ -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<EditorEx> = 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<EditorEx>().apply { addAll<EditorEx>(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
}
}
}
@@ -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<Pair<PsiFile?, List<LineFragment>>?> {
private val LOG = Logger.getInstance(IntentionPreviewComputable::class.java)
override fun compute(): Pair<PsiFile?, List<LineFragment>>? {
ProgressManager.checkCanceled()
val psiFileCopy = ApplicationManager.getApplication().runReadAction(Computable<PsiFile> { nonPhysicalPsiCopy(originalFile, project) })
ProgressManager.checkCanceled()
val documentCopy = ApplicationManager.getApplication().runReadAction(
Computable<Document> { FileDocumentManager.getInstance().getDocument(psiFileCopy.viewProvider.virtualFile) })
if (documentCopy == null) return null
CommandProcessor.getInstance().runUndoTransparentAction(Runnable {
try {
ApplicationManager.getApplication().runReadAction(ThrowableComputable<Unit, RuntimeException> {
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<PsiFile?, List<LineFragment>>(
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<IntentionActionWithTextCaching> =
sequenceOf<IntentionActionWithTextCaching>()
.plus(cachedIntentions.intentions)
.plus(cachedIntentions.inspectionFixes)
.plus(cachedIntentions.errorFixes)
}
@@ -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<EditorEx>) : 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))
}
}
@@ -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
}
}
@@ -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<PsiFile?, List<LineFragment>>?): List<EditorEx> {
if (pair == null) return emptyList()
val psiFileCopy: PsiFile? = pair.first
val lines: List<LineFragment> = 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
}
}
}
@@ -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<EditorEx>()
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<Pair<PsiFile?, List<LineFragment>>?>
fun start() {
component.startLoading()
computation = ApplicationManager.getApplication().executeOnPooledThread(
Callable {
try {
ProgressManager.getInstance().runProcess(IntentionPreviewComputable(originalFile, project, originalEditor, action),
progressIndicator)
}
catch (e: ProcessCanceledException) {
Pair<PsiFile?, List<LineFragment>>(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<EditorEx> = emptyList()) {
component.stopLoading()
component.editors = editors
component.multiPanel.select(index, true)
popup.pack(true, true)
}
}
@@ -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: