package org.intellij.plugins.markdown.ui.actions.styling;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.containers.ContainerUtil;
import org.intellij.plugins.markdown.ui.actions.MarkdownActionUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public abstract class BaseToggleStateAction extends ToggleAction implements DumbAware {
private static final Logger LOG = Logger.getInstance(BaseToggleStateAction.class);
@NotNull
protected abstract String getBoundString(@NotNull CharSequence text, int selectionStart, int selectionEnd);
@Nullable
protected String getExistingBoundString(@NotNull CharSequence text, int startOffset) {
return String.valueOf(text.charAt(startOffset));
}
protected abstract boolean shouldMoveToWordBounds();
@NotNull
protected abstract IElementType getTargetNodeType();
@NotNull
protected SelectionState getCommonState(@NotNull PsiElement element1, @NotNull PsiElement element2) {
return MarkdownActionUtil.getCommonParentOfType(element1, element2, getTargetNodeType()) == null
? SelectionState.NO
: SelectionState.YES;
}
@Override
public void update(@NotNull AnActionEvent e) {
e.getPresentation().setEnabled(MarkdownActionUtil.findMarkdownTextEditor(e) != null);
super.update(e);
}
@Override
public boolean isSelected(AnActionEvent e) {
final Editor editor = MarkdownActionUtil.findMarkdownTextEditor(e);
final PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
if (editor == null || psiFile == null) {
return false;
}
SelectionState lastState = null;
for (Caret caret : editor.getCaretModel().getAllCarets()) {
final Couple<PsiElement> elements = MarkdownActionUtil.getElementsUnderCaretOrSelection(psiFile, caret);
if (elements == null) {
continue;
}
final SelectionState state = getCommonState(elements.getFirst(), elements.getSecond());
if (lastState == null) {
lastState = state;
}
else if (lastState != state) {
lastState = SelectionState.INCONSISTENT;
break;
}
}
if (lastState == SelectionState.INCONSISTENT) {
e.getPresentation().setEnabled(false);
return false;
}
else {
e.getPresentation().setEnabled(true);
return lastState == SelectionState.YES;
}
}
@Override
public void setSelected(AnActionEvent e, final boolean state) {
final Editor editor = MarkdownActionUtil.findMarkdownTextEditor(e);
final PsiFile psiFile = e.getData(CommonDataKeys.PSI_FILE);
if (editor == null || psiFile == null) {
return;
}
WriteCommandAction.runWriteCommandAction(psiFile.getProject(), () -> {
if (!psiFile.isValid()) {
return;
}
final Document document = editor.getDocument();
for (Caret caret : ContainerUtil.reverse(editor.getCaretModel().getAllCarets())) {
if (!state) {
final Couple<PsiElement> elements = MarkdownActionUtil.getElementsUnderCaretOrSelection(psiFile, caret);
if (elements == null) {
continue;
}
final PsiElement closestEmph = MarkdownActionUtil.getCommonParentOfType(elements.getFirst(),
elements.getSecond(),
getTargetNodeType());
if (closestEmph == null) {
LOG.warn("Could not find enclosing element on its destruction");
continue;
}
final TextRange range = closestEmph.getTextRange();
removeEmphFromSelection(document, caret, range);
}
else {
addEmphToSelection(document, caret);
}
}
PsiDocumentManager.getInstance(psiFile.getProject()).commitDocument(document);
});
}
public void removeEmphFromSelection(@NotNull Document document, @NotNull Caret caret, @NotNull TextRange nodeRange) {
final CharSequence text = document.getCharsSequence();
final String boundString = getExistingBoundString(text, nodeRange.getStartOffset());
if (boundString == null) {
LOG.warn("Could not fetch bound string from found node");
return;
}
final int boundLength = boundString.length();
// Easy case --- selection corresponds to some emph
if (nodeRange.getStartOffset() + boundLength == caret.getSelectionStart()
&& nodeRange.getEndOffset() - boundLength == caret.getSelectionEnd()) {
document.deleteString(nodeRange.getEndOffset() - boundLength, nodeRange.getEndOffset());
document.deleteString(nodeRange.getStartOffset(), nodeRange.getStartOffset() + boundLength);
return;
}
int from = caret.getSelectionStart();
int to = caret.getSelectionEnd();
if (shouldMoveToWordBounds()) {
while (from - boundLength > nodeRange.getStartOffset() && Character.isWhitespace(text.charAt(from - 1))) {
from--;
}
while (to + boundLength < nodeRange.getEndOffset() && Character.isWhitespace(text.charAt(to))) {
to++;
}
}
if (to + boundLength == nodeRange.getEndOffset()) {
document.deleteString(nodeRange.getEndOffset() - boundLength, nodeRange.getEndOffset());
}
else {
document.insertString(to, boundString);
}
if (from - boundLength == nodeRange.getStartOffset()) {
document.deleteString(nodeRange.getStartOffset(), nodeRange.getStartOffset() + boundLength);
}
else {
document.insertString(from, boundString);
}
}
public void addEmphToSelection(@NotNull Document document, @NotNull Caret caret) {
int from = caret.getSelectionStart();
int to = caret.getSelectionEnd();
final CharSequence text = document.getCharsSequence();
if (shouldMoveToWordBounds()) {
while (from < to && Character.isWhitespace(text.charAt(from))) {
from++;
}
while (to > from && Character.isWhitespace(text.charAt(to - 1))) {
to--;
}
if (from == to) {
from = caret.getSelectionStart();
to = caret.getSelectionEnd();
}
}
final String boundString = getBoundString(text, from, to);
document.insertString(to, boundString);
document.insertString(from, boundString);
if (caret.getSelectionStart() == caret.getSelectionEnd()) {
caret.moveCaretRelatively(boundString.length(), 0, false, false);
}
}
protected enum SelectionState {
YES,
NO,
INCONSISTENT
}
}