/*
* Copyright 2003-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.nodeEditor;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.search.AbstractSearchPanel;
import jetbrains.mps.ide.search.SearchHistoryStorage;
import jetbrains.mps.nodeEditor.cellLayout.PunctuationUtil;
import jetbrains.mps.nodeEditor.cells.EditorCell_Collection;
import jetbrains.mps.nodeEditor.cells.EditorCell_Label;
import jetbrains.mps.nodeEditor.text.TextRenderUtil;
import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.openapi.editor.message.SimpleEditorMessage;
import jetbrains.mps.project.MPSProject;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.util.CollectionUtil;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SearchPanel extends AbstractSearchPanel {
private EditorComponent myEditor;
private List<SearchEntry> mySearchEntries = new ArrayList<>();
private NodeHighlightManager myHighlightManager;
private EditorMessageOwner myOwner;
private SearchHistoryStorage mySearchHistory;
public SearchPanel(EditorComponent editor) {
super();
myEditor = editor;
}
@Override
protected SearchHistoryStorage getSearchHistory() {
if (mySearchHistory == null) {
final MPSProject p = MPSCommonDataKeys.MPS_PROJECT.getData(DataManager.getInstance().getDataContext(myEditor));
if (p != null) {
mySearchHistory = p.getComponent(SearchHistoryComponent.class);
}
if (mySearchHistory == null) {
mySearchHistory = new SearchHistoryComponent();
}
}
return mySearchHistory;
}
private Pair<List<EditorCell_Label>, String> allCellsAndContent() {
StringBuilder sb = new StringBuilder();
List<EditorCell_Label> cells = new ArrayList<>();
EditorCell rootCell = myEditor.getRootCell();
if (rootCell instanceof EditorCell_Label) {
EditorCell_Label cell_label = (EditorCell_Label) rootCell;
cells.add(cell_label);
sb.append(cell_label.getRenderedText());
}
if (rootCell instanceof EditorCell_Collection) {
EditorCell_Collection collection = (EditorCell_Collection) rootCell;
List<EditorCell_Label> editorCell_labelList = CollectionUtil.filter(EditorCell_Label.class, collection.dfsCells());
for (EditorCell_Label label : editorCell_labelList) {
if (PunctuationUtil.hasLeftGap(label)) {
sb.append(" ");
}
sb.append(label.getRenderedText());
}
cells.addAll(editorCell_labelList);
}
return new Pair<>(cells, sb.toString());
}
@Override
protected boolean showExportToFindTool() {
return true;
}
@Override
protected boolean canExportToFindTool() {
return !getMessages().isEmpty();
}
@Override
public void goToPrevious() {
if (mySearchEntries.size() == 0) {
return;
}
addToHistory();
EditorCell selectedCell = myEditor.getDeepestSelectedCell();
int selectionStart = 0;
boolean isEmpty = false;
if (selectedCell instanceof EditorCell_Label) {
EditorCell_Label labelCell = (EditorCell_Label) selectedCell;
selectionStart = labelCell.getSelectionStart();
isEmpty = labelCell.getText().isEmpty();
}
SearchEntry entryToSelect = null;
for (ListIterator<SearchEntry> it = mySearchEntries.listIterator(mySearchEntries.size()); it.hasPrevious() && entryToSelect == null; ) {
SearchEntry currentEntry = it.previous();
if (CellTraversalUtil.compare(selectedCell, currentEntry.getStartLabel()) >= 0) {
while (entryToSelect == null) {
if (!currentEntry.getStartLabel().equals(selectedCell) || (selectionStart >= currentEntry.getFirstRange().getEndPosition() && !isEmpty)) {
entryToSelect = currentEntry;
}
if (it.hasPrevious()) {
currentEntry = it.previous();
} else {
break;
}
}
}
}
if (entryToSelect == null) {
entryToSelect = mySearchEntries.get(mySearchEntries.size() - 1);
}
entryToSelect.select();
}
@Override
public void goToNext() {
if (mySearchEntries.size() == 0) {
return;
}
addToHistory();
EditorCell selectedCell = myEditor.getDeepestSelectedCell();
int selectionEnd = -1;
boolean isEmpty = false;
if (selectedCell instanceof EditorCell_Label) {
EditorCell_Label labelCell = (EditorCell_Label) selectedCell;
selectionEnd = labelCell.getSelectionEnd();
isEmpty = labelCell.getText().isEmpty();
}
SearchEntry entryToSelect = null;
for (ListIterator<SearchEntry> it = mySearchEntries.listIterator(); it.hasNext() && entryToSelect == null; ) {
SearchEntry currentEntry = it.next();
if (CellTraversalUtil.compare(selectedCell, currentEntry.getStartLabel()) <= 0) {
while (entryToSelect == null) {
if (!currentEntry.getStartLabel().equals(selectedCell) || (selectionEnd <= currentEntry.getFirstRange().getStartPosition() && !isEmpty)) {
entryToSelect = currentEntry;
}
if (it.hasNext()) {
currentEntry = it.next();
} else {
break;
}
}
}
}
if (entryToSelect == null) {
entryToSelect = mySearchEntries.get(0);
}
entryToSelect.select();
}
private void clearHighlight() {
if (myOwner != null && myHighlightManager != null && mySearchEntries.size() <= 100) {
myHighlightManager.clearForOwner(myOwner);
}
}
@Override
protected void search() {
search(true);
}
protected void search(boolean requestFocus) {
clearHighlight();
mySearchEntries.clear();
if (myText.getText().length() == 0) {
myFindResult.setText("");
myText.setBackground(myDefaultBackground);
if (requestFocus) {
myText.requestFocus();
myEditor.repaintExternalComponent();
}
return;
}
selectCell(requestFocus);
updateSearchReport(mySearchEntries.size());
}
public void update(AnActionEvent e) {
}
private void selectCell(boolean requestFocus) {
Pair<List<EditorCell_Label>, String> pair = allCellsAndContent();
final List<EditorCell_Label> cells = pair.o1;
List<Integer> startCellPosition = new ArrayList<>();
List<Integer> endCellPosition = new ArrayList<>();
String content = pair.o2;
int current = 0;
List<EditorCell> emptyCells = new ArrayList<>();
for (EditorCell_Label cell : cells) {
if (cell.getRenderedText().isEmpty()) {
emptyCells.add(cell);
}
}
cells.removeAll(emptyCells);
for (EditorCell_Label cell : cells) {
if (current >= content.length()) {
break;
}
String contentPart = content.substring(current);
int start = contentPart.indexOf(cell.getRenderedText()) + current;
startCellPosition.add(start);
current = start + cell.getRenderedText().length();
endCellPosition.add(current);
}
Pattern pattern = getPattern();
if (pattern == null) {
setErrorMessage("Incorrect regular expression");
return;
}
setErrorMessage(null);
Matcher matcher = pattern.matcher(content);
int index = 0;
SearchEntry searchEntryToSelect = null;
while (matcher.find()) {
while (index < endCellPosition.size() && endCellPosition.get(index) <= matcher.start()) {
index++;
}
if (index >= startCellPosition.size()) {
break;
}
if (startCellPosition.get(index) > matcher.start()) {
// found text does not belong to any cell. Looking for next entry.
continue;
}
EditorCell_Label startCell = cells.get(index);
assert startCell != null;
List<TextRange> textRanges = new ArrayList<>();
for (int rangeIndex = index; rangeIndex < startCellPosition.size() && startCellPosition.get(rangeIndex) < matcher.end(); rangeIndex++) {
int startPosition = Math.max(0, matcher.start() - startCellPosition.get(rangeIndex));
int endPosition = Math.min(matcher.end(), endCellPosition.get(rangeIndex)) - startCellPosition.get(rangeIndex);
EditorCell_Label nextCell = cells.get(rangeIndex);
assert nextCell != null;
textRanges.add(new TextRange(nextCell, startPosition, endPosition));
}
SearchEntry searchEntry = new SearchEntry(startCell, textRanges);
mySearchEntries.add(searchEntry);
//noinspection SuspiciousMethodCalls
if (requestFocus && searchEntryToSelect == null && index >= cells.indexOf(myEditor.getSelectedCell())) {
searchEntryToSelect = searchEntry;
}
}
myOwner = new EditorMessageOwner() {
};
if (!mySearchEntries.isEmpty() && mySearchEntries.size() <= 100) {
highlight(mySearchEntries);
}
if (searchEntryToSelect != null) {
searchEntryToSelect.select();
}
}
private void highlight(final List<SearchEntry> searchEntries) {
ModelAccess.instance().runReadAction(() -> {
myHighlightManager = myEditor.getHighlightManager();
List<EditorMessage> messages = new ArrayList<>();
Map<EditorCell_Label, List<Pair>> cellToPositions = new LinkedHashMap<>();
for (SearchEntry searchEntry : searchEntries) {
for (TextRange range : searchEntry.getRangesIterator()) {
if (!cellToPositions.containsKey(range.getLabel())) {
cellToPositions.put(range.getLabel(), new ArrayList<>());
}
cellToPositions.get(range.getLabel()).add(new Pair(range.getStartPosition(), range.getEndPosition()));
}
}
for (EditorCell_Label cell : cellToPositions.keySet()) {
messages.add(new SearchPanelEditorMessage(cell, cellToPositions.get(cell)));
}
myHighlightManager.mark(messages);
});
}
private List<SearchPanelEditorMessage> getMessages() {
final List<SearchPanelEditorMessage> searchMessages = new ArrayList<SearchPanelEditorMessage>();
if (myEditor == null) {
return searchMessages;
}
for (SimpleEditorMessage candidate : myEditor.getMessages()) {
if (candidate instanceof SearchPanelEditorMessage) {
searchMessages.add((SearchPanelEditorMessage) candidate);
}
}
return searchMessages;
}
@Override
public void exportToFindTool() {
final List<SearchPanelEditorMessage> searchMessages = getMessages();
final List<EditorCell_Label> editorLabels = allCellsAndContent().o1;
Collections.sort(searchMessages, (o1, o2) -> {
Integer i1 = editorLabels.indexOf(o1.getCell(myEditor));
Integer i2 = editorLabels.indexOf(o2.getCell(myEditor));
return i1.compareTo(i2);
});
// TODO FIXME
// UsagesViewTool usagesViewTool = new UsagesViewTool(ProjectHelper.toIdeaProject(myEditor.getOperationContext().getProject()));
// BaseNode baseNode = new BaseNode() {
// public SearchResults doGetResults(SearchQuery query, @NotNull ProgressMonitor monitor) {
// monitor.start("", 1);
// SearchResults<SNode> searchResults = new SearchResults<SNode>();
// for (SearchPanelEditorMessage message : searchMessages) {
// EditorCell cell = message.getCell(myEditor);
// if (cell == null) continue;
// SNode node = cell.getSNode();
// searchResults.getSearchResults().add(new SearchResult<SNode>(node, "Search Panel"));
// }
// monitor.done();
// return searchResults;
// }
// };
// SearchQuery searchQuery = new SearchQuery(null) {
// @NotNull
// public String getCaption() {
// return "Occurrences of '" + myText.getText() + "'";
// }
// };
// usagesViewTool.findUsages(baseNode, searchQuery, false, false, false, null);
}
boolean isTextFieldFocused() {
return myText.isFocusOwner();
}
@Override
public void deactivate() {
setVisible(false);
clearHighlight();
if (!mySearchEntries.isEmpty()) {
mySearchEntries.clear();
}
myFindResult.setText("");
myText.setText("");
myText.setBackground(myDefaultBackground);
revalidate();
myEditor.removeUpperComponent(this);
myEditor.requestFocus();
}
@Override
public void activate() {
String initValue = "";
if (myEditor.getDeepestSelectedCell() instanceof EditorCell_Label) {
EditorCell_Label cell_label = (EditorCell_Label) myEditor.getDeepestSelectedCell();
if (cell_label.getSelectionStart() != cell_label.getSelectionEnd()) {
initValue = TextRenderUtil.getTextBuilderForSelectedCellsOfEditor(myEditor).getText();
}
}
setInitialText(initValue);
myEditor.addUpperComponent(this);
super.activate();
}
private class SearchPanelEditorMessage extends DefaultEditorMessage {
@NotNull
private final List<Pair> myPositions;
/**
* Using cell instead of CellInfo here because SearchPanel itself depends on EditorCells
* and re-execute search query/re-create EditorMessages on each underlying editor relayout
*/
@NotNull
private EditorCell_Label myCell;
public SearchPanelEditorMessage(@NotNull EditorCell_Label cell, @NotNull List<Pair> positions) {
super(cell.getSNode(),
EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES).getBackgroundColor(),
"", SearchPanel.this.myOwner);
myCell = cell;
myPositions = positions;
}
@Override
public EditorCell getCell(EditorComponent editor) {
return myCell;
}
@Override
public boolean acceptCell(EditorCell cell, EditorComponent editor) {
return myCell == cell;
}
@Override
public void paint(Graphics g, EditorComponent editorComponent, EditorCell cell) {
if (cell == null || !(cell instanceof EditorCell_Label)) {
return;
}
EditorCell_Label editorCell = (EditorCell_Label) cell;
for (Pair position : myPositions) {
int startPosition = (Integer) position.o1;
int endPosition = (Integer) position.o2;
if (editorCell.getRenderedText().length() >= endPosition) {
FontMetrics metrics = g.getFontMetrics();
String text = editorCell.getRenderedText().substring(startPosition, endPosition);
int prevStringWidth = metrics.stringWidth(editorCell.getRenderedText().
substring(0, startPosition));
int x = editorCell.getX() + editorCell.getLeftInset()
+ prevStringWidth;
int y = editorCell.getY();
int height = editorCell.getHeight();
int width = metrics.stringWidth(text);
g.setColor(getColor());
// Filling smaller rectangle to not cover frames created by other nessages
g.fillRect(x + 1, y + 1, width - 2, height - 2);
}
}
}
@Override
public boolean sameAs(SimpleEditorMessage message) {
return super.sameAs(message) && this.equals(message);
}
@Override
public boolean isBackground() {
return true;
}
@Override
public int getPriority() {
return 10;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SearchPanelEditorMessage)) {
return false;
}
SearchPanelEditorMessage that = (SearchPanelEditorMessage) o;
if (!myCell.equals(that.myCell)) {
return false;
}
if (!myPositions.equals(that.myPositions)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = myPositions.hashCode();
result = 31 * result + myCell.hashCode();
return result;
}
}
private class SearchEntry {
private EditorCell_Label myStartLabel;
private List<TextRange> myTextRanges;
public SearchEntry(@NotNull EditorCell_Label startLabel, @NotNull List<TextRange> textRanges) {
myStartLabel = startLabel;
assert textRanges.size() > 0;
myTextRanges = textRanges;
}
@NotNull
public Iterable<TextRange> getRangesIterator() {
return myTextRanges;
}
@NotNull
public TextRange getFirstRange() {
return myTextRanges.get(0);
}
@NotNull
public EditorCell_Label getStartLabel() {
return myStartLabel;
}
public void select() {
TextRange range = getFirstRange();
myEditor.changeSelection(range.getLabel());
boolean canSetCaretStart = range.getLabel().isCaretPositionAllowed(range.getStartPosition());
if (canSetCaretStart) {
range.getLabel().setCaretPosition(range.getStartPosition());
}
boolean canSetCaretEnd = range.getLabel().isCaretPositionAllowed(range.getEndPosition());
if (canSetCaretEnd) {
range.getLabel().setCaretPosition(range.getEndPosition(), canSetCaretStart);
}
if (!(canSetCaretStart && canSetCaretEnd)) {
range.getLabel().setSelectionStart(range.getStartPosition());
range.getLabel().setSelectionEnd(range.getEndPosition());
}
}
}
private class TextRange {
private EditorCell_Label myLabel;
private int myStartPosition;
private int myEndPosition;
public TextRange(@NotNull EditorCell_Label firstLabel, int startPosition, int endPosition) {
myLabel = firstLabel;
myStartPosition = startPosition;
myEndPosition = endPosition;
}
@NotNull
public EditorCell_Label getLabel() {
return myLabel;
}
public int getStartPosition() {
return myStartPosition;
}
public int getEndPosition() {
return myEndPosition;
}
}
}