From cf0ab3d79cc0f115a72caa924bca86ffc192b75b Mon Sep 17 00:00:00 2001 From: Dmitry Batrak Date: Thu, 27 Mar 2014 18:55:53 +0400 Subject: [PATCH] IDEA-67767 Copy syntax-coloured code block to clipboard as rtf --- .../options/editor/EditorOptionsPanel.form | 37 +- .../options/editor/EditorOptionsPanel.java | 52 +- .../editorActions/TextBlockTransferable.java | 35 +- .../BaseTextWithMarkupCopyPasteProcessor.java | 95 +++ .../openapi/editor/richcopy/FontMapper.java | 73 ++ .../richcopy/HtmlCopyPasteProcessor.java | 249 ++++++ .../richcopy/RtfCopyPasteProcessor.java | 230 +++++ .../richcopy/TextWithMarkupBuilder.java | 23 + .../richcopy/TextWithMarkupProcessor.java | 803 ++++++++++++++++++ .../richcopy/model/AbstractFlyweightInfo.java | 47 + .../richcopy/model/AbstractRegistry.java | 73 ++ .../editor/richcopy/model/Background.java | 39 + .../editor/richcopy/model/ColorRegistry.java | 25 + .../editor/richcopy/model/FontFamilyName.java | 39 + .../richcopy/model/FontNameRegistry.java | 23 + .../editor/richcopy/model/FontSize.java | 60 ++ .../editor/richcopy/model/FontStyle.java | 60 ++ .../editor/richcopy/model/Foreground.java | 39 + .../editor/richcopy/model/OutputInfo.java | 29 + .../richcopy/model/OutputInfoVisitor.java | 32 + .../editor/richcopy/model/SyntaxInfo.java | 90 ++ .../openapi/editor/richcopy/model/Text.java | 76 ++ .../richcopy/settings/RichCopySettings.java | 96 +++ .../view/InputStreamTransferableData.java | 83 ++ .../richcopy/view/ReaderTransferableData.java | 84 ++ .../src/messages/ApplicationBundle.properties | 7 +- .../src/misc/registry.properties | 4 + .../src/META-INF/LangExtensions.xml | 4 + .../src/componentSets/Lang.xml | 4 + .../editor/richcopy/BlockSelection.html | 3 + .../editor/richcopy/BlockSelection.java | 7 + .../editor/richcopy/BlockSelection.rtf | 18 + .../editor/richcopy/NormalSelection.html | 8 + .../editor/richcopy/NormalSelection.java | 8 + .../editor/richcopy/NormalSelection.rtf | 35 + .../openapi/editor/richcopy/RichCopyTest.java | 76 ++ .../richcopy/SyntaxInfoConstructionTest.java | 276 ++++++ resources-en/src/search/searchableOptions.xml | 7 + 38 files changed, 2926 insertions(+), 23 deletions(-) create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/BaseTextWithMarkupCopyPasteProcessor.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/FontMapper.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/HtmlCopyPasteProcessor.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/RtfCopyPasteProcessor.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupBuilder.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupProcessor.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractFlyweightInfo.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractRegistry.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Background.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/ColorRegistry.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontFamilyName.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontNameRegistry.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontSize.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontStyle.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Foreground.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfo.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfoVisitor.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/SyntaxInfo.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Text.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/settings/RichCopySettings.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/InputStreamTransferableData.java create mode 100644 platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/ReaderTransferableData.java create mode 100644 platform/platform-tests/testData/editor/richcopy/BlockSelection.html create mode 100644 platform/platform-tests/testData/editor/richcopy/BlockSelection.java create mode 100644 platform/platform-tests/testData/editor/richcopy/BlockSelection.rtf create mode 100644 platform/platform-tests/testData/editor/richcopy/NormalSelection.html create mode 100644 platform/platform-tests/testData/editor/richcopy/NormalSelection.java create mode 100644 platform/platform-tests/testData/editor/richcopy/NormalSelection.rtf create mode 100644 platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/RichCopyTest.java create mode 100644 platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/SyntaxInfoConstructionTest.java diff --git a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form index a2f9e5817f69..2d795f480513 100644 --- a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form +++ b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.form @@ -446,11 +446,42 @@ - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java index f63fa2d624c8..dbedfc02fffa 100644 --- a/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java +++ b/platform/lang-impl/src/com/intellij/application/options/editor/EditorOptionsPanel.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2012 JetBrains s.r.o. + * 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. @@ -28,9 +28,12 @@ import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces; +import com.intellij.openapi.editor.richcopy.settings.RichCopySettings; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.TextEditor; @@ -38,8 +41,10 @@ import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.SearchableConfigurable; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.ListCellRendererWrapper; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; import org.jetbrains.annotations.NotNull; @@ -91,6 +96,10 @@ public class EditorOptionsPanel { private JBCheckBox myCbShowQuickDocOnMouseMove; private JBLabel myQuickDocDelayLabel; private JTextField myQuickDocDelayTextField; + private JComboBox myRichCopyColorSchemeComboBox; + private JCheckBox myRichCopyStripWhitespaceCheckBox; + + private static final String ACTIVE_COLOR_SCHEME = ApplicationBundle.message("combobox.richcopy.color.scheme.active"); private final ErrorHighlightingPanel myErrorHighlightingPanel = new ErrorHighlightingPanel(); private final MyConfigurable myConfigurable; @@ -113,6 +122,20 @@ public class EditorOptionsPanel { myCbRenameLocalVariablesInplace.setVisible(OptionsApplicabilityFilter.isApplicable(OptionId.RENAME_IN_PLACE)); + myRichCopyColorSchemeComboBox.setRenderer(new ListCellRendererWrapper() { + @Override + public void customize(JList list, String value, int index, boolean selected, boolean hasFocus) { + final String textToUse; + if (RichCopySettings.ACTIVE_GLOBAL_SCHEME_MARKER.equals(value)) { + textToUse = ACTIVE_COLOR_SCHEME; + } + else { + textToUse = value; + } + setText(textToUse); + } + }); + myConfigurable = new MyConfigurable(); initQuickDocProcessing(); initSoftWrapsSettingsProcessing(); @@ -189,6 +212,20 @@ public class EditorOptionsPanel { myShowOptimizeImportsDialogCheckBox.setSelected(editorSettings.getOptions().SHOW_OPIMIZE_IMPORTS_DIALOG); myErrorHighlightingPanel.reset(); + + RichCopySettings settings = RichCopySettings.getInstance(); + myRichCopyColorSchemeComboBox.removeAllItems(); + EditorColorsScheme[] schemes = EditorColorsManager.getInstance().getAllSchemes(); + myRichCopyColorSchemeComboBox.addItem(RichCopySettings.ACTIVE_GLOBAL_SCHEME_MARKER); + for (EditorColorsScheme scheme : schemes) { + myRichCopyColorSchemeComboBox.addItem(scheme.getName()); + } + String toSelect = settings.getSchemeName(); + if (!StringUtil.isEmpty(toSelect)) { + myRichCopyColorSchemeComboBox.setSelectedItem(toSelect); + } + + myRichCopyStripWhitespaceCheckBox.setSelected(settings.isStripIndents()); } public void apply() throws ConfigurationException { @@ -289,6 +326,14 @@ public class EditorOptionsPanel { uiSettings.CONSOLE_COMMAND_HISTORY_LIMIT = StringUtil.parseInt(myCommandsHistoryLimitField.getText(), uiSettings.CONSOLE_COMMAND_HISTORY_LIMIT); myErrorHighlightingPanel.apply(); + + RichCopySettings settings = RichCopySettings.getInstance(); + Object item = myRichCopyColorSchemeComboBox.getSelectedItem(); + if (item instanceof String) { + settings.setSchemeName(item.toString()); + } + settings.setStripIndents(myRichCopyStripWhitespaceCheckBox.isSelected()); + restartDaemons(); } @@ -406,6 +451,11 @@ public class EditorOptionsPanel { isModified |= isModified(myShowOptimizeImportsDialogCheckBox, editorSettings.getOptions().SHOW_OPIMIZE_IMPORTS_DIALOG); isModified |= myErrorHighlightingPanel.isModified(); + + RichCopySettings settings = RichCopySettings.getInstance(); + isModified |= !Comparing.equal(settings.getSchemeName(), myRichCopyColorSchemeComboBox.getSelectedItem()); + isModified |= isModified(myRichCopyStripWhitespaceCheckBox, settings.isStripIndents()); + return isModified; } 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 904ffe68770c..2c62b5bf7a4b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TextBlockTransferable.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/TextBlockTransferable.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * 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. @@ -27,38 +27,37 @@ 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() { - return myTransferDataFlavors; + // 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()]); } @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 new file mode 100644 index 000000000000..1ed75e202530 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/BaseTextWithMarkupCopyPasteProcessor.java @@ -0,0 +1,95 @@ +/* + * 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; + +import com.intellij.codeInsight.editorActions.CopyPastePostProcessor; +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.richcopy.model.SyntaxInfo; +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.datatransfer.Transferable; + +public abstract class BaseTextWithMarkupCopyPasteProcessor implements CopyPastePostProcessor, TextWithMarkupBuilder { + private static final Logger LOG = Logger.getInstance("#" + BaseTextWithMarkupCopyPasteProcessor.class.getName()); + + private T myData; + + protected BaseTextWithMarkupCopyPasteProcessor(TextWithMarkupProcessor processor) { + processor.addBuilder(this); + } + + @Nullable + @Override + public T collectTransferableData(PsiFile file, Editor editor, int[] startOffsets, int[] endOffsets) { + if (!Registry.is("editor.richcopy.enable")) { + return null; + } + return myData; + } + + @Nullable + @Override + public T extractTransferableData(Transferable content) { + return null; + } + + @Override + public void processTransferableData(Project project, + Editor editor, + RangeMarker bounds, + int caretOffset, + Ref indented, + T value) { + + } + + @Override + public void build(CharSequence charSequence, SyntaxInfo syntaxInfo) { + String stringRepresentation = null; + final StringBuilder buffer = StringBuilderSpinAllocator.alloc(); + try { + buildStringRepresentation(buffer, charSequence, syntaxInfo, Registry.intValue("editor.richcopy.max.size.megabytes") * 1048576); + stringRepresentation = buffer.toString(); + if (Registry.is("editor.richcopy.debug")) { + LOG.info("Resulting text: \n'" + stringRepresentation + "'"); + } + } + catch (Exception e){ + LOG.error(e); + } + finally { + StringBuilderSpinAllocator.dispose(buffer); + } + myData = stringRepresentation == null ? null : createTransferable(stringRepresentation); + } + + @Override + public void reset() { + myData = null; + } + + protected abstract void buildStringRepresentation(@NotNull StringBuilder buffer, @NotNull CharSequence rawText, @NotNull SyntaxInfo syntaxInfo, int maxLength); + protected abstract T createTransferable(@NotNull String data); +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/FontMapper.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/FontMapper.java new file mode 100644 index 000000000000..62bf88e0a824 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/FontMapper.java @@ -0,0 +1,73 @@ +/* + * 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; + +import com.intellij.application.options.OptionsConstants; +import com.intellij.openapi.diagnostic.Logger; +import sun.font.CompositeFont; +import sun.font.Font2D; +import sun.font.FontManager; +import sun.font.FontManagerFactory; + +import java.awt.*; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Java logical font names (like 'Monospaced') don't necessarily make sense for other applications, so we try to map those fonts to + * the corresponding physical font names. + */ +public class FontMapper { + private static final Logger LOG = Logger.getInstance("#" + FontMapper.class.getName()); + + private static final String MAC_OS_FONT_CLASS = "sun.font.CFont"; + private static final String[] logicalFontsToMap = {Font.DIALOG, Font.DIALOG_INPUT, Font.MONOSPACED, Font.SERIF, Font.SANS_SERIF}; + private static final Map logicalToPhysicalMapping = new HashMap(); + + static { + try { + FontManager fontManager = FontManagerFactory.getInstance(); + for (String logicalFont : logicalFontsToMap) { + String physicalFont = null; + Font2D font2D = fontManager.findFont2D(logicalFont, OptionsConstants.DEFAULT_EDITOR_FONT_SIZE, 0); + if (font2D == null) { + continue; + } + else if (font2D instanceof CompositeFont && ((CompositeFont)font2D).getNumSlots() > 0) { + physicalFont = ((CompositeFont)font2D).getSlotFont(0).getFamilyName(Locale.getDefault()); + } + else if (font2D.getClass().getName().equals(MAC_OS_FONT_CLASS)) { + Field field = Class.forName(MAC_OS_FONT_CLASS).getDeclaredField("nativeFontName"); + field.setAccessible(true); + physicalFont = (String)field.get(font2D); + } + if (physicalFont != null) { + logicalToPhysicalMapping.put(logicalFont, physicalFont); + } + } + } + catch (Throwable e) { + LOG.warn("Failed to determine logical to physical font mappings"); + } + } + + public static String getPhysicalFontName(String logicalFontName) { + String mapped = logicalToPhysicalMapping.get(logicalFontName); + return mapped == null ? logicalFontName : mapped; + } +} 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 new file mode 100644 index 000000000000..81e56769e9c6 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/HtmlCopyPasteProcessor.java @@ -0,0 +1,249 @@ +/* + * 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; + +import com.intellij.openapi.editor.richcopy.model.*; +import com.intellij.openapi.editor.richcopy.view.ReaderTransferableData; +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:05 PM + */ +public class HtmlCopyPasteProcessor extends BaseTextWithMarkupCopyPasteProcessor implements OutputInfoVisitor { + @NotNull public static final DataFlavor FLAVOR = new DataFlavor("text/html;class=java.io.Reader", "HTML text"); + + private StringBuilder myResultBuffer; + private CharSequence myRawText; + private ColorRegistry myColorRegistry; + private FontNameRegistry myFontNameRegistry; + private TIntObjectHashMap myColors = new TIntObjectHashMap(); + + private int myForeground; + private int myBackground; + private int myFontFamily; + private int myFontSize; + private boolean myBold; + private boolean myItalic; + private boolean myIgnoreFontSize; + + public HtmlCopyPasteProcessor(TextWithMarkupProcessor processor) { + super(processor); + } + + @Override + protected void buildStringRepresentation(@NotNull StringBuilder buffer, + @NotNull CharSequence rawText, + @NotNull SyntaxInfo syntaxInfo, + int maxLength) { + myResultBuffer = buffer; + myRawText = rawText; + myColorRegistry = syntaxInfo.getColorRegistry(); + myFontNameRegistry = syntaxInfo.getFontNameRegistry(); + buildColorMap(); + try { + myResultBuffer.append("
") + .append("
 0) {
+        appendFontSizeRule(myResultBuffer, fontSize);
+        myIgnoreFontSize = true;
+      }
+      myResultBuffer.append("\" bgcolor=\"");
+      appendColor(myResultBuffer, syntaxInfo.getDefaultBackground());
+      myResultBuffer.append("\">");
+
+      for (OutputInfo info : syntaxInfo.getOutputInfos()) {
+        info.invite(this);
+        if (myResultBuffer.length() > maxLength) {
+          myResultBuffer.append("... truncated ...");
+          break;
+        }
+      }
+      myResultBuffer.append("
"); + } + finally { + myResultBuffer = null; + myRawText = null; + myColorRegistry = null; + myFontNameRegistry = null; + myColors.clear(); + myForeground = 0; + myBackground = 0; + myFontFamily = 0; + myFontSize = 0; + myBold = false; + myItalic = false; + myIgnoreFontSize = false; + } + } + + 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 buildColorMap() { + for (int id : myColorRegistry.getAllIds()) { + StringBuilder b = new StringBuilder("#"); + UIUtil.appendColor(myColorRegistry.dataById(id), b); + myColors.put(id, b.toString()); + } + } + + private void appendColor(StringBuilder builder, int id) { + builder.append(myColors.get(id)); + } + + 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(';'); + } + + @Override + public void visit(@NotNull Text text) { + boolean formattedText = myForeground > 0 || myBackground > 0 || myFontFamily > 0 || myFontSize > 0 || myBold || myItalic; + if (!formattedText) { + escapeAndAdd(text); + 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); + } + if (myFontSize > 0) { + appendFontSizeRule(styleBuffer, myFontSize); + } + myResultBuffer.append(""); + escapeAndAdd(text); + myResultBuffer.append(""); + myResultBuffer.append(closeTagBuffer); + } + finally { + StringBuilderSpinAllocator.dispose(styleBuffer); + StringBuilderSpinAllocator.dispose(closeTagBuffer); + } + } + + private void escapeAndAdd(Text text) { + int start = text.getStartOffset(); + int end = text.getEndOffset(); + for (int i = start; i < end; i++) { + char c = text.getCharAt(myRawText, i); + switch (c) { + case '<': myResultBuffer.append("<"); break; + case '>': myResultBuffer.append(">"); break; + case '&': myResultBuffer.append("&"); break; + default: myResultBuffer.append(c); + } + } + } + + @Override + public void visit(@NotNull Foreground color) { + myForeground = color.getId(); + } + + @Override + public void visit(@NotNull Background color) { + myBackground = color.getId(); + } + + @Override + public void visit(@NotNull FontFamilyName name) { + if (myFontNameRegistry != null) { + myFontFamily = name.getId(); + } + } + + @Override + public void visit(@NotNull FontStyle style) { + myBold = (Font.BOLD & style.getStyle()) != 0; + myItalic = (Font.ITALIC & style.getStyle()) != 0; + } + + @Override + public void visit(@NotNull FontSize size) { + if (!myIgnoreFontSize) { + myFontSize = size.getSize(); + } + } + + @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 new file mode 100644 index 000000000000..ab96fb475f96 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/RtfCopyPasteProcessor.java @@ -0,0 +1,230 @@ +/* + * 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; + +import com.intellij.openapi.editor.richcopy.model.*; +import com.intellij.openapi.editor.richcopy.view.InputStreamTransferableData; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.io.UnsupportedEncodingException; + +/** + * @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"; + + public RtfCopyPasteProcessor(TextWithMarkupProcessor processor) { + super(processor); + } + + @Override + protected void buildStringRepresentation(@NotNull final StringBuilder buffer, + @NotNull final CharSequence rawText, + @NotNull final SyntaxInfo syntaxInfo, + final int maxLength) { + header(syntaxInfo, buffer, new Runnable() { + @Override + public void run() { + rectangularBackground(syntaxInfo, buffer, new Runnable() { + @Override + public void run() { + content(syntaxInfo, buffer, rawText, maxLength); + } + }); + } + }); + } + + @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 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 CharSequence rawText, int maxLength) { + MyVisitor visitor = new MyVisitor(buffer, rawText); + for (OutputInfo info : syntaxInfo.getOutputInfos()) { + info.invite(visitor); + if (buffer.length() > maxLength) { + buffer.append("... truncated ..."); + break; + } + } + } + + 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 OutputInfoVisitor { + + @NotNull private final StringBuilder myBuffer; + @NotNull private final CharSequence 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 CharSequence rawText) { + myBuffer = buffer; + myRawText = rawText; + } + + @Override + public void visit(@NotNull Text text) { + myBuffer.append("\n"); + for (int i = text.getStartOffset(), limit = text.getEndOffset(); i < limit; i++) { + char c = text.getCharAt(myRawText, 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 visit(@NotNull Foreground color) { + saveForeground(myBuffer, color.getId()); + myForegroundId = color.getId(); + } + + @Override + public void visit(@NotNull Background color) { + saveBackground(myBuffer, color.getId()); + myBackgroundId = color.getId(); + } + + @Override + public void visit(@NotNull FontFamilyName name) { + saveFontName(myBuffer, name.getId()); + myFontNameId = name.getId(); + } + + @Override + public void visit(@NotNull FontSize size) { + saveFontSize(myBuffer, size.getSize()); + myFontSize = size.getSize(); + } + + @Override + public void visit(@NotNull FontStyle style) { + // 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.getStyle(); + if ((myFontStyle & Font.ITALIC) > 0) { + myBuffer.append(ITALIC); + } + if ((myFontStyle & Font.BOLD) > 0) { + myBuffer.append(BOLD); + } + } + } +} 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 new file mode 100644 index 000000000000..5879c0640a99 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupBuilder.java @@ -0,0 +1,23 @@ +/* + * 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; + +import com.intellij.openapi.editor.richcopy.model.SyntaxInfo; + +public interface TextWithMarkupBuilder { + void reset(); + void build(CharSequence charSequence, 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 new file mode 100644 index 000000000000..426eff8a0ab2 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/TextWithMarkupProcessor.java @@ -0,0 +1,803 @@ +/* + * 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; + +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.codeInsight.daemon.impl.HighlightInfoType; +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.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.editor.ex.DisposableIterator; +import com.intellij.openapi.editor.ex.MarkupModelEx; +import com.intellij.openapi.editor.ex.RangeHighlighterEx; +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.markup.HighlighterLayer; +import com.intellij.openapi.editor.markup.MarkupModel; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.editor.richcopy.model.*; +import com.intellij.openapi.editor.richcopy.settings.RichCopySettings; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +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.containers.ContainerUtilRt; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.awt.datatransfer.Transferable; +import java.util.*; +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 List myBuilders = new ArrayList(); + + public void addBuilder(TextWithMarkupBuilder builder) { + myBuilders.add(builder); + } + + @Nullable + @Override + public TextBlockTransferableData collectTransferableData(PsiFile file, Editor editor, int[] startOffsets, int[] endOffsets) { + if (!Registry.is("editor.richcopy.enable")) { + return null; + } + + try { + for (TextWithMarkupBuilder builder : myBuilders) { + builder.reset(); + } + + RichCopySettings settings = RichCopySettings.getInstance(); + Document document = editor.getDocument(); + final int indentSymbolsToStrip; + final int firstLineStartOffset; + if (settings.isStripIndents() && startOffsets.length == 1) { + Pair p = calcIndentSymbolsToStrip(document, startOffsets[0], endOffsets[0]); + firstLineStartOffset = p.first; + indentSymbolsToStrip = p.second; + } + else { + firstLineStartOffset = startOffsets[0]; + indentSymbolsToStrip = 0; + } + logInitial(document, startOffsets, endOffsets, indentSymbolsToStrip, firstLineStartOffset); + CharSequence text = document.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); + Context context = new Context(text, schemeToUse, indentSymbolsToStrip); + + for (int i = 0; i < startOffsets.length; i++) { + int startOffsetToUse; + if (i == 0) { + startOffsetToUse = firstLineStartOffset; + } + else { + startOffsetToUse = startOffsets[i]; + context.outputInfos.add(new Text('\n')); + } + int endOffsetToUse = endOffsets[i]; + context.reset(); + if (endOffsetToUse <= startOffsetToUse) { + continue; + } + DisposableIterator it = aggregateSyntaxInfo(schemeToUse, + wrap(highlighter, text, schemeToUse, startOffsetToUse, endOffsetToUse), + wrap(markupModel, text, schemeToUse, startOffsetToUse, endOffsetToUse)); + try { + while (it.hasNext()) { + SegmentInfo info = it.next(); + if (info.startOffset >= endOffsetToUse) { + break; + } + context.onNewData(info); + } + } + finally { + it.dispose(); + } + context.onIterationEnd(endOffsetToUse); + } + SyntaxInfo syntaxInfo = context.finish(); + logSyntaxInfo(syntaxInfo); + + for (TextWithMarkupBuilder builder : myBuilders) { + builder.build(text, syntaxInfo); + } + } + catch (Exception e) { + // catching the exception so that the rest of copy/paste functionality can still work fine + LOG.error(e); + } + return null; + } + + @Nullable + @Override + public TextBlockTransferableData extractTransferableData(Transferable content) { + return null; + } + + @Override + public void processTransferableData(Project project, + Editor editor, + RangeMarker bounds, + int caretOffset, + Ref indented, + TextBlockTransferableData value) { + + } + + private static void logInitial(@NotNull Document document, + @NotNull int[] startOffsets, + @NotNull int[] endOffsets, + int indentSymbolsToStrip, + int firstLineStartOffset) + { + if (!Registry.is("editor.richcopy.debug")) { + return; + } + + StringBuilder buffer = new StringBuilder(); + CharSequence text = document.getCharsSequence(); + for (int i = 0; i < startOffsets.length; i++) { + int start = startOffsets[i]; + int lineStart = document.getLineStartOffset(document.getLineNumber(start)); + int end = endOffsets[i]; + int lineEnd = document.getLineEndOffset(document.getLineNumber(end)); + buffer.append(" region #").append(i).append(": ").append(start).append('-').append(end).append(", text at range ") + .append(lineStart).append('-').append(lineEnd).append(": \n'").append(text.subSequence(lineStart, lineEnd)).append("'\n"); + } + if (buffer.length() > 0) { + buffer.setLength(buffer.length() - 1); + } + LOG.info(String.format( + "Preparing syntax-aware text. Given: %s selection, indent symbols to strip=%d, first line start offset=%d, selected text:%n%s", + startOffsets.length > 1 ? "block" : "regular", indentSymbolsToStrip, firstLineStartOffset, buffer + )); + } + + private static void logSyntaxInfo(@NotNull SyntaxInfo info) { + if (!Registry.is("editor.richcopy.debug")) { + return; + } + LOG.info("Constructed syntax info: " + info); + } + + private static Pair calcIndentSymbolsToStrip( + @NotNull Document document, int startOffset, int endOffset) + { + int startLine = document.getLineNumber(startOffset); + int endLine = document.getLineNumber(endOffset); + CharSequence text = document.getCharsSequence(); + int maximumCommonIndent = Integer.MAX_VALUE; + int firstLineStart = startOffset; + int firstLineEnd = startOffset; + for (int line = startLine; line <= endLine; line++) { + int lineStartOffset = document.getLineStartOffset(line); + int lineEndOffset = document.getLineEndOffset(line); + if (line == startLine) { + firstLineStart = lineStartOffset; + firstLineEnd = lineEndOffset; + } + int nonWsOffset = lineEndOffset; + for (int i = lineStartOffset; i < lineEndOffset && (i - lineStartOffset) < maximumCommonIndent && i < endOffset; i++) { + char c = text.charAt(i); + if (c != ' ' && c != '\t') { + nonWsOffset = i; + break; + } + } + if (nonWsOffset >= lineEndOffset) { + continue; // Blank line + } + int indent = nonWsOffset - lineStartOffset; + maximumCommonIndent = Math.min(maximumCommonIndent, indent); + if (maximumCommonIndent == 0) { + break; + } + } + int startOffsetToUse = Math.min(firstLineEnd, Math.max(startOffset, firstLineStart + maximumCommonIndent)); + return Pair.create(startOffsetToUse, maximumCommonIndent); + } + + private static DisposableIterator 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.fontSize, 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 public final List outputInfos = ContainerUtilRt.newArrayList(); + + @NotNull private final ColorRegistry myColorRegistry = new ColorRegistry(); + @NotNull private final FontNameRegistry myFontNameRegistry = new FontNameRegistry(); + + @NotNull private final CharSequence myText; + @NotNull private final Color myDefaultForeground; + @NotNull private final Color myDefaultBackground; + + @Nullable private Color myBackground; + @Nullable private Color myForeground; + @Nullable private String myFontFamilyName; + + private final int myIndentSymbolsToStrip; + + private int myFontStyle = -1; + private int myFontSize = -1; + private int myStartOffset = -1; + + private int mySingleFontSize; + + private int myIndentSymbolsToStripAtCurrentLine; + + Context(@NotNull CharSequence charSequence, @NotNull EditorColorsScheme scheme, int indentSymbolsToStrip) { + myText = charSequence; + myDefaultForeground = scheme.getDefaultForeground(); + myDefaultBackground = scheme.getDefaultBackground(); + myIndentSymbolsToStrip = indentSymbolsToStrip; + } + + public void reset() { + myStartOffset = -1; + myIndentSymbolsToStripAtCurrentLine = 0; + } + + public void onNewData(@NotNull SegmentInfo info) { + if (myStartOffset < 0) { + myStartOffset = info.startOffset; + } + + boolean whiteSpacesOnly = containsWhiteSpacesOnly(info); + + processBackground(info); + if (!whiteSpacesOnly) { + processForeground(info); + processFontFamilyName(info); + processFontStyle(info); + processFontSize(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; + } + } + return true; + } + + private void processFontStyle(@NotNull SegmentInfo info) { + if (info.fontStyle != myFontStyle) { + addTextIfPossible(info.startOffset); + outputInfos.add(new FontStyle(info.fontStyle)); + myFontStyle = info.fontStyle; + } + } + + private void processFontSize(@NotNull SegmentInfo info) { + if (mySingleFontSize == 0) { + mySingleFontSize = info.fontSize; + } + else if (mySingleFontSize > 0 && mySingleFontSize != info.fontSize) { + mySingleFontSize = -1; + } + if (info.fontSize != myFontSize) { + addTextIfPossible(info.startOffset); + outputInfos.add(new FontSize(info.fontSize)); + myFontSize = info.fontSize; + } + } + + private void processFontFamilyName(@NotNull SegmentInfo info) { + String fontFamilyName = FontMapper.getPhysicalFontName(info.fontFamilyName); + if (!fontFamilyName.equals(myFontFamilyName)) { + addTextIfPossible(info.startOffset); + outputInfos.add(new FontFamilyName(myFontNameRegistry.getId(fontFamilyName))); + myFontFamilyName = fontFamilyName; + } + } + + private void processForeground(@NotNull SegmentInfo info) { + if (myForeground == null && info.foreground != null) { + addTextIfPossible(info.startOffset); + myForeground = info.foreground; + outputInfos.add(new Foreground(myColorRegistry.getId(info.foreground))); + } + else if (myForeground != null) { + Color c = info.foreground == null ? myDefaultForeground : info.foreground; + if (!myForeground.equals(c)) { + addTextIfPossible(info.startOffset); + outputInfos.add(new Foreground(myColorRegistry.getId(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; + outputInfos.add(new Background(myColorRegistry.getId(info.background))); + } + else if (myBackground != null) { + Color c = info.background == null ? myDefaultBackground : info.background; + if (!myBackground.equals(c)) { + addTextIfPossible(info.startOffset); + outputInfos.add(new Background(myColorRegistry.getId(c))); + myBackground = c; + } + } + } + + private void addTextIfPossible(int endOffset) { + if (endOffset <= myStartOffset) { + return; + } + + for (int i = myStartOffset; i < endOffset; i++) { + char c = myText.charAt(i); + switch (c) { + case '\n': + myIndentSymbolsToStripAtCurrentLine = myIndentSymbolsToStrip; + outputInfos.add(new Text(myStartOffset, i + 1)); + myStartOffset = i + 1; + break; + // Intended fall-through. + case ' ': + case '\t': + if (myIndentSymbolsToStripAtCurrentLine > 0) { + myIndentSymbolsToStripAtCurrentLine--; + myStartOffset++; + continue; + } + default: myIndentSymbolsToStripAtCurrentLine = 0; + } + } + + if (myStartOffset < endOffset) { + outputInfos.add(new Text(myStartOffset, endOffset)); + myStartOffset = endOffset; + } + } + + public void onIterationEnd(int endOffset) { + addTextIfPossible(endOffset); + } + + @NotNull + public SyntaxInfo finish() { + int foreground = myColorRegistry.getId(myDefaultForeground); + int background = myColorRegistry.getId(myDefaultBackground); + myColorRegistry.seal(); + myFontNameRegistry.seal(); + return new SyntaxInfo(outputInfos, foreground, background, mySingleFontSize, myFontNameRegistry, myColorRegistry); + } + } + + private static class SegmentInfo implements Comparable { + + @Nullable public final Color foreground; + @Nullable public final Color background; + @NotNull public final String fontFamilyName; + + public final int fontStyle; + public final int fontSize; + public final int startOffset; + public final int endOffset; + + SegmentInfo(@Nullable Color foreground, + @Nullable Color background, + @NotNull String fontFamilyName, + int fontStyle, + int fontSize, + int startOffset, + int endOffset) + { + this.foreground = foreground; + this.background = background; + this.fontFamilyName = fontFamilyName; + this.fontStyle = fontStyle; + this.fontSize = fontSize; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + @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(); + int currentFontSize = font.getSize(); + String candidateFontFamilyName; + int candidateFontSize; + for (int i = start + 1; i < end; i++) { + font = ComplementaryFontsRegistry.getFontAbleToDisplay(text.charAt(i), fontSize, fontStyle, defaultFontFamily).getFont(); + candidateFontFamilyName = font.getFamily(); + candidateFontSize = font.getSize(); + if (!candidateFontFamilyName.equals(currentFontFamilyName) || currentFontSize != candidateFontSize) { + result.add(new SegmentInfo(attribute.getForegroundColor(), + attribute.getBackgroundColor(), + currentFontFamilyName, + fontStyle, + currentFontSize, + currentStart, + i + )); + currentStart = i; + currentFontFamilyName = candidateFontFamilyName; + currentFontSize = candidateFontSize; + } + } + + if (currentStart < end) { + result.add(new SegmentInfo(attribute.getForegroundColor(), + attribute.getBackgroundColor(), + currentFontFamilyName, + fontStyle, + currentFontSize, + currentStart, + end + )); + } + + return result; + } + + @Override + public int compareTo(@NotNull SegmentInfo o) { + return startOffset - o.startOffset; + } + + @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 + fontSize; + result = 31 * result + startOffset; + result = 31 * result + endOffset; + return result; + } + + @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 (fontSize != info.fontSize) 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); + } + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractFlyweightInfo.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractFlyweightInfo.java new file mode 100644 index 000000000000..955ebbe087b2 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractFlyweightInfo.java @@ -0,0 +1,47 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:11 PM + */ +public abstract class AbstractFlyweightInfo implements OutputInfo { + + private final int myId; + + public AbstractFlyweightInfo(int id) { + myId = id; + } + + public int getId() { + return myId; + } + + @Override + public int hashCode() { + return myId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AbstractFlyweightInfo info = (AbstractFlyweightInfo)o; + return myId == info.myId; + } +} 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 new file mode 100644 index 000000000000..23c0c01cdb09 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/AbstractRegistry.java @@ -0,0 +1,73 @@ +/* + * 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 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 { + + @NotNull private final TIntObjectHashMap myDataById = new TIntObjectHashMap(); + + private transient TObjectIntHashMap myIdsByData = new TObjectIntHashMap(); + + @NotNull + public T dataById(int id) throws IllegalArgumentException { + T result = myDataById.get(id); + if (result == null) { + throw new IllegalArgumentException("No data is registered for id " + id); + } + return result; + } + + public int getId(@NotNull T data) throws IllegalStateException { + if (myIdsByData == null) { + throw new IllegalStateException(String.format( + "Can't register data '%s'. Reason: the %s registry is already sealed", data, getClass().getName() + )); + } + int id = myIdsByData.get(data); + if (id <= 0) { + id = myIdsByData.size() + 1; + myDataById.put(id, data); + myIdsByData.put(data, id); + } + return id; + } + + public int[] getAllIds() { + int[] result = myDataById.keys(); + Arrays.sort(result); + return result; + } + + public int size() { + return myDataById.size(); + } + + public void seal() { + myIdsByData = null; + myDataById.compact(); + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Background.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Background.java new file mode 100644 index 000000000000..a584bce38b66 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Background.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:23 PM + */ +public class Background extends AbstractFlyweightInfo { + + public Background(int id) { + super(id); + } + + @Override + public void invite(@NotNull OutputInfoVisitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return "background=" + getId(); + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/ColorRegistry.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/ColorRegistry.java new file mode 100644 index 000000000000..c319e1424f75 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/ColorRegistry.java @@ -0,0 +1,25 @@ +/* + * 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 java.awt.*; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:13 PM + */ +public class ColorRegistry extends AbstractRegistry { +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontFamilyName.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontFamilyName.java new file mode 100644 index 000000000000..ba1a21043a82 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontFamilyName.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/25/13 6:48 PM + */ +public class FontFamilyName extends AbstractFlyweightInfo { + + public FontFamilyName(int id) { + super(id); + } + + @Override + public void invite(@NotNull OutputInfoVisitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return "font family=" + getId(); + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontNameRegistry.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontNameRegistry.java new file mode 100644 index 000000000000..8158c96a85bf --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontNameRegistry.java @@ -0,0 +1,23 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:21 PM + */ +public class FontNameRegistry extends AbstractRegistry { +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontSize.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontSize.java new file mode 100644 index 000000000000..3783f33df9ed --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontSize.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/25/13 11:23 PM + */ +public class FontSize implements OutputInfo { + + private final int mySize; + + public FontSize(int size) { + mySize = size; + } + + public int getSize() { + return mySize; + } + + @Override + public void invite(@NotNull OutputInfoVisitor visitor) { + visitor.visit(this); + } + + @Override + public int hashCode() { + return mySize; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FontSize size = (FontSize)o; + + return mySize == size.mySize; + } + + @Override + public String toString() { + return "font size=" + mySize; + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontStyle.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontStyle.java new file mode 100644 index 000000000000..8180a68b8dc2 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/FontStyle.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/25/13 7:10 PM + */ +public class FontStyle implements OutputInfo { + + private final int myStyle; + + public FontStyle(int style) { + myStyle = style; + } + + public int getStyle() { + return myStyle; + } + + @Override + public void invite(@NotNull OutputInfoVisitor visitor) { + visitor.visit(this); + } + + @Override + public int hashCode() { + return myStyle; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FontStyle style = (FontStyle)o; + + return myStyle == style.myStyle; + } + + @Override + public String toString() { + return "font style=" + myStyle; + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Foreground.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Foreground.java new file mode 100644 index 000000000000..dfa95b1d1066 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Foreground.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:23 PM + */ +public class Foreground extends AbstractFlyweightInfo { + + public Foreground(int id) { + super(id); + } + + @Override + public void invite(@NotNull OutputInfoVisitor visitor) { + visitor.visit(this); + } + + @Override + public String toString() { + return "foreground=" + getId(); + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfo.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfo.java new file mode 100644 index 000000000000..c071ac1d957f --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfo.java @@ -0,0 +1,29 @@ +/* + * 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.io.Serializable; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:06 PM + */ +public interface OutputInfo extends Serializable { + + void invite(@NotNull OutputInfoVisitor visitor); +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfoVisitor.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfoVisitor.java new file mode 100644 index 000000000000..29a46e17b169 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/OutputInfoVisitor.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:07 PM + */ +public interface OutputInfoVisitor { + + void visit(@NotNull Text text); + void visit(@NotNull Foreground color); + void visit(@NotNull Background color); + void visit(@NotNull FontFamilyName name); + void visit(@NotNull FontStyle style); + void visit(@NotNull FontSize size); +} 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..024ffc5f87b1 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/SyntaxInfo.java @@ -0,0 +1,90 @@ +/* + * 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.io.Serializable; +import java.util.List; + +/** + * @author Denis Zhdanov + * @since 3/25/13 1:19 PM + */ +public class SyntaxInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull private final List myOutputInfos; + @NotNull private final ColorRegistry myColorRegistry; + @NotNull private final FontNameRegistry myFontNameRegistry; + + private final int myDefaultForeground; + private final int myDefaultBackground; + private final int mySingleFontSize; + + public SyntaxInfo(@NotNull List infos, + int defaultForeground, + int defaultBackground, + int singleFontSize, + @NotNull FontNameRegistry fontNameRegistry, + @NotNull ColorRegistry colorRegistry) + { + myOutputInfos = infos; + myDefaultForeground = defaultForeground; + myDefaultBackground = defaultBackground; + mySingleFontSize = singleFontSize; + myFontNameRegistry = fontNameRegistry; + myColorRegistry = colorRegistry; + } + + @NotNull + public List getOutputInfos() { + return myOutputInfos; + } + + @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() { + return String.format("default colors: foreground=%d, background=%d; output infos: %s", + myDefaultForeground, myDefaultBackground, myOutputInfos); + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Text.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Text.java new file mode 100644 index 000000000000..e15416949fa5 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/model/Text.java @@ -0,0 +1,76 @@ +/* + * 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; + +/** + * @author Denis Zhdanov + * @since 3/23/13 3:08 PM + */ +public class Text implements OutputInfo { + + private final int myStartOffset; + private final int myEndOffset; + + public Text(int startOffset, int endOffset) { + myStartOffset = startOffset; + myEndOffset = endOffset; + } + + public Text(char c) { + myStartOffset = -1; + myEndOffset = c; + } + + @Override + public void invite(@NotNull OutputInfoVisitor visitor) { + visitor.visit(this); + } + + public int getStartOffset() { + return myStartOffset; + } + + public int getEndOffset() { + return myStartOffset == -1 ? 0 : myEndOffset; + } + + public char getCharAt(CharSequence charSequence, int offset) { + return myStartOffset == -1 ? (char)myEndOffset : charSequence.charAt(offset); + } + + @Override + public int hashCode() { + int result = myStartOffset; + result = 31 * result + myEndOffset; + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Text text = (Text)o; + return myEndOffset == text.myEndOffset && myStartOffset == text.myStartOffset; + } + + @Override + public String toString() { + return myStartOffset == -1 ? String.format("text=%c", myEndOffset) : String.format("text=%d-%d", myStartOffset, myEndOffset); + } +} diff --git a/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/settings/RichCopySettings.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/settings/RichCopySettings.java new file mode 100644 index 000000000000..ec99560b9753 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/settings/RichCopySettings.java @@ -0,0 +1,96 @@ +/* + * 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.settings; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.*; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.util.xmlb.XmlSerializerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Denis Zhdanov + * @since 3/26/13 6:40 PM + */ +@State( + name = "EditorRichCopySettings", + storages = {@Storage(file = StoragePathMacros.APP_CONFIG + "/editor.rich.copy.xml")} +) +public class RichCopySettings implements PersistentStateComponent, ApplicationComponent { + + @NotNull public static final String ACTIVE_GLOBAL_SCHEME_MARKER = "__ACTIVE_GLOBAL_SCHEME__"; + + private String mySchemeName = ACTIVE_GLOBAL_SCHEME_MARKER; + private boolean myStripIndents = true; + + @NotNull + public static RichCopySettings getInstance() { + return ApplicationManager.getApplication().getComponent(RichCopySettings.class); + } + + @NotNull + public EditorColorsScheme getColorsScheme(@NotNull EditorColorsScheme editorColorsScheme) { + EditorColorsScheme result = null; + if (!ACTIVE_GLOBAL_SCHEME_MARKER.equals(mySchemeName)) { + result = EditorColorsManager.getInstance().getScheme(mySchemeName); + } + return result == null ? editorColorsScheme : result; + } + + @Nullable + @Override + public RichCopySettings getState() { + return this; + } + + @Override + public void loadState(RichCopySettings state) { + XmlSerializerUtil.copyBean(state, this); + } + + @Override + public void initComponent() { + } + + @Override + public void disposeComponent() { + } + + @NotNull + @Override + public String getComponentName() { + return getClass().getName(); + } + + @NotNull + public String getSchemeName() { + return mySchemeName == null ? ACTIVE_GLOBAL_SCHEME_MARKER : mySchemeName; + } + + public void setSchemeName(@Nullable String schemeName) { + mySchemeName = schemeName; + } + + public boolean isStripIndents() { + return myStripIndents; + } + + public void setStripIndents(boolean stripIndents) { + myStripIndents = stripIndents; + } +} 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 new file mode 100644 index 000000000000..29ee172216c0 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/InputStreamTransferableData.java @@ -0,0 +1,83 @@ +/* + * 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/ReaderTransferableData.java b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/ReaderTransferableData.java new file mode 100644 index 000000000000..6deea2c02671 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/editor/richcopy/view/ReaderTransferableData.java @@ -0,0 +1,84 @@ +/* + * 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/platform-resources-en/src/messages/ApplicationBundle.properties b/platform/platform-resources-en/src/messages/ApplicationBundle.properties index d5dccd501729..56e82930433b 100644 --- a/platform/platform-resources-en/src/messages/ApplicationBundle.properties +++ b/platform/platform-resources-en/src/messages/ApplicationBundle.properties @@ -631,4 +631,9 @@ settings.code.style.general.formatter.marker.title=Markers settings.code.style.general.formatter.marker.options.title=Options import.scheme.chooser.source=From\: import.scheme.chooser.destination=To\: -checkbox.reformat.on.typing.rbrace=Reformat block on typing '}' \ No newline at end of file +checkbox.reformat.on.typing.rbrace=Reformat block on typing '}' + +group.richcopy=Rich-text copy +checkbox.richcopy.strip.whitespace.at.line.start=Strip common indent +combobox.richcopy.color.scheme=Color scheme +combobox.richcopy.color.scheme.active=Active scheme diff --git a/platform/platform-resources-en/src/misc/registry.properties b/platform/platform-resources-en/src/misc/registry.properties index dda05b924e0c..66e94b777705 100644 --- a/platform/platform-resources-en/src/misc/registry.properties +++ b/platform/platform-resources-en/src/misc/registry.properties @@ -378,3 +378,7 @@ editor.injected.highlighting.enabled.description=Disables injected fragments hig run.processes.with.pty=false ide.certificate.manager=true + +editor.richcopy.enable=true +editor.richcopy.debug=false +editor.richcopy.max.size.megabytes=10 diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 62fe04251a31..a174ffc87b7b 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -880,6 +880,10 @@ + + + + diff --git a/platform/platform-resources/src/componentSets/Lang.xml b/platform/platform-resources/src/componentSets/Lang.xml index dc2fabfbeb07..403a2197e950 100644 --- a/platform/platform-resources/src/componentSets/Lang.xml +++ b/platform/platform-resources/src/componentSets/Lang.xml @@ -37,6 +37,10 @@ com.intellij.codeInsight.preview.ImageOrColorPreviewManager + + + com.intellij.openapi.editor.richcopy.settings.RichCopySettings + diff --git a/platform/platform-tests/testData/editor/richcopy/BlockSelection.html b/platform/platform-tests/testData/editor/richcopy/BlockSelection.html new file mode 100644 index 000000000000..e169d164d1a2 --- /dev/null +++ b/platform/platform-tests/testData/editor/richcopy/BlockSelection.html @@ -0,0 +1,3 @@ +
 int field;
+
+ public static void main(String[] args) {
\ No newline at end of file diff --git a/platform/platform-tests/testData/editor/richcopy/BlockSelection.java b/platform/platform-tests/testData/editor/richcopy/BlockSelection.java new file mode 100644 index 000000000000..2694d7d958f2 --- /dev/null +++ b/platform/platform-tests/testData/editor/richcopy/BlockSelection.java @@ -0,0 +1,7 @@ +public class Basic { + int field; + + public static void main(String[] args) { + System.out.println("Hello\tworld!"); + } +} diff --git a/platform/platform-tests/testData/editor/richcopy/BlockSelection.rtf b/platform/platform-tests/testData/editor/richcopy/BlockSelection.rtf new file mode 100644 index 000000000000..8b13a71c6cfe --- /dev/null +++ b/platform/platform-tests/testData/editor/richcopy/BlockSelection.rtf @@ -0,0 +1,18 @@ +{\rtf1\ansi\deff0{\colortbl;\red0\green0\blue128;\red0\green0\blue0;\red255\green255\blue255;} +{\fonttbl{\f1 Monospaced;}} + +\s0\box\brdrhair\brdrcf2\brsp317\cbpat3\cb3 + \cf1\f1\plain +\cf1\f1\b\fs24 +int \cf2\plain +\cf2\f1\fs24 +field; +\line + +\line + + \cf1\plain +\cf1\f1\fs24\b +public static void \cf2\plain +\cf2\f1\fs24 +main(String[] args) \{\par} \ No newline at end of file diff --git a/platform/platform-tests/testData/editor/richcopy/NormalSelection.html b/platform/platform-tests/testData/editor/richcopy/NormalSelection.html new file mode 100644 index 000000000000..f25aa58a5983 --- /dev/null +++ b/platform/platform-tests/testData/editor/richcopy/NormalSelection.html @@ -0,0 +1,8 @@ +
public class Basic {
+  int field;
+
+  public static void main(String[] args) {
+    System.out.println("Hello\tworld!");
+  }
+}
+
\ No newline at end of file diff --git a/platform/platform-tests/testData/editor/richcopy/NormalSelection.java b/platform/platform-tests/testData/editor/richcopy/NormalSelection.java new file mode 100644 index 000000000000..1870eecd5d9b --- /dev/null +++ b/platform/platform-tests/testData/editor/richcopy/NormalSelection.java @@ -0,0 +1,8 @@ +public class Basic { + int field; + + public static void main(String[] args) { + System.out.println("Hello\tworld!"); + } +} + \ No newline at end of file diff --git a/platform/platform-tests/testData/editor/richcopy/NormalSelection.rtf b/platform/platform-tests/testData/editor/richcopy/NormalSelection.rtf new file mode 100644 index 000000000000..ac483486c661 --- /dev/null +++ b/platform/platform-tests/testData/editor/richcopy/NormalSelection.rtf @@ -0,0 +1,35 @@ +{\rtf1\ansi\deff0{\colortbl;\red0\green0\blue128;\red0\green0\blue0;\red0\green128\blue0;\red255\green255\blue255;} +{\fonttbl{\f1 Monospaced;}} + +\s0\box\brdrhair\brdrcf2\brsp317\cbpat4\cb4\cf1\f1\plain +\cf1\f1\b\fs24 +public class \cf2\plain +\cf2\f1\fs24 +Basic \{\line + + \cf1\plain +\cf1\f1\fs24\b +int \cf2\plain +\cf2\f1\fs24 +field;\line + +\line + + \cf1\plain +\cf1\f1\fs24\b +public static void \cf2\plain +\cf2\f1\fs24 +main(String[] args) \{\line + + System.out.println(\cf3\plain +\cf3\f1\fs24\b +"Hello\cf1 +\\t\cf3 +world!"\cf2\plain +\cf2\f1\fs24 +);\line + + \}\line + +\}\line +\par} \ No newline at end of file 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 new file mode 100644 index 000000000000..0a4ef2a1f685 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/RichCopyTest.java @@ -0,0 +1,76 @@ +/* + * 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; + +import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.openapi.ide.CopyPasteManager; +import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + +import java.awt.datatransfer.Transferable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +public class RichCopyTest extends LightPlatformCodeInsightFixtureTestCase { + public void testNormalSelection() throws Exception { + doTest(false); + } + + public void testBlockSelection() throws Exception { + doTest(true); + } + + private void doTest(boolean columnMode) throws Exception { + myFixture.configureByFile(getTestName(false) + ".java"); + if (columnMode) { + myFixture.performEditorAction("EditorToggleColumnMode"); + } + myFixture.performEditorAction("EditorCopy"); + Transferable contents = CopyPasteManager.getInstance().getContents(); + assertNotNull(contents); + + assertTrue(contents.isDataFlavorSupported(HtmlCopyPasteProcessor.FLAVOR)); + String expectedHtml = getFileContents(getTestName(false) + ".html"); + String actualHtml = readFully((Reader)contents.getTransferData(HtmlCopyPasteProcessor.FLAVOR)); + assertEquals("HTML contents differs", expectedHtml, actualHtml); + + assertTrue(contents.isDataFlavorSupported(RtfCopyPasteProcessor.FLAVOR)); + String expectedRtf = getFileContents(getTestName(false) + ".rtf"); + String actualRtf = readFully((InputStream)contents.getTransferData(RtfCopyPasteProcessor.FLAVOR)); + assertEquals("RTF contents differs", expectedRtf, actualRtf); + } + + private static String readFully(InputStream inputStream) throws IOException { + return new String(FileUtilRt.loadBytes(inputStream)); + } + + private static String readFully(Reader reader) throws IOException { + return new String(FileUtilRt.loadText(reader, 10000)); + } + + private String getFileContents(String fileName) throws IOException { + return FileUtilRt.loadFile(new File(getTestDataPath() + fileName), true); + } + + @NotNull + @Override + protected String getTestDataPath() { + return PathManagerEx.getHomePath(getClass()) + "/platform/platform-tests/testData/editor/richcopy/"; + } +} 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 new file mode 100644 index 000000000000..632eb7ebf914 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/richcopy/SyntaxInfoConstructionTest.java @@ -0,0 +1,276 @@ +/* + * 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; + +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.*; +import com.intellij.openapi.util.Ref; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; +import junit.framework.TestCase; + +import java.awt.*; +import java.util.Arrays; +import java.util.List; + +/** + * @author Denis Zhdanov + * @since 3/27/13 11:11 AM + */ +public class SyntaxInfoConstructionTest extends LightPlatformCodeInsightFixtureTestCase { + public void testBlockSelection() { + String text = + "package org;\n" + + "\n" + + "public class TestClass {\n" + + "\n" + + " int field;\n" + + "\n" + + " public int getField() {\n" + + " return field;\n" + + " }\n" + + "}"; + init(text); + + int blockSelectionStartOffset = text.indexOf("public int"); + Editor editor = myFixture.getEditor(); + LogicalPosition blockSelectionStartPosition = editor.offsetToLogicalPosition(blockSelectionStartOffset); + LogicalPosition blockSelectionEndPosition = new LogicalPosition( + blockSelectionStartPosition.line + 2, + editor.offsetToLogicalPosition(text.indexOf('{', blockSelectionStartOffset)).column + 1); + editor.getSelectionModel().setBlockSelection(blockSelectionStartPosition, blockSelectionEndPosition); + + verifySyntaxInfo( + new Foreground(1), new FontFamilyName(1), new FontStyle(Font.BOLD), new FontSize(12), new Text(60, 71), // 'public int ' + new Foreground(2), new FontStyle(Font.PLAIN), new Text(71, 83), // 'getField() {' + new Text('\n'), // '\n' + new Text(88, 92), // ' ' - indent before 'return field;' + new Foreground(1), new FontStyle(Font.BOLD), new Text(92, 99), // 'return ' + new Foreground(3), new Text(99, 104), // 'field'; + new Foreground(2), new FontStyle(Font.PLAIN), new Text(104, 105), // ';' + new Text('\n'), // '\n' + new Text(110, 111) // '}' + ); + } + + public void testColumnModeBlockSelection() { + String text = + "package org;\n" + + "\n" + + "public class TestClass {\n" + + "\n" + + " int field;\n" + + "\n" + + " public int getField() {\n" + + " return field;\n" + + " }\n" + + "}"; + init(text); + + int blockSelectionStartOffset = text.indexOf("public int"); + Editor editor = myFixture.getEditor(); + ((EditorEx) editor).setColumnMode(true); + LogicalPosition blockSelectionStartPosition = editor.offsetToLogicalPosition(blockSelectionStartOffset); + LogicalPosition blockSelectionEndPosition = new LogicalPosition( + blockSelectionStartPosition.line + 2, + editor.offsetToLogicalPosition(text.indexOf('{', blockSelectionStartOffset)).column + 1); + editor.getSelectionModel().setBlockSelection(blockSelectionStartPosition, blockSelectionEndPosition); + + verifySyntaxInfo( + new Foreground(1), new FontFamilyName(1), new FontStyle(Font.BOLD), new FontSize(12), new Text(60, 71), // 'public int ' + new Foreground(2), new FontStyle(Font.PLAIN), new Text(71, 83), // 'getField() {' + new Text('\n'), // '\n' + new Text(88, 92), // ' ' - indent before 'return field;' + new Foreground(1), new FontStyle(Font.BOLD), new Text(92, 99), // 'return ' + new Foreground(3), new Text(99, 104), // 'field'; + new Foreground(2), new FontStyle(Font.PLAIN), new Text(104, 105), // ';' + new Text('\n'), // '\n' + new Text(110, 111) // '}' + ); + } + + public void testColumnModeBlockSelectionWithGaps() { + String text = + "public class TestClass {\n" + + "\n" + + " int field;\n" + + "\n" + + " int otherField;\n" + + "}"; + init(text); + + int blockSelectionStartOffset = text.indexOf("int"); + Editor editor = myFixture.getEditor(); + ((EditorEx) editor).setColumnMode(true); + LogicalPosition blockSelectionStartPosition = editor.offsetToLogicalPosition(blockSelectionStartOffset); + LogicalPosition blockSelectionEndPosition = new LogicalPosition(blockSelectionStartPosition.line + 2, blockSelectionStartPosition.column + 16); + editor.getSelectionModel().setBlockSelection(blockSelectionStartPosition, blockSelectionEndPosition); + + verifySyntaxInfo( + new Foreground(1), new FontFamilyName(1), new FontStyle(Font.BOLD), new FontSize(12), new Text(30, 34), // 'int ' + new Foreground(2), new Text(34, 39), // 'field' + new Foreground(3), new FontStyle(Font.PLAIN), new Text(39, 40), // ';' + new Text('\n'), // '\n' + new Text('\n'), // '\n' + new Foreground(1), new FontStyle(Font.BOLD), new Text(46, 50), // 'int ' + new Foreground(2), new Text(50, 60), // 'otherField'; + new Foreground(3), new FontStyle(Font.PLAIN), new Text(60, 61) // ';' + ); + } + + public void testRegularSelection() { + // We want to exclude unnecessary indents from the pasted results. + String text = + "package org;\n" + + "\n" + + "public class TestClass {\n" + + "\n" + + " int field;\n" + + "\n" + + " public int getField() {\n" + + " return field;\n" + + " }\n" + + "}"; + init(text); + + int selectionStart = text.indexOf("public int"); + int selectionEnd = text.indexOf('}', selectionStart) + 1; + SelectionModel selectionModel = myFixture.getEditor().getSelectionModel(); + selectionModel.setSelection(selectionStart, selectionEnd); + + List expected = Arrays.asList( + new Foreground(1), new FontFamilyName(1), new FontStyle(Font.BOLD), new FontSize(12), new Text(60, 71), // 'public int ' + new Foreground(2), new FontStyle(Font.PLAIN), new Text(71, 84), // 'getField() {\n' + new Text(88, 92), // ' ' - indent before 'return field;' + new Foreground(1), new FontStyle(Font.BOLD), new Text(92, 99), // 'return ' + new Foreground(3), new Text(99, 104), // 'field' + new Foreground(2), new FontStyle(Font.PLAIN), new Text(104, 106), // ';\n' + new Text(110, 111) // '}' + ); + + TestCase.assertEquals(expected, getSyntaxInfo().getOutputInfos()); + + selectionModel.setSelection(selectionStart - 2, selectionEnd); + TestCase.assertEquals(expected, getSyntaxInfo().getOutputInfos()); + + selectionModel.setSelection(selectionStart - 4, selectionEnd); + TestCase.assertEquals(expected, getSyntaxInfo().getOutputInfos()); + } + + public void testIncorrectFirstLineCalculationOffset() { + init("\"tr\" #> template.statusList.sortBy(_.index).map(fromStatus =>"); + + verifySyntaxInfo( + new FontFamilyName(1), new FontStyle(Font.PLAIN), new FontSize(12), new Text(8, 34) + ); + } + + public void testJavadoc() { + init( + "package org;\n" + + "\n" + + "import java.io.Serializable;\n" + + "\n" + + "/**\n" + + " * Code in here\n" + + " * Hi man\n" + + " * @param \n" + + " */\n" + + "public interface SampleTest extends Serializable {\n" + + " boolean isNotNull();\n" + + " T getValue();\n" + + "}" + ); + verifySyntaxInfo( + new Foreground(1), new FontFamilyName(1), new FontStyle(Font.ITALIC), new FontSize(12), new Text(44, 48), + new Text(48, 59), + new Background(2), new Text(59, 65), + new Background(3), new Text(65, 69), + new Background(2), new Text(69, 76), + new Background(3), new Text(76, 77), + new Text(77, 80), + new Background(2), new Text(80, 88), + new Background(3), new Text(88, 90), + new Background(2), new Text(90, 99), + new Background(3), new Text(99, 104), + new Text(104, 107), + new FontStyle(Font.BOLD + Font.ITALIC), new Text(107, 114), + new Foreground(4), new Text(114, 118), + new Text(118, 119) + ); + } + + public void testIndentStrippingWhenFirstLineIsMostIndented() throws Exception { + init("public class Test {\n" + + " int field;\n" + + "}"); + verifySyntaxInfo( + new Text(20, 22), // ' ' + new Foreground(1), new FontFamilyName(1), new FontStyle(Font.BOLD), new FontSize(12), new Text(22, 26), // 'int ' + new Foreground(2), new Text(26, 31), // 'field' + new Foreground(3), new FontStyle(Font.PLAIN), new Text(31, 33), // ';\n' + new Text(33, 34) // '}' + ); + } + + public void testIndentStrippingWhenSelectionEndIsBeforeNonWsCharactersOnTheLine() throws Exception { + init("public class Test {\n" + + " int field;\n" + + "}"); + verifySyntaxInfo( + new Foreground(1), new FontFamilyName(1), new FontStyle(Font.BOLD), new FontSize(12), new Text(22, 26), // 'int ' + new Foreground(2), new Text(26, 31), // 'field' + new Foreground(3), new FontStyle(Font.PLAIN), new Text(31, 33) // ';\n' + ); + } + + private SyntaxInfo getSyntaxInfo() { + final Ref syntaxInfo = new Ref(); + Editor editor = myFixture.getEditor(); + + TextWithMarkupProcessor processor = new TextWithMarkupProcessor(); + processor.addBuilder(new TextWithMarkupBuilder() { + @Override + public void reset() { + } + + @Override + public void build(CharSequence charSequence, SyntaxInfo info) { + syntaxInfo.set(info); + } + }); + SelectionModel selectionModel = editor.getSelectionModel(); + processor.collectTransferableData(myFixture.getFile(), editor, selectionModel.getBlockSelectionStarts(), selectionModel.getBlockSelectionEnds()); + + return syntaxInfo.get(); + } + + private void init(String text) { + myFixture.configureByText(getTestName(true) + ".java", text); + myFixture.doHighlighting(); + } + + private void verifySyntaxInfo(OutputInfo... infos) { + assertEquals(Arrays.asList(infos), getSyntaxInfo().getOutputInfos()); + } + + @Override + protected boolean isWriteActionRequired() { + return false; + } +} diff --git a/resources-en/src/search/searchableOptions.xml b/resources-en/src/search/searchableOptions.xml index 8e26b981e3cc..f0c1983a67c2 100644 --- a/resources-en/src/search/searchableOptions.xml +++ b/resources-en/src/search/searchableOptions.xml @@ -4327,6 +4327,13 @@