diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/CopyHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/CopyHandler.java index 618e771f7e9e..861a8384f737 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/CopyHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/CopyHandler.java @@ -56,15 +56,15 @@ public class CopyHandler extends EditorActionHandler { final CodeInsightSettings settings = CodeInsightSettings.getInstance(); - if (file == null || settings.ADD_IMPORTS_ON_PASTE == CodeInsightSettings.NO){ - if (myOriginalAction != null){ + if (file == null || settings.ADD_IMPORTS_ON_PASTE == CodeInsightSettings.NO) { + if (myOriginalAction != null) { myOriginalAction.execute(editor, dataContext); } return; } final SelectionModel selectionModel = editor.getSelectionModel(); - if(!selectionModel.hasSelection(true) && !selectionModel.hasBlockSelection()) { + if (!selectionModel.hasSelection(true) && !selectionModel.hasBlockSelection()) { if (Registry.is(CopyAction.SKIP_COPY_AND_CUT_FOR_EMPTY_SELECTION_KEY)) { return; } @@ -89,7 +89,7 @@ public class CopyHandler extends EditorActionHandler { final int[] endOffsets = selectionModel.getBlockSelectionEnds(); List transferableDatas = new ArrayList(); - for(CopyPastePostProcessor processor: Extensions.getExtensions(CopyPastePostProcessor.EP_NAME)) { + for (CopyPastePostProcessor processor : Extensions.getExtensions(CopyPastePostProcessor.EP_NAME)) { final TextBlockTransferableData e = processor.collectTransferableData(file, editor, startOffsets, endOffsets); if (e != null) { transferableDatas.add(e); @@ -98,7 +98,7 @@ public class CopyHandler extends EditorActionHandler { String rawText = TextBlockTransferable.convertLineSeparators(selectionModel.getSelectedText(true), "\n", transferableDatas); String escapedText = null; - for(CopyPastePreProcessor processor: Extensions.getExtensions(CopyPastePreProcessor.EP_NAME)) { + for (CopyPastePreProcessor processor : Extensions.getExtensions(CopyPastePreProcessor.EP_NAME)) { escapedText = processor.preprocessOnCopy(file, startOffsets, endOffsets, rawText); if (escapedText != null) { break; diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TextBlockTransferable.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TextBlockTransferable.java index 2c62b5bf7a4b..075f7618eddd 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TextBlockTransferable.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TextBlockTransferable.java @@ -27,37 +27,38 @@ import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; class TextBlockTransferable implements Transferable { private final Collection myExtraData; private final RawText myRawText; private final String myText; + private final DataFlavor[] myTransferDataFlavors; public TextBlockTransferable(String text, Collection extraData, RawText rawText) { myText = text; myExtraData = extraData; myRawText = rawText; + + List dataFlavors = new ArrayList(); + Collections.addAll(dataFlavors, DataFlavor.stringFlavor, DataFlavor.plainTextFlavor); + final DataFlavor flavor = RawText.getDataFlavor(); + if (myRawText != null && flavor != null) { + dataFlavors.add(flavor); + } + for(TextBlockTransferableData data: extraData) { + final DataFlavor blockFlavor = data.getFlavor(); + if (blockFlavor != null) { + dataFlavors.add(blockFlavor); + } + } + myTransferDataFlavors = dataFlavors.toArray(new DataFlavor[dataFlavors.size()]); } @Override public DataFlavor[] getTransferDataFlavors() { - // We don't cache the result to allow certain dataflavors to 'expire'. - // This is used e.g. for RTF and HTML flavors to avoid memory leaking. - List flavors = new ArrayList(myExtraData.size() + 3); - flavors.add(DataFlavor.stringFlavor); - flavors.add(DataFlavor.plainTextFlavor); - final DataFlavor flavor = RawText.getDataFlavor(); - if (myRawText != null && flavor != null) { - flavors.add(flavor); - } - for(TextBlockTransferableData data : myExtraData) { - final DataFlavor blockFlavor = data.getFlavor(); - if (blockFlavor != null) { - flavors.add(blockFlavor); - } - } - return flavors.toArray(new DataFlavor[flavors.size()]); + return myTransferDataFlavors; } @Override diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/BaseTextWithMarkupCopyPasteProcessor.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/BaseTextWithMarkupCopyPasteProcessor.java index 434dbec160d1..8132487ab157 100644 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/BaseTextWithMarkupCopyPasteProcessor.java +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/BaseTextWithMarkupCopyPasteProcessor.java @@ -16,32 +16,24 @@ package com.intellij.openapi.editor.richcopy; import com.intellij.codeInsight.editorActions.CopyPastePostProcessor; +import com.intellij.codeInsight.editorActions.CopyPastePreProcessor; import com.intellij.codeInsight.editorActions.TextBlockTransferableData; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.RangeMarker; +import com.intellij.openapi.editor.RawText; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; +import com.intellij.openapi.editor.richcopy.view.RawTextHolder; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.registry.Registry; import com.intellij.psi.PsiFile; -import com.intellij.util.StringBuilderSpinAllocator; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.awt.*; import java.awt.datatransfer.Transferable; -public abstract class BaseTextWithMarkupCopyPasteProcessor implements CopyPastePostProcessor, TextWithMarkupBuilder { - private static final Logger LOG = Logger.getInstance("#" + BaseTextWithMarkupCopyPasteProcessor.class.getName()); - private static final String TRUNCATED_MESSAGE = "... truncated ..."; - +public abstract class BaseTextWithMarkupCopyPasteProcessor + implements CopyPastePostProcessor, TextWithMarkupBuilder { private T myData; - private int mySizeLimit; - protected StringBuilder myBuilder; - protected String myDefaultFontFamily; - protected Color myDefaultForeground; - protected Color myDefaultBackground; - protected int myFontSize; protected BaseTextWithMarkupCopyPasteProcessor(TextWithMarkupProcessor processor) { processor.addBuilder(this); @@ -53,10 +45,7 @@ public abstract class BaseTextWithMarkupCopyPasteProcessor mySizeLimit; - if (overflowed) { - setFontFamily(myDefaultFontFamily); - setForeground(myDefaultForeground); - setBackground(myDefaultBackground); - addTextFragment(TRUNCATED_MESSAGE, 0, TRUNCATED_MESSAGE.length()); + protected abstract T doBuild(SyntaxInfo info); + + public static class RawTextSetter implements CopyPastePreProcessor { + private final BaseTextWithMarkupCopyPasteProcessor myProcessor; + + public RawTextSetter(BaseTextWithMarkupCopyPasteProcessor processor) { + myProcessor = processor; } - return overflowed; - } - @Override - public void complete() { - try { - doComplete(); - String data = myBuilder.toString(); - if (Registry.is("editor.richcopy.debug")) { - LOG.info("Resulting text: \n" + data); + @Override + @Nullable + public String preprocessOnCopy(final PsiFile file, final int[] startOffsets, final int[] endOffsets, String text) { + if (myProcessor.myData != null) { + myProcessor.myData.setRawText(text); + myProcessor.myData = null; } - myData = createTransferable(data); + return null; // noop } - finally { - releaseBuilder(); + + @Override + public String preprocessOnPaste(final Project project, final PsiFile file, final Editor editor, String text, final RawText rawText) { + return text; // noop } } - protected abstract T createTransferable(@NotNull String data); - protected abstract void doInit(); - protected abstract void doComplete(); - - private void allocateBuilder() { - if (myBuilder == null) { - myBuilder = StringBuilderSpinAllocator.alloc(); - } - } - - private void releaseBuilder() { - if (myBuilder != null) { - StringBuilderSpinAllocator.dispose(myBuilder); - myBuilder = null; - } - } } diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/HtmlCopyPasteProcessor.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/HtmlCopyPasteProcessor.java index 9456947c8d0f..480556813b40 100644 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/HtmlCopyPasteProcessor.java +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/HtmlCopyPasteProcessor.java @@ -15,184 +15,27 @@ */ package com.intellij.openapi.editor.richcopy; -import com.intellij.openapi.editor.richcopy.view.ReaderTransferableData; -import com.intellij.util.StringBuilderSpinAllocator; -import com.intellij.util.ui.UIUtil; -import org.jetbrains.annotations.NotNull; - -import java.awt.*; -import java.awt.datatransfer.DataFlavor; -import java.util.HashMap; -import java.util.Map; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; +import com.intellij.openapi.editor.richcopy.view.HtmlTransferableData; /** * @author Denis Zhdanov * @since 3/28/13 1:05 PM */ -public class HtmlCopyPasteProcessor extends BaseTextWithMarkupCopyPasteProcessor { - @NotNull public static final DataFlavor FLAVOR = new DataFlavor("text/html;class=java.io.Reader", "HTML text"); - - private Map myRenderedColors = new HashMap(); - - private Color myForeground; - private Color myBackground; - private String myFontFamily; - private boolean myBold; - private boolean myItalic; - +public class HtmlCopyPasteProcessor extends BaseTextWithMarkupCopyPasteProcessor { public HtmlCopyPasteProcessor(TextWithMarkupProcessor processor) { super(processor); } @Override - protected void doInit() { - myForeground = null; - myBackground = null; - myFontFamily = null; - myBold = false; - myItalic = false; - myBuilder.append("
");
+  protected HtmlTransferableData doBuild(SyntaxInfo info) {
+    return new HtmlTransferableData(info);
   }
 
-  @Override
-  protected void doComplete() {
-    myBuilder.append("
"); - } - - @Override - public void setFontFamily(String fontFamily) { - myFontFamily = myDefaultFontFamily.equals(fontFamily) ? null : fontFamily; - } - - @Override - public void setFontStyle(int fontStyle) { - myBold = (Font.BOLD & fontStyle) != 0; - myItalic = (Font.ITALIC & fontStyle) != 0; - } - - @Override - public void setForeground(Color foreground) { - myForeground = foreground; - } - - @Override - public void setBackground(Color background) { - myBackground = myDefaultBackground.equals(background) ? null : background; - } - - @Override - public void addTextFragment(CharSequence charSequence, int startOffset, int endOffset) { - boolean formattedText = myForeground != null || myBackground != null || myFontFamily != null || myBold || myItalic; - if (!formattedText) { - escapeAndAdd(charSequence, startOffset, endOffset); - return; + @SuppressWarnings("ClassNameSameAsAncestorName") + public static class RawTextSetter extends BaseTextWithMarkupCopyPasteProcessor.RawTextSetter { + public RawTextSetter(HtmlCopyPasteProcessor processor) { + super(processor); } - - StringBuilder styleBuffer = StringBuilderSpinAllocator.alloc(); - StringBuilder closeTagBuffer = StringBuilderSpinAllocator.alloc(); - try { - if (myForeground != null) { - defineForeground(myForeground, styleBuffer, closeTagBuffer); - } - if (myBackground != null) { - defineBackground(myBackground, styleBuffer, closeTagBuffer); - } - if (myBold) { - defineBold(styleBuffer, closeTagBuffer); - } - if (myItalic) { - defineItalic(styleBuffer, closeTagBuffer); - } - if (myFontFamily != null) { - appendFontFamilyRule(styleBuffer, myFontFamily); - } - myBuilder.append(""); - escapeAndAdd(charSequence, startOffset, endOffset); - myBuilder.append(""); - myBuilder.append(closeTagBuffer); - } - finally { - StringBuilderSpinAllocator.dispose(styleBuffer); - StringBuilderSpinAllocator.dispose(closeTagBuffer); - } - } - - private void defineForeground(Color foreground, @NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { - myBuilder.append(""); - styleBuffer.append("color:"); - appendColor(styleBuffer, foreground); - styleBuffer.append(";"); - closeTagBuffer.insert(0, ""); - } - - private void defineBackground(Color background, @NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { - myBuilder.append(""); - styleBuffer.append("background-color:"); - appendColor(styleBuffer, background); - styleBuffer.append(";"); - closeTagBuffer.insert(0, ""); - } - - private void defineBold(@NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { - myBuilder.append(""); - styleBuffer.append("font-weight:bold;"); - closeTagBuffer.insert(0, ""); - } - - private void defineItalic(@NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { - myBuilder.append(""); - styleBuffer.append("font-style:italic;"); - closeTagBuffer.insert(0, ""); - } - - private void appendColor(StringBuilder builder, Color color) { - String colorAsString = myRenderedColors.get(color); - if (colorAsString == null) { - StringBuilder b = StringBuilderSpinAllocator.alloc(); - try { - b.append('#'); - UIUtil.appendColor(color, b); - colorAsString = b.toString(); - myRenderedColors.put(color, colorAsString); - } - finally { - StringBuilderSpinAllocator.dispose(b); - } - } - builder.append(colorAsString); - } - - private static void appendFontFamilyRule(@NotNull StringBuilder styleBuffer, String fontFamily) { - styleBuffer.append("font-family:'").append(fontFamily).append("';"); - } - - private void escapeAndAdd(CharSequence charSequence, int start, int end) { - for (int i = start; i < end; i++) { - char c = charSequence.charAt(i); - switch (c) { - case '<': myBuilder.append("<"); break; - case '>': myBuilder.append(">"); break; - case '&': myBuilder.append("&"); break; - default: myBuilder.append(c); - } - } - } - - @Override - protected ReaderTransferableData createTransferable(@NotNull String data) { - return new ReaderTransferableData(data, FLAVOR); } } diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/RtfCopyPasteProcessor.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/RtfCopyPasteProcessor.java index 2a59e400044e..be559b53804f 100644 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/RtfCopyPasteProcessor.java +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/RtfCopyPasteProcessor.java @@ -15,184 +15,27 @@ */ package com.intellij.openapi.editor.richcopy; -import com.intellij.openapi.editor.richcopy.model.*; -import com.intellij.openapi.editor.richcopy.view.InputStreamTransferableData; -import com.intellij.util.StringBuilderSpinAllocator; -import org.jetbrains.annotations.NotNull; - -import java.awt.*; -import java.awt.datatransfer.DataFlavor; -import java.io.UnsupportedEncodingException; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; +import com.intellij.openapi.editor.richcopy.view.RtfTransferableData; /** * @author Denis Zhdanov * @since 3/25/13 2:18 PM */ -public class RtfCopyPasteProcessor extends BaseTextWithMarkupCopyPasteProcessor { - @NotNull public static final DataFlavor FLAVOR = new DataFlavor("text/rtf;class=java.io.InputStream", "RTF text"); - - private static final String CHARSET = "US-ASCII"; - - @NotNull private static final String HEADER_PREFIX = "{\\rtf1\\ansi\\deff0"; - @NotNull private static final String HEADER_SUFFIX = "}"; - @NotNull private static final String TAB = "\\tab\n"; - @NotNull private static final String NEW_LINE = "\\line\n"; - @NotNull private static final String BOLD = "\\b"; - @NotNull private static final String ITALIC = "\\i"; - @NotNull private static final String PLAIN = "\\plain\n"; - - private int myBackgroundId; - private int myForegroundId; - private int myFontNameId; - private ColorRegistry myColorRegistry; - private FontNameRegistry myFontNameRegistry; - +public class RtfCopyPasteProcessor extends BaseTextWithMarkupCopyPasteProcessor { public RtfCopyPasteProcessor(TextWithMarkupProcessor processor) { super(processor); } @Override - protected void doInit() { - myBackgroundId = -1; - myForegroundId = -1; - myFontNameId = -1; - myColorRegistry = new ColorRegistry(); - myFontNameRegistry = new FontNameRegistry(); - - int defaultForegroundId = myColorRegistry.getId(myDefaultForeground); - int defaultBackgroundId = myColorRegistry.getId(myDefaultBackground); - - myBuilder.append("\n\\s0\\box\\brdrhair\\brdrcf").append(defaultForegroundId).append("\\brsp317").append("\\cbpat").append(defaultBackgroundId); - saveBackground(defaultBackgroundId); + protected RtfTransferableData doBuild(SyntaxInfo info) { + return new RtfTransferableData(info); } - @Override - protected void doComplete() { - StringBuilder builder = StringBuilderSpinAllocator.alloc(); - try { - builder.append(HEADER_PREFIX); - - // Color table. - builder.append("{\\colortbl;"); - for (int id : myColorRegistry.getAllIds()) { - Color color = myColorRegistry.dataById(id); - builder.append(String.format("\\red%d\\green%d\\blue%d;", color.getRed(), color.getGreen(), color.getBlue())); - } - builder.append("}\n"); - - // Font table. - builder.append("{\\fonttbl"); - for (int id : myFontNameRegistry.getAllIds()) { - String fontName = myFontNameRegistry.dataById(id); - builder.append(String.format("{\\f%d %s;}", id, fontName)); - } - builder.append("}\n"); - - myBuilder.insert(0, builder); + @SuppressWarnings("ClassNameSameAsAncestorName") + public static class RawTextSetter extends BaseTextWithMarkupCopyPasteProcessor.RawTextSetter { + public RawTextSetter(RtfCopyPasteProcessor processor) { + super(processor); } - finally { - StringBuilderSpinAllocator.dispose(builder); - } - myBuilder.append("\\par"); - myBuilder.append(HEADER_SUFFIX); - } - - @Override - public void setFontFamily(String fontFamily) { - int id = myFontNameRegistry.getId(fontFamily); - saveFontName(id); - myFontNameId = id; - } - - @Override - public void setFontStyle(int fontStyle) { - // Reset formatting settings - myBuilder.append(PLAIN); - - // Restore target formatting settings. - if (myForegroundId >= 0) { - saveForeground(myForegroundId); - } - if (myBackgroundId >= 0) { - saveBackground(myBackgroundId); - } - if (myFontNameId >= 0) { - saveFontName(myFontNameId); - } - saveFontSize(myFontSize); - - if ((fontStyle & Font.ITALIC) > 0) { - myBuilder.append(ITALIC); - } - if ((fontStyle & Font.BOLD) > 0) { - myBuilder.append(BOLD); - } - } - - @Override - public void setForeground(Color foreground) { - int id = myColorRegistry.getId(foreground); - saveForeground(id); - myForegroundId = id; - } - - @Override - public void setBackground(Color background) { - int id = myColorRegistry.getId(background); - saveBackground(id); - myBackgroundId = id; - } - - @Override - public void addTextFragment(CharSequence charSequence, int startOffset, int endOffset) { - myBuilder.append("\n"); - for (int i = startOffset; i < endOffset; i++) { - char c = charSequence.charAt(i); - if (c > 127) { - // Escape non-ascii symbols. - myBuilder.append(String.format("\\u%04d?", (int)c)); - continue; - } - - switch (c) { - case '\t': - myBuilder.append(TAB); - continue; - case '\n': - myBuilder.append(NEW_LINE); - continue; - case '\\': - case '{': - case '}': - myBuilder.append('\\'); - } - myBuilder.append(c); - } - } - - @Override - protected InputStreamTransferableData createTransferable(@NotNull String data) { - try { - return new InputStreamTransferableData(data.getBytes(CHARSET), FLAVOR); - } - catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // shouldn't happen - } - } - - private void saveBackground(int id) { - myBuilder.append("\\cb").append(id); - } - - private void saveForeground(int id) { - myBuilder.append("\\cf").append(id); - } - - private void saveFontName(int id) { - myBuilder.append("\\f").append(id); - } - - private void saveFontSize(int size) { - myBuilder.append("\\fs").append(size * 2); } } diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupBuilder.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupBuilder.java index 813a62a3b1a6..063d7d9c1a7b 100644 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupBuilder.java +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupBuilder.java @@ -15,17 +15,9 @@ */ package com.intellij.openapi.editor.richcopy; -import java.awt.*; -import java.util.EventListener; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; -// we're extending EventListener to be able to use EventDispatcher in TextWithMarkupProcessor -public interface TextWithMarkupBuilder extends EventListener { - void init(Color defaultForeground, Color defaultBackground, String defaultFontFamily, int fontSize); - boolean isOverflowed(); - void setFontFamily(String fontFamily); - void setFontStyle(int fontStyle); - void setForeground(Color foreground); - void setBackground(Color background); - void addTextFragment(CharSequence charSequence, int startOffset, int endOffset); - void complete(); +public interface TextWithMarkupBuilder { + void reset(); + void build(SyntaxInfo syntaxInfo); } diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupProcessor.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupProcessor.java index 7829c92a9870..af2309daf4a5 100644 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupProcessor.java +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupProcessor.java @@ -21,9 +21,7 @@ import com.intellij.codeInsight.editorActions.CopyPastePostProcessor; import com.intellij.codeInsight.editorActions.TextBlockTransferableData; import com.intellij.ide.highlighter.HighlighterFactory; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.RangeMarker; +import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.editor.ex.DisposableIterator; @@ -33,9 +31,11 @@ import com.intellij.openapi.editor.highlighter.EditorHighlighter; import com.intellij.openapi.editor.highlighter.HighlighterIterator; import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry; import com.intellij.openapi.editor.impl.DocumentMarkupModel; +import com.intellij.openapi.editor.impl.FontInfo; import com.intellij.openapi.editor.markup.HighlighterLayer; import com.intellij.openapi.editor.markup.MarkupModel; import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; import com.intellij.openapi.editor.richcopy.settings.RichCopySettings; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; @@ -43,24 +43,24 @@ import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.registry.Registry; import com.intellij.psi.PsiFile; import com.intellij.psi.TokenType; -import com.intellij.util.EventDispatcher; -import com.intellij.util.containers.ContainerUtilRt; +import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; import java.awt.datatransfer.Transferable; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; -import java.util.Queue; public class TextWithMarkupProcessor implements CopyPastePostProcessor { private static final Logger LOG = Logger.getInstance("#" + TextWithMarkupProcessor.class.getName()); - private final EventDispatcher myBuilders = EventDispatcher.create(TextWithMarkupBuilder.class); + private final List myBuilders = new ArrayList(); public void addBuilder(TextWithMarkupBuilder builder) { - myBuilders.addListener(builder); + myBuilders.add(builder); } @Nullable @@ -71,78 +71,82 @@ public class TextWithMarkupProcessor implements CopyPastePostProcessor carets = editor.getCaretModel().getAllCarets(); + Caret firstCaret = carets.get(0); final int indentSymbolsToStrip; final int firstLineStartOffset; - if (settings.isStripIndents() && startOffsets.length == 1) { - Pair p = calcIndentSymbolsToStrip(document, startOffsets[0], endOffsets[0]); + if (settings.isStripIndents() && carets.size() == 1) { + Pair p = calcIndentSymbolsToStrip(editor.getDocument(), firstCaret.getSelectionStart(), firstCaret.getSelectionEnd()); firstLineStartOffset = p.first; indentSymbolsToStrip = p.second; } else { - firstLineStartOffset = startOffsets[0]; + firstLineStartOffset = firstCaret.getSelectionStart(); indentSymbolsToStrip = 0; } - logInitial(document, startOffsets, endOffsets, indentSymbolsToStrip, firstLineStartOffset); - CharSequence text = document.getCharsSequence(); + logInitial(editor, startOffsets, endOffsets, indentSymbolsToStrip, firstLineStartOffset); + CharSequence text = editor.getDocument().getCharsSequence(); EditorColorsScheme schemeToUse = settings.getColorsScheme(editor.getColorsScheme()); EditorHighlighter highlighter = HighlighterFactory.createHighlighter(file.getVirtualFile(), schemeToUse, file.getProject()); highlighter.setText(text); - MarkupModel markupModel = DocumentMarkupModel.forDocument(document, file.getProject(), false); + MarkupModel markupModel = DocumentMarkupModel.forDocument(editor.getDocument(), file.getProject(), false); + Context context = new Context(text, schemeToUse, indentSymbolsToStrip); + int shift = 0; + int endOffset = 0; + Caret prevCaret = null; - myBuilders.getMulticaster().init(schemeToUse.getDefaultForeground(), - schemeToUse.getDefaultBackground(), - FontMapper.getPhysicalFontName(schemeToUse.getEditorFontName()), - schemeToUse.getEditorFontSize()); - - EventDispatcher activeBuilders = EventDispatcher.create(TextWithMarkupBuilder.class); - activeBuilders.getListeners().addAll(myBuilders.getListeners()); - Context context = new Context(activeBuilders.getMulticaster(), text, schemeToUse, indentSymbolsToStrip); - - outer: - for (int i = 0; i < startOffsets.length; i++) { + for (Caret caret : carets) { + int caretSelectionStart = caret.getSelectionStart(); + int caretSelectionEnd = caret.getSelectionEnd(); int startOffsetToUse; - if (i == 0) { + if (caret == firstCaret) { startOffsetToUse = firstLineStartOffset; } else { - startOffsetToUse = startOffsets[i]; - activeBuilders.getMulticaster().addTextFragment("\n", 0, 1); + startOffsetToUse = caretSelectionStart; + assert prevCaret != null; + String prevCaretSelectedText = prevCaret.getSelectedText(); + // Block selection fills short lines by white spaces. + int fillStringLength = prevCaretSelectedText == null ? 0 : prevCaretSelectedText.length() - (prevCaret.getSelectionEnd() - prevCaret.getSelectionStart()); + int endLineOffset = endOffset + shift + fillStringLength; + context.builder.addText(endLineOffset, endLineOffset + 1); + shift++; // Block selection ends '\n' at line end + shift += fillStringLength; } - int endOffsetToUse = endOffsets[i]; - context.reset(); - if (endOffsetToUse <= startOffsetToUse) { + shift += endOffset - caretSelectionStart; + endOffset = caretSelectionEnd; + context.reset(shift); + prevCaret = caret; + if (endOffset <= startOffsetToUse) { continue; } - DisposableIterator it = aggregateSyntaxInfo(schemeToUse, - wrap(highlighter, text, schemeToUse, startOffsetToUse, endOffsetToUse), - wrap(markupModel, text, schemeToUse, startOffsetToUse, endOffsetToUse)); + MarkupIterator markupIterator = new MarkupIterator(text, + new CompositeRangeIterator(schemeToUse, + new HighlighterRangeIterator(highlighter, startOffsetToUse, endOffset), + new MarkupModelRangeIterator(markupModel, schemeToUse, startOffsetToUse, endOffset)), + schemeToUse); try { - while (it.hasNext()) { - Iterator builderIterator = activeBuilders.getListeners().iterator(); - while (builderIterator.hasNext()) { - if (builderIterator.next().isOverflowed()) { - builderIterator.remove(); - if (activeBuilders.getListeners().isEmpty()) { - break outer; - } - } - } - SegmentInfo info = it.next(); - if (info.startOffset >= endOffsetToUse) { - break; - } - context.onNewData(info); - } + context.iterate(markupIterator, endOffset); } finally { - it.dispose(); + markupIterator.dispose(); } - context.onIterationEnd(endOffsetToUse); } + SyntaxInfo syntaxInfo = context.finish(); + logSyntaxInfo(syntaxInfo); - myBuilders.getMulticaster().complete(); + for (TextWithMarkupBuilder builder : myBuilders) { + builder.build(syntaxInfo); + } } catch (Exception e) { // catching the exception so that the rest of copy/paste functionality can still work fine @@ -167,7 +171,7 @@ public class TextWithMarkupProcessor implements CopyPastePostProcessor calcIndentSymbolsToStrip( @NotNull Document document, int startOffset, int endOffset) { @@ -233,267 +245,10 @@ public class TextWithMarkupProcessor implements CopyPastePostProcessor aggregateSyntaxInfo(@NotNull EditorColorsScheme colorsScheme, - @NotNull final DisposableIterator>... iterators) - { - final Color defaultForeground = colorsScheme.getDefaultForeground(); - final Color defaultBackground = colorsScheme.getDefaultBackground(); - return new DisposableIterator() { - - @NotNull private final Queue myInfos = new PriorityQueue(); - @NotNull private final Map>> myEndMarkers - = new IdentityHashMap>>(); - - { - for (DisposableIterator> iterator : iterators) { - extract(iterator); - } - } - - @Override - public boolean hasNext() { - return !myInfos.isEmpty(); - } - - @Override - public SegmentInfo next() { - SegmentInfo result = myInfos.remove(); - DisposableIterator> iterator = myEndMarkers.remove(result); - if (iterator != null) { - extract(iterator); - } - while (!myInfos.isEmpty()) { - SegmentInfo toMerge = myInfos.peek(); - if (toMerge.endOffset > result.endOffset) { - break; - } - myInfos.remove(); - result = merge(result, toMerge); - DisposableIterator> it = myEndMarkers.remove(toMerge); - if (it != null) { - extract(it); - } - } - return result; - } - - @NotNull - private SegmentInfo merge(@NotNull SegmentInfo info1, @NotNull SegmentInfo info2) { - Color background = info1.background; - if (background == null || defaultBackground.equals(background)) { - background = info2.background; - } - - Color foreground = info1.foreground; - if (foreground == null || defaultForeground.equals(foreground)) { - foreground = info2.foreground; - } - - int fontStyle = info1.fontStyle; - if (fontStyle == Font.PLAIN) { - fontStyle = info2.fontStyle; - } - return new SegmentInfo(foreground, background, info1.fontFamilyName, fontStyle, info1.startOffset, info1.endOffset); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - - @Override - public void dispose() { - for (DisposableIterator> iterator : iterators) { - iterator.dispose(); - } - } - - private void extract(@NotNull DisposableIterator> iterator) { - while (iterator.hasNext()) { - List infos = iterator.next(); - if (infos.isEmpty()) { - continue; - } - myInfos.addAll(infos); - myEndMarkers.put(infos.get(infos.size() - 1), iterator); - break; - } - } - }; - } - - @NotNull - private static DisposableIterator> wrap(@NotNull final EditorHighlighter highlighter, - @NotNull final CharSequence text, - @NotNull final EditorColorsScheme colorsScheme, - final int startOffset, - final int endOffset) - { - final HighlighterIterator highlighterIterator = highlighter.createIterator(startOffset); - return new DisposableIterator>() { - - @Nullable private List myCached; - - @Override - public boolean hasNext() { - return myCached != null || updateCached(); - } - - @Override - public List next() { - if (myCached != null) { - List result = myCached; - myCached = null; - return result; - } - - if (!updateCached()) { - throw new UnsupportedOperationException(); - } - return myCached; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - - @Override - public void dispose() { - } - - private boolean updateCached() { - if (highlighterIterator.atEnd()) { - return false; - } - int tokenStart = Math.max(highlighterIterator.getStart(), startOffset); - if (tokenStart >= endOffset) { - return false; - } - - if (highlighterIterator.getTokenType() == TokenType.BAD_CHARACTER) { - // Skip syntax errors. - highlighterIterator.advance(); - return updateCached(); - } - TextAttributes attributes = highlighterIterator.getTextAttributes(); - int tokenEnd = Math.min(highlighterIterator.getEnd(), endOffset); - myCached = SegmentInfo.produce(attributes, text, colorsScheme, tokenStart, tokenEnd); - highlighterIterator.advance(); - return true; - } - }; - } - - @SuppressWarnings("unchecked") - @NotNull - private static DisposableIterator> wrap(@NotNull MarkupModel model, - @NotNull final CharSequence charSequence, - @NotNull final EditorColorsScheme colorsScheme, - final int startOffset, - final int endOffset) - { - if (!(model instanceof MarkupModelEx)) { - return DisposableIterator.EMPTY; - } - final DisposableIterator iterator = ((MarkupModelEx)model).overlappingIterator(startOffset, endOffset); - final Color defaultForeground = colorsScheme.getDefaultForeground(); - final Color defaultBackground = colorsScheme.getDefaultBackground(); - return new DisposableIterator>() { - - @Nullable private List myCached; - - @Override - public boolean hasNext() { - return myCached != null || updateCached(); - } - - @Override - public List next() { - if (myCached != null) { - List result = myCached; - myCached = null; - return result; - } - - if (!updateCached()) { - throw new UnsupportedOperationException(); - } - return myCached; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - - @Override - public void dispose() { - iterator.dispose(); - } - - private boolean updateCached() { - if (!iterator.hasNext()) { - return false; - } - - RangeHighlighterEx highlighter = iterator.next(); - while (highlighter == null - || !highlighter.isValid() - || !isInterestedHighlightLayer(highlighter.getLayer())) - { - if (!iterator.hasNext()) { - return false; - } - highlighter = iterator.next(); - } - - int tokenStart = Math.max(highlighter.getStartOffset(), startOffset); - if (tokenStart >= endOffset) { - return false; - } - - TextAttributes attributes = null; - Object tooltip = highlighter.getErrorStripeTooltip(); - if (tooltip instanceof HighlightInfo) { - HighlightInfo info = (HighlightInfo)tooltip; - TextAttributesKey key = info.forcedTextAttributesKey; - if (key == null) { - HighlightInfoType type = info.type; - key = type.getAttributesKey(); - } - if (key != null) { - attributes = colorsScheme.getAttributes(key); - } - } - - if (attributes == null) { - return updateCached(); - } - Color foreground = attributes.getForegroundColor(); - Color background = attributes.getBackgroundColor(); - if ((foreground == null || defaultForeground.equals(foreground)) - && (background == null || defaultBackground.equals(background)) - && attributes.getFontType() == Font.PLAIN) - { - return updateCached(); - } - - int tokenEnd = Math.min(highlighter.getEndOffset(), endOffset); - //noinspection ConstantConditions - myCached = SegmentInfo.produce(attributes, charSequence, colorsScheme, tokenStart, tokenEnd); - return true; - } - - private boolean isInterestedHighlightLayer(int layer) { - return layer != HighlighterLayer.CARET_ROW && layer != HighlighterLayer.SELECTION && layer != HighlighterLayer.ERROR - && layer != HighlighterLayer.WARNING; - } - }; - } - private static class Context { - @NotNull private final TextWithMarkupBuilder myBuilder; + + private final SyntaxInfo.Builder builder; + @NotNull private final CharSequence myText; @NotNull private final Color myDefaultForeground; @NotNull private final Color myDefaultBackground; @@ -506,91 +261,91 @@ public class TextWithMarkupProcessor implements CopyPastePostProcessor= endOffset) { + break; + } + if (myStartOffset < 0) { + myStartOffset = startOffset; + } - boolean whiteSpacesOnly = containsWhiteSpacesOnly(info); + boolean whiteSpacesOnly = CharArrayUtil.isEmptyOrSpaces(myText, startOffset, iterator.getEndOffset()); - processBackground(info); - if (!whiteSpacesOnly) { - processForeground(info); - processFontFamilyName(info); - processFontStyle(info); - } - } - - private boolean containsWhiteSpacesOnly(@NotNull SegmentInfo info) { - for (int i = info.startOffset, limit = info.endOffset; i < limit; i++) { - char c = myText.charAt(i); - if (c != ' ' && c != '\t' && c != '\n') { - return false; + processBackground(startOffset, iterator.getBackgroundColor()); + if (!whiteSpacesOnly) { + processForeground(startOffset, iterator.getForegroundColor()); + processFontFamilyName(startOffset, iterator.getFontFamilyName()); + processFontStyle(startOffset, iterator.getFontStyle()); } } - return true; + addTextIfPossible(endOffset); } - private void processFontStyle(@NotNull SegmentInfo info) { - if (info.fontStyle != myFontStyle) { - addTextIfPossible(info.startOffset); - myBuilder.setFontStyle(info.fontStyle); - myFontStyle = info.fontStyle; + private void processFontStyle(int startOffset, int fontStyle) { + if (fontStyle != myFontStyle) { + addTextIfPossible(startOffset); + builder.addFontStyle(fontStyle); + myFontStyle = fontStyle; } } - private void processFontFamilyName(@NotNull SegmentInfo info) { - String fontFamilyName = FontMapper.getPhysicalFontName(info.fontFamilyName); + private void processFontFamilyName(int startOffset, String fontName) { + String fontFamilyName = FontMapper.getPhysicalFontName(fontName); if (!fontFamilyName.equals(myFontFamilyName)) { - addTextIfPossible(info.startOffset); - myBuilder.setFontFamily(fontFamilyName); + addTextIfPossible(startOffset); + builder.addFontFamilyName(fontFamilyName); myFontFamilyName = fontFamilyName; } } - private void processForeground(@NotNull SegmentInfo info) { - if (myForeground == null && info.foreground != null) { - addTextIfPossible(info.startOffset); - myForeground = info.foreground; - myBuilder.setForeground(info.foreground); + private void processForeground(int startOffset, Color foreground) { + if (myForeground == null && foreground != null) { + addTextIfPossible(startOffset); + myForeground = foreground; + builder.addForeground(foreground); } else if (myForeground != null) { - Color c = info.foreground == null ? myDefaultForeground : info.foreground; + Color c = foreground == null ? myDefaultForeground : foreground; if (!myForeground.equals(c)) { - addTextIfPossible(info.startOffset); - myBuilder.setForeground(c); + addTextIfPossible(startOffset); + builder.addForeground(c); myForeground = c; } } } - private void processBackground(@NotNull SegmentInfo info) { - if (myBackground == null && info.background != null && !myDefaultBackground.equals(info.background)) { - addTextIfPossible(info.startOffset); - myBackground = info.background; - myBuilder.setBackground(info.background); + private void processBackground(int startOffset, Color background) { + if (myBackground == null && background != null && !myDefaultBackground.equals(background)) { + addTextIfPossible(startOffset); + myBackground = background; + builder.addBackground(background); } else if (myBackground != null) { - Color c = info.background == null ? myDefaultBackground : info.background; + Color c = background == null ? myDefaultBackground : background; if (!myBackground.equals(c)) { - addTextIfPossible(info.startOffset); - myBuilder.setBackground(c); + addTextIfPossible(startOffset); + builder.addBackground(c); myBackground = c; } } @@ -606,7 +361,7 @@ public class TextWithMarkupProcessor implements CopyPastePostProcessor { + private static class MarkupIterator { + private final SegmentIterator mySegmentIterator; + private final RangeIterator myRangeIterator; + private int myCurrentFontStyle; + private Color myCurrentForegroundColor; + private Color myCurrentBackgroundColor; - @Nullable public final Color foreground; - @Nullable public final Color background; - @NotNull public final String fontFamilyName; + private MarkupIterator(@NotNull CharSequence charSequence, @NotNull RangeIterator rangeIterator, @NotNull EditorColorsScheme colorsScheme) { + myRangeIterator = rangeIterator; + mySegmentIterator = new SegmentIterator(charSequence, colorsScheme.getEditorFontName(), colorsScheme.getEditorFontSize()); + } - public final int fontStyle; - public final int startOffset; - public final int endOffset; + public boolean atEnd() { + return myRangeIterator.atEnd() && mySegmentIterator.atEnd(); + } - SegmentInfo(@Nullable Color foreground, - @Nullable Color background, - @NotNull String fontFamilyName, - int fontStyle, - int startOffset, - int endOffset) - { - this.foreground = foreground; - this.background = background; - this.fontFamilyName = fontFamilyName; - this.fontStyle = fontStyle; - this.startOffset = startOffset; - this.endOffset = endOffset; + public void advance() { + if (mySegmentIterator.atEnd()) { + myRangeIterator.advance(); + TextAttributes textAttributes = myRangeIterator.getTextAttributes(); + myCurrentFontStyle = textAttributes == null ? Font.PLAIN : textAttributes.getFontType(); + myCurrentForegroundColor = textAttributes == null ? null : textAttributes.getForegroundColor(); + myCurrentBackgroundColor = textAttributes == null ? null : textAttributes.getBackgroundColor(); + mySegmentIterator.reset(myRangeIterator.getRangeStart(), myRangeIterator.getRangeEnd(), myCurrentFontStyle); + } + mySegmentIterator.advance(); + } + + public int getStartOffset() { + return mySegmentIterator.getCurrentStartOffset(); + } + + public int getEndOffset() { + return mySegmentIterator.getCurrentEndOffset(); + } + + public int getFontStyle() { + return myCurrentFontStyle; } @NotNull - public static List produce(@NotNull TextAttributes attribute, - @NotNull CharSequence text, - @NotNull EditorColorsScheme colorsScheme, - int start, - int end) - { - if (end <= start) { - return Collections.emptyList(); - } - List result = ContainerUtilRt.newArrayList(); - int currentStart = start; - int fontSize = colorsScheme.getEditorFontSize(); - int fontStyle = attribute.getFontType(); - String defaultFontFamily = colorsScheme.getEditorFontName(); - Font font = ComplementaryFontsRegistry.getFontAbleToDisplay(text.charAt(start), fontSize, fontStyle, defaultFontFamily).getFont(); - String currentFontFamilyName = font.getFamily(); - String candidateFontFamilyName; - for (int i = start + 1; i < end; i++) { - font = ComplementaryFontsRegistry.getFontAbleToDisplay(text.charAt(i), fontSize, fontStyle, defaultFontFamily).getFont(); - candidateFontFamilyName = font.getFamily(); - if (!candidateFontFamilyName.equals(currentFontFamilyName)) { - result.add(new SegmentInfo(attribute.getForegroundColor(), - attribute.getBackgroundColor(), - currentFontFamilyName, - fontStyle, - currentStart, - i - )); - currentStart = i; - currentFontFamilyName = candidateFontFamilyName; - } - } - - if (currentStart < end) { - result.add(new SegmentInfo(attribute.getForegroundColor(), - attribute.getBackgroundColor(), - currentFontFamilyName, - fontStyle, - currentStart, - end - )); - } - - return result; + public String getFontFamilyName() { + return mySegmentIterator.getCurrentFontFamilyName(); } - @Override - public int compareTo(@NotNull SegmentInfo o) { - return startOffset - o.startOffset; + @Nullable + public Color getForegroundColor() { + return myCurrentForegroundColor; } - @Override - public int hashCode() { - int result = foreground != null ? foreground.hashCode() : 0; - result = 31 * result + (background != null ? background.hashCode() : 0); - result = 31 * result + fontFamilyName.hashCode(); - result = 31 * result + fontStyle; - result = 31 * result + startOffset; - result = 31 * result + endOffset; - return result; + @Nullable + public Color getBackgroundColor() { + return myCurrentBackgroundColor; } - @SuppressWarnings("RedundantIfStatement") - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - SegmentInfo info = (SegmentInfo)o; - - if (endOffset != info.endOffset) return false; - if (fontStyle != info.fontStyle) return false; - if (startOffset != info.startOffset) return false; - if (background != null ? !background.equals(info.background) : info.background != null) return false; - if (!fontFamilyName.equals(info.fontFamilyName)) return false; - if (foreground != null ? !foreground.equals(info.foreground) : info.foreground != null) return false; - - return true; - } - - @Override - public String toString() { - StringBuilder fontStyleAsString = new StringBuilder(); - if (fontStyle == Font.PLAIN) { - fontStyleAsString.append("plain"); - } - else { - if ((fontStyle & Font.BOLD) != 0) { - fontStyleAsString.append("bold "); - } - if ((fontStyle & Font.ITALIC) != 0) { - fontStyleAsString.append("italic "); - } - if (fontStyleAsString.length() > 0) { - fontStyleAsString.setLength(fontStyleAsString.length() - 1); - } - else { - fontStyleAsString.append("unknown font style"); - } - } - return String.format("%d-%d: %s, %s", startOffset, endOffset, fontFamilyName, fontStyleAsString); + public void dispose() { + myRangeIterator.dispose(); } } + + private static class CompositeRangeIterator implements RangeIterator { + private final @NotNull Color myDefaultForeground; + private final @NotNull Color myDefaultBackground; + private final IteratorWrapper[] myIterators; + private final TextAttributes myMergedAttributes = new TextAttributes(); + private int overlappingRangesCount; + private int myCurrentStart; + private int myCurrentEnd; + + // iterators have priority corresponding to their order in the parameter list - rightmost having the largest priority + public CompositeRangeIterator(@NotNull EditorColorsScheme colorsScheme, RangeIterator... iterators) { + myDefaultForeground = colorsScheme.getDefaultForeground(); + myDefaultBackground = colorsScheme.getDefaultBackground(); + myIterators = new IteratorWrapper[iterators.length]; + for (int i = 0; i < iterators.length; i++) { + myIterators[i] = new IteratorWrapper(iterators[i], i); + } + } + + @Override + public boolean atEnd() { + boolean validIteratorExists = false; + for (int i = 0; i < myIterators.length; i++) { + IteratorWrapper wrapper = myIterators[i]; + if (wrapper == null) { + continue; + } + RangeIterator iterator = wrapper.iterator; + if (!iterator.atEnd() || overlappingRangesCount > 0 && (i >= overlappingRangesCount || iterator.getRangeEnd() > myCurrentEnd)) { + validIteratorExists = true; + } + } + return !validIteratorExists; + } + + @Override + public void advance() { + int max = overlappingRangesCount == 0 ? myIterators.length : overlappingRangesCount; + for (int i = 0; i < max; i++) { + IteratorWrapper wrapper = myIterators[i]; + if (wrapper == null) { + continue; + } + RangeIterator iterator = wrapper.iterator; + if (overlappingRangesCount > 0 && iterator.getRangeEnd() > myCurrentEnd) { + continue; + } + if (iterator.atEnd()) { + iterator.dispose(); + myIterators[i] = null; + } + else { + iterator.advance(); + } + } + Arrays.sort(myIterators, RANGE_SORTER); + myCurrentStart = Math.max(myIterators[0].iterator.getRangeStart(), myCurrentEnd); + myCurrentEnd = Integer.MAX_VALUE; + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < myIterators.length; i++) { + IteratorWrapper wrapper = myIterators[i]; + if (wrapper == null) { + break; + } + RangeIterator iterator = wrapper.iterator; + int nearestBound; + if (iterator.getRangeStart() > myCurrentStart) { + nearestBound = iterator.getRangeStart(); + } + else { + nearestBound = iterator.getRangeEnd(); + } + myCurrentEnd = Math.min(myCurrentEnd, nearestBound); + } + myCurrentEnd = getRangeEnd(); + for (overlappingRangesCount = 1; overlappingRangesCount < myIterators.length; overlappingRangesCount++) { + IteratorWrapper wrapper = myIterators[overlappingRangesCount]; + if (wrapper == null || wrapper.iterator.getRangeStart() > myCurrentStart) { + break; + } + } + } + + private final Comparator RANGE_SORTER = new Comparator() { + @Override + public int compare(IteratorWrapper o1, IteratorWrapper o2) { + if (o1 == null) { + return 1; + } + if (o2 == null) { + return -1; + } + int startDiff = Math.max(o1.iterator.getRangeStart(), myCurrentEnd) - Math.max(o2.iterator.getRangeStart(), myCurrentEnd); + if (startDiff != 0) { + return startDiff; + } + return o2.order - o1.order; + } + }; + + @Override + public int getRangeStart() { + return myCurrentStart; + } + + @Override + public int getRangeEnd() { + return myCurrentEnd; + } + + @Override + public TextAttributes getTextAttributes() { + TextAttributes ta = myIterators[0].iterator.getTextAttributes(); + myMergedAttributes.setAttributes(ta.getForegroundColor(), ta.getBackgroundColor(), null, null, null, ta.getFontType()); + for (int i = 1; i < overlappingRangesCount; i++) { + merge(myIterators[i].iterator.getTextAttributes()); + } + return myMergedAttributes; + } + + private void merge(TextAttributes attributes) { + Color myBackground = myMergedAttributes.getBackgroundColor(); + if (myBackground == null || myDefaultBackground.equals(myBackground)) { + myMergedAttributes.setBackgroundColor(attributes.getBackgroundColor()); + } + Color myForeground = myMergedAttributes.getForegroundColor(); + if (myForeground == null || myDefaultForeground.equals(myForeground)) { + myMergedAttributes.setForegroundColor(attributes.getForegroundColor()); + } + if (myMergedAttributes.getFontType() == Font.PLAIN) { + myMergedAttributes.setFontType(attributes.getFontType()); + } + } + + @Override + public void dispose() { + for (IteratorWrapper wrapper : myIterators) { + if (wrapper != null) { + wrapper.iterator.dispose(); + } + } + } + + private static class IteratorWrapper { + private final RangeIterator iterator; + private final int order; + + private IteratorWrapper(RangeIterator iterator, int order) { + this.iterator = iterator; + this.order = order; + } + } + } + + private static class MarkupModelRangeIterator implements RangeIterator { + private final boolean myUnsupportedModel; + private final int myStartOffset; + private final int myEndOffset; + private final EditorColorsScheme myColorsScheme; + private final Color myDefaultForeground; + private final Color myDefaultBackground; + private final DisposableIterator myIterator; + + private int myCurrentStart; + private int myCurrentEnd; + private TextAttributes myCurrentAttributes; + private int myNextStart; + private int myNextEnd; + private TextAttributes myNextAttributes; + + private MarkupModelRangeIterator(@Nullable MarkupModel markupModel, + @NotNull EditorColorsScheme colorsScheme, + int startOffset, + int endOffset) { + myStartOffset = startOffset; + myEndOffset = endOffset; + myColorsScheme = colorsScheme; + myDefaultForeground = colorsScheme.getDefaultForeground(); + myDefaultBackground = colorsScheme.getDefaultBackground(); + myUnsupportedModel = !(markupModel instanceof MarkupModelEx); + if (myUnsupportedModel) { + myIterator = null; + return; + } + myIterator = ((MarkupModelEx)markupModel).overlappingIterator(startOffset, endOffset); + findNextSuitableRange(); + } + + @Override + public boolean atEnd() { + return myUnsupportedModel || myNextAttributes == null; + } + + @Override + public void advance() { + myCurrentStart = myNextStart; + myCurrentEnd = myNextEnd; + myCurrentAttributes = myNextAttributes; + findNextSuitableRange(); + } + + private void findNextSuitableRange() { + myNextAttributes = null; + while(myIterator.hasNext()) { + RangeHighlighterEx highlighter = myIterator.next(); + if (highlighter == null || !highlighter.isValid() || !isInterestedInLayer(highlighter.getLayer())) { + continue; + } + // LINES_IN_RANGE highlighters are not supported currently + myNextStart = Math.max(highlighter.getStartOffset(), myStartOffset); + myNextEnd = Math.min(highlighter.getEndOffset(), myEndOffset); + if (myNextStart >= myEndOffset) { + break; + } + if (myNextStart < myCurrentEnd) { + continue; // overlapping ranges withing document markup model are not supported currently + } + TextAttributes attributes = null; + Object tooltip = highlighter.getErrorStripeTooltip(); + if (tooltip instanceof HighlightInfo) { + HighlightInfo info = (HighlightInfo)tooltip; + TextAttributesKey key = info.forcedTextAttributesKey; + if (key == null) { + HighlightInfoType type = info.type; + key = type.getAttributesKey(); + } + if (key != null) { + attributes = myColorsScheme.getAttributes(key); + } + } + if (attributes == null) { + continue; + } + Color foreground = attributes.getForegroundColor(); + Color background = attributes.getBackgroundColor(); + if ((foreground == null || myDefaultForeground.equals(foreground)) + && (background == null || myDefaultBackground.equals(background)) + && attributes.getFontType() == Font.PLAIN) { + continue; + } + myNextAttributes = attributes; + break; + } + } + + private static boolean isInterestedInLayer(int layer) { + return layer != HighlighterLayer.CARET_ROW + && layer != HighlighterLayer.SELECTION + && layer != HighlighterLayer.ERROR + && layer != HighlighterLayer.WARNING; + } + + @Override + public int getRangeStart() { + return myCurrentStart; + } + + @Override + public int getRangeEnd() { + return myCurrentEnd; + } + + @Override + public TextAttributes getTextAttributes() { + return myCurrentAttributes; + } + + @Override + public void dispose() { + if (myIterator != null) { + myIterator.dispose(); + } + } + } + + private static class HighlighterRangeIterator implements RangeIterator { + private final HighlighterIterator myIterator; + private final int myStartOffset; + private final int myEndOffset; + + private int myCurrentStart; + private int myCurrentEnd; + private TextAttributes myCurrentAttributes; + + public HighlighterRangeIterator(@NotNull EditorHighlighter highlighter, int startOffset, int endOffset) { + myStartOffset = startOffset; + myEndOffset = endOffset; + myIterator = highlighter.createIterator(startOffset); + skipBadCharacters(); + } + + @Override + public boolean atEnd() { + return myIterator.atEnd() || getCurrentStart() >= myEndOffset; + } + + private int getCurrentStart() { + return Math.max(myIterator.getStart(), myStartOffset); + } + + private int getCurrentEnd() { + return Math.min(myIterator.getEnd(), myEndOffset); + } + + private void skipBadCharacters() { + while (!myIterator.atEnd() && myIterator.getTokenType() == TokenType.BAD_CHARACTER) { + myIterator.advance(); + } + } + + @Override + public void advance() { + myCurrentStart = getCurrentStart(); + myCurrentEnd = getCurrentEnd(); + myCurrentAttributes = myIterator.getTextAttributes(); + myIterator.advance(); + skipBadCharacters(); + } + + @Override + public int getRangeStart() { + return myCurrentStart; + } + + @Override + public int getRangeEnd() { + return myCurrentEnd; + } + + @Override + public TextAttributes getTextAttributes() { + return myCurrentAttributes; + } + + @Override + public void dispose() { + } + } + + private interface RangeIterator { + boolean atEnd(); + void advance(); + int getRangeStart(); + int getRangeEnd(); + TextAttributes getTextAttributes(); + void dispose(); + } + + private static class SegmentIterator { + private final CharSequence myCharSequence; + private final String myDefaultFontFamilyName; + private final int myFontSize; + + private int myCurrentStartOffset; + private int myCurrentOffset; + private int myEndOffset; + private int myFontStyle; + private String myCurrentFontFamilyName; + private String myNextFontFamilyName; + + private SegmentIterator(CharSequence charSequence, String defaultFontFamilyName, int fontSize) { + myCharSequence = charSequence; + myDefaultFontFamilyName = defaultFontFamilyName; + myFontSize = fontSize; + } + + public void reset(int startOffset, int endOffset, int fontStyle) { + myCurrentOffset = startOffset; + myEndOffset = endOffset; + myFontStyle = fontStyle; + } + + public boolean atEnd() { + return myCurrentOffset >= myEndOffset; + } + + public void advance() { + myCurrentFontFamilyName = myNextFontFamilyName; + myCurrentStartOffset = myCurrentOffset; + for (; myCurrentOffset < myEndOffset; myCurrentOffset++) { + FontInfo fontInfo = ComplementaryFontsRegistry.getFontAbleToDisplay(myCharSequence.charAt(myCurrentOffset), + myFontSize, + myFontStyle, + myDefaultFontFamilyName); + String fontFamilyName = fontInfo.getFont().getFamily(); + + if (myCurrentFontFamilyName == null) { + myCurrentFontFamilyName = fontFamilyName; + } + else if (!myCurrentFontFamilyName.equals(fontFamilyName)) { + myNextFontFamilyName = fontFamilyName; + break; + } + } + } + + public int getCurrentStartOffset() { + return myCurrentStartOffset; + } + + public int getCurrentEndOffset() { + return myCurrentOffset; + } + + public String getCurrentFontFamilyName() { + return myCurrentFontFamilyName; + } + } + + } diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractRegistry.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractRegistry.java index 23c0c01cdb09..0435235fdbb9 100644 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractRegistry.java +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractRegistry.java @@ -19,14 +19,13 @@ import gnu.trove.TIntObjectHashMap; import gnu.trove.TObjectIntHashMap; import org.jetbrains.annotations.NotNull; -import java.io.Serializable; import java.util.Arrays; /** * @author Denis Zhdanov * @since 3/23/13 3:17 PM */ -public abstract class AbstractRegistry implements Serializable { +public abstract class AbstractRegistry { @NotNull private final TIntObjectHashMap myDataById = new TIntObjectHashMap(); diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/MarkupHandler.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/MarkupHandler.java new file mode 100644 index 000000000000..cdd01c48cf21 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/MarkupHandler.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.model; + +public interface MarkupHandler { + void handleText(int startOffset, int endOffset) throws Exception; + void handleForeground(int foregroundId) throws Exception; + void handleBackground(int backgroundId) throws Exception; + void handleFont(int fontNameId) throws Exception; + void handleStyle(int style) throws Exception; +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfoSerializer.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfoSerializer.java new file mode 100644 index 000000000000..75ae37604c2c --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfoSerializer.java @@ -0,0 +1,123 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.model; + +import com.intellij.util.io.CompactDataInput; +import com.intellij.util.io.CompactDataOutput; +import org.iq80.snappy.SnappyInputStream; +import org.iq80.snappy.SnappyOutputStream; + +import java.io.IOException; + +/** + * Not synchronized, stream implementations must be used from one thread at a time only + */ +public class OutputInfoSerializer { + private static final int TEXT_ID = 0; + private static final int STYLE_ID = 1; + private static final int FOREGROUND_ID = 2; + private static final int BACKGROUND_ID = 3; + private static final int FONT_ID = 4; + + public static class OutputStream implements MarkupHandler { + private final CompactDataOutput myOutputStream; + private final java.io.OutputStream myUnderlyingOutputStream; + private int myCurrentOffset; + + public OutputStream(java.io.OutputStream stream) throws IOException { + myUnderlyingOutputStream = new SnappyOutputStream(stream); + myOutputStream = new CompactDataOutput(myUnderlyingOutputStream); + } + + public void close() throws IOException { + myUnderlyingOutputStream.close(); + } + + @Override + public void handleText(int startOffset, int endOffset) throws IOException { + myOutputStream.write(TEXT_ID); + myOutputStream.writeInt(startOffset - myCurrentOffset); + myOutputStream.writeInt(endOffset - startOffset); + myCurrentOffset = endOffset; + } + + @Override + public void handleForeground(int foregroundId) throws IOException { + myOutputStream.write(FOREGROUND_ID); + myOutputStream.writeInt(foregroundId); + } + + @Override + public void handleBackground(int backgroundId) throws IOException { + myOutputStream.write(BACKGROUND_ID); + myOutputStream.writeInt(backgroundId); + } + + @Override + public void handleFont(int fontNameId) throws IOException { + myOutputStream.write(FONT_ID); + myOutputStream.writeInt(fontNameId); + } + + @Override + public void handleStyle(int style) throws IOException { + myOutputStream.write(STYLE_ID); + myOutputStream.writeInt(style); + } + } + + public static class InputStream { + private final CompactDataInput myInputStream; + private final java.io.InputStream myUnderlyingInputStream; + private int myCurrentOffset; + + public InputStream(java.io.InputStream stream) throws IOException { + myUnderlyingInputStream = new SnappyInputStream(stream); + myInputStream = new CompactDataInput(myUnderlyingInputStream); + } + + public void read(MarkupHandler handler) throws Exception { + int id = myInputStream.readByte(); + switch (id) { + case TEXT_ID: + int startOffset = myCurrentOffset + myInputStream.readInt(); + myCurrentOffset = startOffset; + int endOffset = myCurrentOffset + myInputStream.readInt(); + myCurrentOffset = endOffset; + handler.handleText(startOffset, endOffset); + break; + case STYLE_ID: + handler.handleStyle(myInputStream.readInt()); + break; + case FOREGROUND_ID: + handler.handleForeground(myInputStream.readInt()); + break; + case BACKGROUND_ID: + handler.handleBackground(myInputStream.readInt()); + break; + case FONT_ID: + handler.handleFont(myInputStream.readInt()); + break; + default: + throw new IllegalStateException("Unknown tag id: " + id); + } + } + + public void close() throws IOException { + myUnderlyingInputStream.close(); + } + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/SyntaxInfo.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/SyntaxInfo.java new file mode 100644 index 000000000000..7bd88c526125 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/SyntaxInfo.java @@ -0,0 +1,252 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.model; + +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author Denis Zhdanov + * @since 3/25/13 1:19 PM + */ +public class SyntaxInfo { + private final int myOutputInfoCount; + private final byte[] myOutputInfosSerialized; + @NotNull private final ColorRegistry myColorRegistry; + @NotNull private final FontNameRegistry myFontNameRegistry; + + private final int myDefaultForeground; + private final int myDefaultBackground; + private final int mySingleFontSize; + + private SyntaxInfo(int outputInfoCount, + byte[] outputInfosSerialized, + int defaultForeground, + int defaultBackground, + int singleFontSize, + @NotNull FontNameRegistry fontNameRegistry, + @NotNull ColorRegistry colorRegistry) + { + myOutputInfoCount = outputInfoCount; + myOutputInfosSerialized = outputInfosSerialized; + myDefaultForeground = defaultForeground; + myDefaultBackground = defaultBackground; + mySingleFontSize = singleFontSize; + myFontNameRegistry = fontNameRegistry; + myColorRegistry = colorRegistry; + } + + @NotNull + public ColorRegistry getColorRegistry() { + return myColorRegistry; + } + + @NotNull + public FontNameRegistry getFontNameRegistry() { + return myFontNameRegistry; + } + + public int getDefaultForeground() { + return myDefaultForeground; + } + + public int getDefaultBackground() { + return myDefaultBackground; + } + + /** + * @return positive value if all tokens have the same font size (returned value); + * non-positive value otherwise + */ + public int getSingleFontSize() { + return mySingleFontSize; + } + + @Override + public String toString() { + final StringBuilder b = new StringBuilder(); + b.append("default colors: foreground=").append(myDefaultForeground).append(", background=").append(myDefaultBackground).append("; output infos: "); + boolean first = true; + MarkupIterator it = new MarkupIterator(); + try { + while(it.hasNext()) { + if (first) { + b.append(','); + } + it.processNext(new MarkupHandler() { + @Override + public void handleText(int startOffset, int endOffset) throws Exception { + b.append("text(").append(startOffset).append(",").append(endOffset).append(")"); + } + + @Override + public void handleForeground(int foregroundId) throws Exception { + b.append("foreground(").append(foregroundId).append(")"); + } + + @Override + public void handleBackground(int backgroundId) throws Exception { + b.append("background(").append(backgroundId).append(")"); + } + + @Override + public void handleFont(int fontNameId) throws Exception { + b.append("font(").append(fontNameId).append(")"); + } + + @Override + public void handleStyle(int style) throws Exception { + b.append("style(").append(style).append(")"); + } + }); + first = false; + } + return b.toString(); + } + finally { + it.dispose(); + } + } + + public static class Builder { + private final ColorRegistry myColorRegistry = new ColorRegistry(); + private final FontNameRegistry myFontNameRegistry = new FontNameRegistry(); + private final int myDefaultForeground; + private final int myDefaultBackground; + private final int myFontSize; + private final ByteArrayOutputStream myStream = new ByteArrayOutputStream(); + private final OutputInfoSerializer.OutputStream myOutputInfoStream; + private int myOutputInfoCount; + + public Builder(Color defaultForeground, Color defaultBackground, int fontSize) { + myDefaultForeground = myColorRegistry.getId(defaultForeground); + myDefaultBackground = myColorRegistry.getId(defaultBackground); + myFontSize = fontSize; + try { + myOutputInfoStream = new OutputInfoSerializer.OutputStream(myStream); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void addFontStyle(int fontStyle) { + try { + myOutputInfoStream.handleStyle(fontStyle); + } + catch (IOException e) { + throw new RuntimeException(e); + } + myOutputInfoCount++; + } + + public void addFontFamilyName(String fontFamilyName) { + try { + myOutputInfoStream.handleFont(myFontNameRegistry.getId(fontFamilyName)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + myOutputInfoCount++; + } + + public void addForeground(Color foreground) { + try { + myOutputInfoStream.handleForeground(myColorRegistry.getId(foreground)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + myOutputInfoCount++; + } + + public void addBackground(Color background) { + try { + myOutputInfoStream.handleBackground(myColorRegistry.getId(background)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + myOutputInfoCount++; + } + + public void addText(int startOffset, int endOffset) { + try { + myOutputInfoStream.handleText(startOffset, endOffset); + } + catch (IOException e) { + throw new RuntimeException(e); + } + myOutputInfoCount++; + } + + public SyntaxInfo build() { + myColorRegistry.seal(); + myFontNameRegistry.seal(); + try { + myOutputInfoStream.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + return new SyntaxInfo(myOutputInfoCount, myStream.toByteArray(), myDefaultForeground, myDefaultBackground, myFontSize, myFontNameRegistry, myColorRegistry); + } + } + + public class MarkupIterator { + private int pos; + private final OutputInfoSerializer.InputStream myOutputInfoStream; + + public MarkupIterator() { + try { + myOutputInfoStream = new OutputInfoSerializer.InputStream(new ByteArrayInputStream(myOutputInfosSerialized)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + public boolean hasNext() { + return pos < myOutputInfoCount; + } + + public void processNext(MarkupHandler handler) { + if (!hasNext()) { + throw new IllegalStateException(); + } + pos++; + try { + myOutputInfoStream.read(handler); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void dispose() { + try { + myOutputInfoStream.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/AbstractSyntaxAwareInputStreamTransferableData.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/AbstractSyntaxAwareInputStreamTransferableData.java new file mode 100644 index 000000000000..bb15a20912ba --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/AbstractSyntaxAwareInputStreamTransferableData.java @@ -0,0 +1,130 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.view; + +import com.intellij.codeInsight.editorActions.TextBlockTransferableData; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.util.StringBuilderSpinAllocator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.datatransfer.DataFlavor; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +/** + * @author Denis Zhdanov + * @since 3/28/13 1:20 PM + */ +public abstract class AbstractSyntaxAwareInputStreamTransferableData extends InputStream implements TextBlockTransferableData, RawTextHolder +{ + + private static final Logger LOG = Logger.getInstance("#" + AbstractSyntaxAwareInputStreamTransferableData.class.getName()); + + protected String myRawText; + @NotNull + protected final SyntaxInfo mySyntaxInfo; + @NotNull + private final DataFlavor myDataFlavor; + + @Nullable private transient InputStream myDelegate; + + public AbstractSyntaxAwareInputStreamTransferableData(@NotNull SyntaxInfo syntaxInfo, @NotNull DataFlavor flavor) { + mySyntaxInfo = syntaxInfo; + myDataFlavor = flavor; + } + + @Override + public DataFlavor getFlavor() { + return myDataFlavor; + } + + @Override + public int getOffsetCount() { + return 0; + } + + @Override + public int getOffsets(int[] offsets, int index) { + return index; + } + + @Override + public int setOffsets(int[] offsets, int index) { + return index; + } + + @Override + public int read() throws IOException { + return getDelegate().read(); + } + + @Override + public int read(@NotNull byte[] b, int off, int len) throws IOException { + return getDelegate().read(b, off, len); + } + + @Override + public void close() throws IOException { + myDelegate = null; + } + + @Override + public void setRawText(String rawText) { + myRawText = rawText; + } + + @NotNull + private InputStream getDelegate() { + if (myDelegate != null) { + return myDelegate; + } + + int maxLength = Registry.intValue("editor.richcopy.max.size.megabytes") * 1048576; + final StringBuilder buffer = StringBuilderSpinAllocator.alloc(); + try { + try { + build(buffer, maxLength); + } + catch (Exception e) { + LOG.error(e); + } + String s = buffer.toString(); + if (Registry.is("editor.richcopy.debug")) { + LOG.info("Resulting text: \n" + s); + } + try { + myDelegate = new ByteArrayInputStream(s.getBytes(getCharset())); + } + catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return myDelegate; + } + finally { + StringBuilderSpinAllocator.dispose(buffer); + } + } + + protected abstract void build(@NotNull StringBuilder holder, int maxLength); + + @NotNull + protected abstract String getCharset(); +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/AbstractSyntaxAwareReaderTransferableData.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/AbstractSyntaxAwareReaderTransferableData.java new file mode 100644 index 000000000000..b4de6e59517e --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/AbstractSyntaxAwareReaderTransferableData.java @@ -0,0 +1,121 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.view; + +import com.intellij.codeInsight.editorActions.TextBlockTransferableData; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.util.StringBuilderSpinAllocator; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.datatransfer.DataFlavor; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +/** + * @author Denis Zhdanov + * @since 3/28/13 7:09 PM + */ +public abstract class AbstractSyntaxAwareReaderTransferableData extends Reader implements TextBlockTransferableData, RawTextHolder +{ + + private static final Logger LOG = Logger.getInstance("#" + AbstractSyntaxAwareReaderTransferableData.class.getName()); + + protected String myRawText; + @NotNull + protected final SyntaxInfo mySyntaxInfo; + @NotNull + private final DataFlavor myDataFlavor; + + @Nullable private transient Reader myDelegate; + + public AbstractSyntaxAwareReaderTransferableData(@NotNull SyntaxInfo syntaxInfo, @NotNull DataFlavor dataFlavor) { + mySyntaxInfo = syntaxInfo; + myDataFlavor = dataFlavor; + } + + @Override + public DataFlavor getFlavor() { + return myDataFlavor; + } + + @Override + public int getOffsetCount() { + return 0; + } + + @Override + public int getOffsets(int[] offsets, int index) { + return index; + } + + @Override + public int setOffsets(int[] offsets, int index) { + return index; + } + + @Override + public int read() throws IOException { + return getDelegate().read(); + } + + @Override + public int read(@NotNull char[] cbuf, int off, int len) throws IOException { + return getDelegate().read(cbuf, off, len); + } + + @Override + public void close() throws IOException { + myDelegate = null; + } + + @Override + public void setRawText(String rawText) { + myRawText = rawText; + } + + @NotNull + private Reader getDelegate() { + if (myDelegate != null) { + return myDelegate; + } + + int maxLength = Registry.intValue("editor.richcopy.max.size.megabytes") * 1048576; + final StringBuilder buffer = StringBuilderSpinAllocator.alloc(); + try { + try { + build(buffer, maxLength); + } + catch (Exception e) { + LOG.error(e); + } + String s = buffer.toString(); + if (Registry.is("editor.richcopy.debug")) { + LOG.info("Resulting text: \n" + s); + } + myDelegate = new StringReader(s); + return myDelegate; + } + finally { + StringBuilderSpinAllocator.dispose(buffer); + } + } + + protected abstract void build(@NotNull StringBuilder holder, int maxLength); +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/HtmlTransferableData.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/HtmlTransferableData.java new file mode 100644 index 000000000000..0d64830b5041 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/HtmlTransferableData.java @@ -0,0 +1,224 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.view; + +import com.intellij.openapi.editor.richcopy.model.ColorRegistry; +import com.intellij.openapi.editor.richcopy.model.FontNameRegistry; +import com.intellij.openapi.editor.richcopy.model.MarkupHandler; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; +import com.intellij.util.StringBuilderSpinAllocator; +import com.intellij.util.ui.UIUtil; +import gnu.trove.TIntObjectHashMap; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.awt.datatransfer.DataFlavor; + +/** + * @author Denis Zhdanov + * @since 3/28/13 1:06 PM + */ +public class HtmlTransferableData extends AbstractSyntaxAwareReaderTransferableData implements MarkupHandler { + + @NotNull public static final DataFlavor FLAVOR = new DataFlavor("text/html;class=java.io.Reader", "HTML text"); + + private StringBuilder myResultBuffer; + private ColorRegistry myColorRegistry; + private FontNameRegistry myFontNameRegistry; + + private int myForeground; + private int myBackground; + private int myFontFamily; + private boolean myBold; + private boolean myItalic; + + private final TIntObjectHashMap myColors = new TIntObjectHashMap(); + + public HtmlTransferableData(@NotNull SyntaxInfo syntaxInfo) { + super(syntaxInfo, FLAVOR); + } + + @Override + protected void build(@NotNull StringBuilder holder, int maxLength) { + myResultBuffer = holder; + myColorRegistry = mySyntaxInfo.getColorRegistry(); + myFontNameRegistry = mySyntaxInfo.getFontNameRegistry(); + try { + buildColorMap(); + myResultBuffer.append("
") + .append("
");
+
+      SyntaxInfo.MarkupIterator it = mySyntaxInfo.new MarkupIterator();
+      try {
+        while(it.hasNext()) {
+          it.processNext(this);
+          if (myResultBuffer.length() > maxLength) {
+            myResultBuffer.append("... truncated ...");
+            break;
+          }
+        }
+      }
+      finally {
+        it.dispose();
+      }
+      myResultBuffer.append("
"); + } + finally { + myResultBuffer = null; + myRawText = null; + myColorRegistry = null; + myFontNameRegistry = null; + myColors.clear(); + } + } + + private void defineForeground(int id, @NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { + myResultBuffer.append(""); + styleBuffer.append("color:"); + appendColor(styleBuffer, id); + styleBuffer.append(";"); + closeTagBuffer.insert(0, ""); + } + + private void defineBackground(int id, @NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { + myResultBuffer.append(""); + styleBuffer.append("background-color:"); + appendColor(styleBuffer, id); + styleBuffer.append(";"); + closeTagBuffer.insert(0, ""); + } + + private void defineBold(@NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { + myResultBuffer.append(""); + styleBuffer.append("font-weight:bold;"); + closeTagBuffer.insert(0, ""); + } + + private void defineItalic(@NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) { + myResultBuffer.append(""); + styleBuffer.append("font-style:italic;"); + closeTagBuffer.insert(0, ""); + } + + private void appendColor(StringBuilder builder, int id) { + builder.append(myColors.get(id)); + } + + private void buildColorMap() { + for (int id : myColorRegistry.getAllIds()) { + StringBuilder b = new StringBuilder("#"); + UIUtil.appendColor(myColorRegistry.dataById(id), b); + myColors.put(id, b.toString()); + } + } + + private void appendFontFamilyRule(@NotNull StringBuilder styleBuffer, int fontFamilyId) { + styleBuffer.append("font-family:'").append(myFontNameRegistry.dataById(fontFamilyId)).append("';"); + } + + private static void appendFontSizeRule(@NotNull StringBuilder styleBuffer, int fontSize) { + styleBuffer.append("font-size:").append(fontSize).append("pt;"); + } + + @Override + public void handleText(int startOffset, int endOffset) { + boolean formattedText = myForeground > 0 || myBackground > 0 || myFontFamily > 0 || myBold || myItalic; + if (!formattedText) { + escapeAndAdd(startOffset, endOffset); + return; + } + + StringBuilder styleBuffer = StringBuilderSpinAllocator.alloc(); + StringBuilder closeTagBuffer = StringBuilderSpinAllocator.alloc(); + try { + if (myForeground > 0) { + defineForeground(myForeground, styleBuffer, closeTagBuffer); + } + if (myBackground > 0) { + defineBackground(myBackground, styleBuffer, closeTagBuffer); + } + if (myBold) { + defineBold(styleBuffer, closeTagBuffer); + } + if (myItalic) { + defineItalic(styleBuffer, closeTagBuffer); + } + if (myFontFamily > 0) { + appendFontFamilyRule(styleBuffer, myFontFamily); + } + myResultBuffer.append(""); + escapeAndAdd(startOffset, endOffset); + myResultBuffer.append(""); + myResultBuffer.append(closeTagBuffer); + } + finally { + StringBuilderSpinAllocator.dispose(styleBuffer); + StringBuilderSpinAllocator.dispose(closeTagBuffer); + } + } + + private void escapeAndAdd(int start, int end) { + for (int i = start; i < end; i++) { + char c = myRawText.charAt(i); + switch (c) { + case '<': myResultBuffer.append("<"); break; + case '>': myResultBuffer.append(">"); break; + case '&': myResultBuffer.append("&"); break; + default: myResultBuffer.append(c); + } + } + } + + @Override + public void handleForeground(int foregroundId) throws Exception { + myForeground = foregroundId; + } + + @Override + public void handleBackground(int backgroundId) throws Exception { + myBackground = backgroundId; + } + + @Override + public void handleFont(int fontNameId) throws Exception { + if (myFontNameRegistry != null) { + myFontFamily = fontNameId; + } + } + + @Override + public void handleStyle(int style) throws Exception { + myBold = (Font.BOLD & style) != 0; + myItalic = (Font.ITALIC & style) != 0; + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/InputStreamTransferableData.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/InputStreamTransferableData.java deleted file mode 100644 index 29ee172216c0..000000000000 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/InputStreamTransferableData.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.view; - -import com.intellij.codeInsight.editorActions.TextBlockTransferableData; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.awt.datatransfer.DataFlavor; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * @author Denis Zhdanov - * @since 3/28/13 1:20 PM - */ -public class InputStreamTransferableData extends InputStream implements TextBlockTransferableData -{ - @NotNull - private final DataFlavor myDataFlavor; - @Nullable - private InputStream myDelegate; - - public InputStreamTransferableData(@NotNull byte[] data, @NotNull DataFlavor flavor) { - myDataFlavor = flavor; - myDelegate = new ByteArrayInputStream(data); - } - - @Override - public DataFlavor getFlavor() { - return myDelegate == null ? null : myDataFlavor; - } - - @Override - public int getOffsetCount() { - return 0; - } - - @Override - public int getOffsets(int[] offsets, int index) { - return index; - } - - @Override - public int setOffsets(int[] offsets, int index) { - return index; - } - - @Override - public int read() throws IOException { - if (myDelegate == null) { - return -1; - } - return myDelegate.read(); - } - - @Override - public int read(@NotNull byte[] b, int off, int len) throws IOException { - if (myDelegate == null) { - return -1; - } - return myDelegate.read(b, off, len); - } - - @Override - public void close() throws IOException { - myDelegate = null; - } -} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/RawTextHolder.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/RawTextHolder.java new file mode 100644 index 000000000000..f24d780d26e9 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/RawTextHolder.java @@ -0,0 +1,20 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.view; + +public interface RawTextHolder { + void setRawText(String rawText); +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/ReaderTransferableData.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/ReaderTransferableData.java deleted file mode 100644 index 6deea2c02671..000000000000 --- a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/ReaderTransferableData.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.view; - -import com.intellij.codeInsight.editorActions.TextBlockTransferableData; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.awt.datatransfer.DataFlavor; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; - -/** - * @author Denis Zhdanov - * @since 3/28/13 7:09 PM - */ -public class ReaderTransferableData extends Reader implements TextBlockTransferableData -{ - @NotNull - private final DataFlavor myDataFlavor; - @Nullable - private Reader myDelegate; - - @SuppressWarnings("AbstractMethodCallInConstructor") - public ReaderTransferableData(@NotNull String data, @NotNull DataFlavor dataFlavor) { - myDataFlavor = dataFlavor; - myDelegate = new StringReader(data); - } - - @Override - public DataFlavor getFlavor() { - return myDelegate == null ? null : myDataFlavor; - } - - @Override - public int getOffsetCount() { - return 0; - } - - @Override - public int getOffsets(int[] offsets, int index) { - return index; - } - - @Override - public int setOffsets(int[] offsets, int index) { - return index; - } - - @Override - public int read() throws IOException { - if (myDelegate == null) { - return -1; - } - return myDelegate.read(); - } - - @Override - public int read(@NotNull char[] cbuf, int off, int len) throws IOException { - if (myDelegate == null) { - return -1; - } - return myDelegate.read(cbuf, off, len); - } - - @Override - public void close() throws IOException { - myDelegate = null; - } -} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/RtfTransferableData.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/RtfTransferableData.java new file mode 100644 index 000000000000..996e1c130054 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/RtfTransferableData.java @@ -0,0 +1,220 @@ +/* + * Copyright 2000-2014 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 com.intellij.openapi.editor.richcopy.view; + +import com.intellij.openapi.editor.richcopy.model.ColorRegistry; +import com.intellij.openapi.editor.richcopy.model.FontNameRegistry; +import com.intellij.openapi.editor.richcopy.model.MarkupHandler; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.awt.datatransfer.DataFlavor; + +public class RtfTransferableData extends AbstractSyntaxAwareInputStreamTransferableData { + + @NotNull public static final DataFlavor FLAVOR = new DataFlavor("text/rtf;class=java.io.InputStream", "RTF text"); + + @NotNull private static final String HEADER_PREFIX = "{\\rtf1\\ansi\\deff0"; + @NotNull private static final String HEADER_SUFFIX = "}"; + @NotNull private static final String TAB = "\\tab\n"; + @NotNull private static final String NEW_LINE = "\\line\n"; + @NotNull private static final String BOLD = "\\b"; + @NotNull private static final String ITALIC = "\\i"; + @NotNull private static final String PLAIN = "\\plain\n"; + + public RtfTransferableData(@NotNull SyntaxInfo syntaxInfo) { + super(syntaxInfo, FLAVOR); + } + + @Override + protected void build(@NotNull final StringBuilder holder, final int maxLength) { + header(mySyntaxInfo, holder, new Runnable() { + @Override + public void run() { + rectangularBackground(mySyntaxInfo, holder, new Runnable() { + @Override + public void run() { + content(mySyntaxInfo, holder, myRawText, maxLength); + } + }); + } + }); + } + + @NotNull + @Override + protected String getCharset() { + return "US-ASCII"; + } + + private static void header(@NotNull SyntaxInfo syntaxInfo, @NotNull StringBuilder buffer, @NotNull Runnable next) { + buffer.append(HEADER_PREFIX); + + // Color table. + buffer.append("{\\colortbl;"); + ColorRegistry colorRegistry = syntaxInfo.getColorRegistry(); + for (int id : colorRegistry.getAllIds()) { + Color color = colorRegistry.dataById(id); + buffer.append(String.format("\\red%d\\green%d\\blue%d;", color.getRed(), color.getGreen(), color.getBlue())); + } + buffer.append("}\n"); + + // Font table. + buffer.append("{\\fonttbl"); + FontNameRegistry fontNameRegistry = syntaxInfo.getFontNameRegistry(); + for (int id : fontNameRegistry.getAllIds()) { + String fontName = fontNameRegistry.dataById(id); + buffer.append(String.format("{\\f%d %s;}", id, fontName)); + } + buffer.append("}\n"); + + next.run(); + buffer.append(HEADER_SUFFIX); + } + + private static void rectangularBackground(@NotNull SyntaxInfo syntaxInfo, @NotNull StringBuilder buffer, @NotNull Runnable next) { + buffer.append("\n\\s0\\box\\brdrhair\\brdrcf").append(syntaxInfo.getDefaultForeground()).append("\\brsp317").append("\\cbpat") + .append(syntaxInfo.getDefaultBackground()); + saveBackground(buffer, syntaxInfo.getDefaultBackground()); + next.run(); + buffer.append("\\par"); + } + + private static void content(@NotNull SyntaxInfo syntaxInfo, @NotNull StringBuilder buffer, @NotNull String rawText, int maxLength) { + MyVisitor visitor = new MyVisitor(buffer, rawText, syntaxInfo.getSingleFontSize()); + SyntaxInfo.MarkupIterator it = syntaxInfo.new MarkupIterator(); + try { + while(it.hasNext()) { + it.processNext(visitor); + if (buffer.length() > maxLength) { + buffer.append("... truncated ..."); + break; + } + } + } + finally { + it.dispose(); + } + } + + private static void saveBackground(@NotNull StringBuilder buffer, int id) { + buffer.append("\\cb").append(id); + } + + private static void saveForeground(@NotNull StringBuilder buffer, int id) { + buffer.append("\\cf").append(id); + } + + private static void saveFontName(@NotNull StringBuilder buffer, int id) { + buffer.append("\\f").append(id); + } + + private static void saveFontSize(@NotNull StringBuilder buffer, int size) { + buffer.append("\\fs").append(size * 2); + } + + private static class MyVisitor implements MarkupHandler { + + @NotNull private final StringBuilder myBuffer; + @NotNull private final String myRawText; + + private int myBackgroundId = -1; + private int myForegroundId = -1; + private int myFontNameId = -1; + private int myFontStyle = -1; + private int myFontSize = -1; + + MyVisitor(@NotNull StringBuilder buffer, @NotNull String rawText, int fontSize) { + myBuffer = buffer; + myRawText = rawText; + myFontSize = fontSize; + } + + @Override + public void handleText(int startOffset, int endOffset) throws Exception { + myBuffer.append("\n"); + for (int i = startOffset; i < endOffset; i++) { + char c = myRawText.charAt(i); + if (c > 127) { + // Escape non-ascii symbols. + myBuffer.append(String.format("\\u%04d?", (int)c)); + continue; + } + + switch (c) { + case '\t': + myBuffer.append(TAB); + continue; + case '\n': + myBuffer.append(NEW_LINE); + continue; + case '\\': + case '{': + case '}': + myBuffer.append('\\'); + } + myBuffer.append(c); + } + } + + @Override + public void handleForeground(int foregroundId) throws Exception { + saveForeground(myBuffer, foregroundId); + myForegroundId = foregroundId; + } + + @Override + public void handleBackground(int backgroundId) throws Exception { + saveBackground(myBuffer, backgroundId); + myBackgroundId = backgroundId; + } + + @Override + public void handleFont(int fontNameId) throws Exception { + saveFontName(myBuffer, fontNameId); + myFontNameId = fontNameId; + } + + @Override + public void handleStyle(int style) throws Exception { + // Reset formatting settings + myBuffer.append(PLAIN); + + // Restore target formatting settings. + if (myForegroundId >= 0) { + saveForeground(myBuffer, myForegroundId); + } + if (myBackgroundId >= 0) { + saveBackground(myBuffer, myBackgroundId); + } + if (myFontNameId >= 0) { + saveFontName(myBuffer, myFontNameId); + } + if (myFontSize > 0) { + saveFontSize(myBuffer, myFontSize); + } + + myFontStyle = style; + if ((myFontStyle & Font.ITALIC) > 0) { + myBuffer.append(ITALIC); + } + if ((myFontStyle & Font.BOLD) > 0) { + myBuffer.append(BOLD); + } + } + } +} diff --git a/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java b/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java index dea9dd8457bf..102000eefb91 100644 --- a/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java +++ b/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java @@ -154,7 +154,7 @@ public class CopyPasteManagerEx extends CopyPasteManager implements ClipboardOwn addToTheTopOfTheStack(content); } else { - moveContentToStackTop(same); + moveContentToStackTop(same, false); // notification is done in setContents() method } } catch (UnsupportedFlavorException ignore) { } @@ -283,12 +283,18 @@ public class CopyPasteManagerEx extends CopyPasteManager implements ClipboardOwn } public void moveContentToStackTop(Transferable t) { + moveContentToStackTop(t, true); + } + + private void moveContentToStackTop(Transferable t, boolean notifyOthers) { Transferable current = myData.isEmpty() ? null : myData.get(0); if (!Comparing.equal(t, current)) { myData.remove(t); myData.add(0, t); - setSystemClipboardContent(t); - fireContentChanged(current, t); + if (notifyOthers) { + setSystemClipboardContent(t); + fireContentChanged(current, t); + } } } } diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index e0066831eb66..d96b19d8b77d 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -884,6 +884,9 @@ + + + diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/RichCopyTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/RichCopyTest.java index 7dcc51625f2f..0f777d2ff333 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/RichCopyTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/RichCopyTest.java @@ -16,6 +16,8 @@ package com.intellij.openapi.editor.richcopy; import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.openapi.editor.richcopy.view.HtmlTransferableData; +import com.intellij.openapi.editor.richcopy.view.RtfTransferableData; import com.intellij.openapi.ide.CopyPasteManager; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; @@ -48,14 +50,14 @@ public class RichCopyTest extends LightPlatformCodeInsightFixtureTestCase { Transferable contents = CopyPasteManager.getInstance().getContents(); assertNotNull(contents); - assertTrue(contents.isDataFlavorSupported(HtmlCopyPasteProcessor.FLAVOR)); + assertTrue(contents.isDataFlavorSupported(HtmlTransferableData.FLAVOR)); String expectedHtml = getFileContents(getTestName(false) + ".html"); - String actualHtml = readFully((Reader)contents.getTransferData(HtmlCopyPasteProcessor.FLAVOR)); + String actualHtml = readFully((Reader)contents.getTransferData(HtmlTransferableData.FLAVOR)); assertMatches("HTML contents differs", expectedHtml, actualHtml); - assertTrue(contents.isDataFlavorSupported(RtfCopyPasteProcessor.FLAVOR)); + assertTrue(contents.isDataFlavorSupported(RtfTransferableData.FLAVOR)); String expectedRtf = getFileContents(getTestName(false) + ".rtf"); - String actualRtf = readFully((InputStream)contents.getTransferData(RtfCopyPasteProcessor.FLAVOR)); + String actualRtf = readFully((InputStream)contents.getTransferData(RtfTransferableData.FLAVOR)); assertMatches("RTF contents differs", expectedRtf, actualRtf); } diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/SyntaxInfoConstructionTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/SyntaxInfoConstructionTest.java index d6c0b309e2a1..128320c6295b 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/SyntaxInfoConstructionTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/SyntaxInfoConstructionTest.java @@ -19,12 +19,13 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.richcopy.model.ColorRegistry; +import com.intellij.openapi.editor.richcopy.model.MarkupHandler; +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; import com.intellij.ui.JBColor; import junit.framework.TestCase; -import java.awt.*; - /** * @author Denis Zhdanov * @since 3/27/13 11:11 AM @@ -235,56 +236,64 @@ public class SyntaxInfoConstructionTest extends LightPlatformCodeInsightFixtureT } private String getSyntaxInfo() { - final StringBuilder syntaxInfo = new StringBuilder(); - Editor editor = myFixture.getEditor(); + final StringBuilder builder = new StringBuilder(); + final Editor editor = myFixture.getEditor(); + final String text = editor.getSelectionModel().getSelectedText(true); + assertNotNull(text); TextWithMarkupProcessor processor = new TextWithMarkupProcessor(); processor.addBuilder(new TextWithMarkupBuilder() { @Override - public void init(Color defaultForeground, Color defaultBackground, String defaultFontFamily, int fontSize) { - assertEquals(JBColor.BLACK, defaultForeground); - assertEquals(JBColor.WHITE, defaultBackground); - assertEquals(getFontSize(), fontSize); + public void reset() { + } @Override - public boolean isOverflowed() { - return false; - } + public void build(SyntaxInfo syntaxInfo) { + final ColorRegistry colorRegistry = syntaxInfo.getColorRegistry(); + assertEquals(JBColor.BLACK, colorRegistry.dataById(syntaxInfo.getDefaultForeground())); + assertEquals(JBColor.WHITE, colorRegistry.dataById(syntaxInfo.getDefaultBackground())); + assertEquals(getFontSize(), syntaxInfo.getSingleFontSize()); + SyntaxInfo.MarkupIterator it = syntaxInfo.new MarkupIterator(); + try { + while (it.hasNext()) { + it.processNext(new MarkupHandler() { + @Override + public void handleText(int startOffset, int endOffset) throws Exception { + builder.append("text=").append(text.substring(startOffset, endOffset)).append('\n'); + } - @Override - public void setFontFamily(String fontFamily) { - } + @Override + public void handleForeground(int foregroundId) throws Exception { + builder.append("foreground=").append(colorRegistry.dataById(foregroundId)).append(','); + } - @Override - public void setFontStyle(int fontStyle) { - syntaxInfo.append("fontStyle=").append(fontStyle).append(','); - } + @Override + public void handleBackground(int backgroundId) throws Exception { + builder.append("background=").append(colorRegistry.dataById(backgroundId)).append(','); + } - @Override - public void setForeground(Color foreground) { - syntaxInfo.append("foreground=").append(foreground).append(','); - } + @Override + public void handleFont(int fontNameId) throws Exception { + assertEquals(1, fontNameId); + } - @Override - public void setBackground(Color background) { - syntaxInfo.append("background=").append(background).append(','); + @Override + public void handleStyle(int style) throws Exception { + builder.append("fontStyle=").append(style).append(','); + } + }); + } + } + finally { + it.dispose(); + } } - - @Override - public void addTextFragment(CharSequence charSequence, int startOffset, int endOffset) { - syntaxInfo.append("text=").append(charSequence.subSequence(startOffset, endOffset)).append('\n'); - } - - @Override - public void complete() { - - } - }); + }); SelectionModel selectionModel = editor.getSelectionModel(); processor.collectTransferableData(myFixture.getFile(), editor, selectionModel.getBlockSelectionStarts(), selectionModel.getBlockSelectionEnds()); - return syntaxInfo.toString(); + return builder.toString(); } private void init(String text) {