package floobits.impl; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.markup.*; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.vfs.ReadonlyStatusHandler; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CodeStyleSettingsManager; import com.intellij.ui.JBColor; import com.intellij.ui.awt.RelativePoint; import floobits.common.Constants; import floobits.common.HighlightContext; import floobits.common.dmp.FlooPatchPosition; import floobits.common.interfaces.IDoc; import floobits.common.protocol.handlers.FlooHandler; import floobits.utilities.Colors; import floobits.utilities.Flog; import javax.swing.*; import java.awt.*; import java.util.*; import java.util.List; public class DocImpl extends IDoc { private final ContextImpl context; private final Document document; private int editorWidth = 0; public final static HashMap<Integer, HashMap<String, LinkedList<RangeHighlighter>>> highlights = new HashMap<Integer, HashMap<String, LinkedList<RangeHighlighter>>>(); public DocImpl(ContextImpl context, Document document) { this.context = context; this.document = document; CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(context.project); // Using deprecated because not all versions and forks of Intellij Have this. // Replace with settings.getRightMargin(null); one day editorWidth = settings.RIGHT_MARGIN; } public String toString() { return document.toString(); } protected LinkedList<RangeHighlighter> getHighlightsForUser(String path, int userID) { HashMap<String, LinkedList<RangeHighlighter>> integerRangeHighlighterHashMap = highlights.get(userID); if (integerRangeHighlighterHashMap == null) { return null; } final LinkedList<RangeHighlighter> rangeHighlighters = integerRangeHighlighterHashMap.get(path); if (rangeHighlighters == null) { return null; } return rangeHighlighters; } protected void removeHighlights(MarkupModel markupModel, LinkedList<RangeHighlighter> appliedHighlighters){ if (appliedHighlighters == null) { return; } RangeHighlighter[] existingHighlighters = markupModel.getAllHighlighters(); for (RangeHighlighter rangeHighlighter: appliedHighlighters) { for (RangeHighlighter markupHighlighter : existingHighlighters) { if (rangeHighlighter == markupHighlighter) { markupModel.removeHighlighter(rangeHighlighter); } } } } @Override public void removeHighlight(Integer userId, final String path) { HashMap<String, LinkedList<RangeHighlighter>> integerRangeHighlighterHashMap = highlights.get(userId); if (integerRangeHighlighterHashMap == null) { return; } final LinkedList<RangeHighlighter> rangeHighlighters = integerRangeHighlighterHashMap.get(path); if (rangeHighlighters == null) { return; } Editor[] editors = EditorFactory.getInstance().getEditors(document, context.project); for (Editor editor : editors) { if (editor.isDisposed()) { continue; } removeHighlights(editor.getMarkupModel(), rangeHighlighters); } rangeHighlighters.clear(); } protected void applyHighlight_(HighlightContext highlight) { final TextAttributes attributes = new TextAttributes(); final JBColor color = Colors.getColorForUser(highlight.username); final ContextImpl context = (ContextImpl) highlight.context; FlooHandler handler = highlight.context.getFlooHandler(); if (handler == null) { return; } attributes.setEffectColor(color); attributes.setEffectType(EffectType.SEARCH_MATCH); attributes.setBackgroundColor(color); attributes.setForegroundColor(Color.WHITE); int textLength = highlight.textLength; int userID = highlight.userid; LinkedList<RangeHighlighter> appliedHighlighters = getHighlightsForUser(highlight.path, userID); LinkedList<RangeHighlighter> newHighlighters = new LinkedList<RangeHighlighter>(); boolean first = true; Editor[] editors = EditorFactory.getInstance().getEditors(document, context.project); for (List<Integer> range : highlight.ranges) { int start = range.get(0); int end = range.get(1); if (start == end) { end += 1; } if (end > textLength) { end = textLength; } if (start >= textLength) { start = textLength - 1; } final int balloonOffset = start; for (final Editor editor : editors) { if (editor.isDisposed()) { continue; } final MarkupModel markupModel = editor.getMarkupModel(); removeHighlights(markupModel, appliedHighlighters); RangeHighlighter rangeHighlighter = markupModel.addRangeHighlighter(start, end, HighlighterLayer.ERROR + 100, attributes, HighlighterTargetArea.EXACT_RANGE); newHighlighters.add(rangeHighlighter); CaretModel caretModel = editor.getCaretModel(); final LogicalPosition logPos = editor.offsetToLogicalPosition(start); final String htmlText = String.format("<p style=\"color:#333\">%s</p>", highlight.username); final ContextImpl.BalloonState balloonState = context.gravatars.get(highlight.gravatar); if (balloonState != null) { int previousLine; Image img; img = balloonState.smallGravatar; previousLine = balloonState.lineNumber; if (first) { first = false; if (highlight.force) { caretModel.moveToOffset(start); LogicalPosition newPosition = caretModel.getLogicalPosition(); ScrollingModel scrollingModel = editor.getScrollingModel(); scrollingModel.disableAnimation(); scrollingModel.scrollTo(newPosition, ScrollType.MAKE_VISIBLE); } } final int bubblePos = editorWidth; if (previousLine != logPos.line && !handler.state.username.equals(highlight.username) && img != null) { final Image gravatarImg = img; ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (context.getFlooHandler() == null) { return; } Point p; try { VisualPosition visPos = new VisualPosition(editor.offsetToVisualPosition(balloonOffset).line, bubblePos); p = editor.visualPositionToXY(visPos); } catch (IndexOutOfBoundsException e) { return; } catch (Throwable e) { return; } Balloon balloon; if (balloonState.balloon != null && !balloonState.balloon.isDisposed()) { balloonState.balloon.setAnimationEnabled(false); balloonState.balloon.dispose(); } balloon = JBPopupFactory.getInstance() .createHtmlTextBalloonBuilder(htmlText, new ImageIcon(gravatarImg), Color.LIGHT_GRAY, null) .setFadeoutTime(750) .setBorderColor(color) .createBalloon(); balloonState.lineNumber = logPos.line; balloon.setAnimationEnabled(false); balloon.show(new RelativePoint(editor.getContentComponent(), p), Balloon.Position.atRight); balloon.setAnimationEnabled(true); balloonState.balloon = balloon; } }); } } } HashMap<String, LinkedList<RangeHighlighter>> integerRangeHighlighterHashMap = highlights.get(userID); if (integerRangeHighlighterHashMap == null) { integerRangeHighlighterHashMap = new HashMap<String, LinkedList<RangeHighlighter>>(); highlights.put(userID, integerRangeHighlighterHashMap); } integerRangeHighlighterHashMap.put(highlight.path, newHighlighters); } } @Override public void applyHighlight(HighlightContext highlight) { final FileEditorManager manager = FileEditorManager.getInstance(context.project); final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if ((highlight.force || highlight.following) && virtualFile != null && virtualFile.isValid()) { boolean spam = false; if (!manager.isFileOpen(virtualFile) || !Arrays.asList(manager.getSelectedFiles()).contains(virtualFile)) { spam = true; } if (spam && highlight.username.length() > 0 && highlight.force) { context.statusMessage(String.format("%s has summoned you!", highlight.username)); } manager.openFile(virtualFile, false, true); } highlight.textLength = document.getTextLength(); if (highlight.textLength == 0) { return; } synchronized (context) { try { context.setListener(false); highlight.force = highlight.force || highlight.following; highlight.context = context; applyHighlight_(highlight); } catch (Throwable e) { Flog.error(e); } finally { context.setListener(true); } } } @Override public void save() { if (context.project == null) { Flog.info("Document: %s can not be saved.", document); return; } if (!ReadonlyStatusHandler.ensureDocumentWritable(context.project, document)) { Flog.info("Document: %s is not writable, can not save.", document); return; } setReadOnly(false); try { FileDocumentManager.getInstance().saveDocument(document); } catch (Throwable e) { Flog.error(e); } } public void setText(final String text) { document.setText(text); } @Override public String getText() { return document.getText(); } @Override public void setReadOnly(boolean readOnly) { document.setReadOnly(readOnly); } @Override public boolean makeWritable() { if (!document.isWritable()) { document.setReadOnly(false); } try { return ReadonlyStatusHandler.ensureDocumentWritable(context.project, document); } catch (Throwable e) { Flog.error(e); } return false; } @Override public FileImpl getVirtualFile() { VirtualFile file = FileDocumentManager.getInstance().getFile(document); if (file == null) { return null; } return new FileImpl(file); } public String patch(FlooPatchPosition[] positions) { for (FlooPatchPosition flooPatchPosition : positions) { final int start = Math.max(0, flooPatchPosition.start); int end_ld = Math.max(start + flooPatchPosition.end, start); end_ld = Math.min(end_ld, document.getTextLength()); final String contents = Constants.NEW_LINE.matcher(flooPatchPosition.text).replaceAll("\n"); final int finalEnd_ld = end_ld; synchronized (context) { try { context.setListener(false); document.replaceString(start, finalEnd_ld, contents); } catch (Throwable e) { Flog.error(e); } finally { context.setListener(true); } } } return document.getText(); } }