IDEA-67767 Copy syntax-coloured code block to clipboard as rtf

This commit is contained in:
Dmitry Batrak
2014-04-08 18:50:28 +04:00
parent f80bf32d98
commit cf0ab3d79c
38 changed files with 2926 additions and 23 deletions
@@ -446,11 +446,42 @@
</component>
</children>
</grid>
<vspacer id="9f8">
<grid id="62a96" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<properties/>
<clientProperties>
<BorderFactoryClass class="java.lang.String" value="com.intellij.ui.IdeBorderFactory$PlainSmallWithIndent"/>
</clientProperties>
<border type="none" title-resource-bundle="messages/ApplicationBundle" title-key="group.richcopy"/>
<children>
<component id="8b6fe" class="javax.swing.JCheckBox" binding="myRichCopyStripWhitespaceCheckBox">
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="true"/>
<text resource-bundle="messages/ApplicationBundle" key="checkbox.richcopy.strip.whitespace.at.line.start"/>
</properties>
</component>
<component id="828ad" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text resource-bundle="messages/ApplicationBundle" key="combobox.richcopy.color.scheme"/>
</properties>
</component>
<component id="3b679" class="javax.swing.JComboBox" binding="myRichCopyColorSchemeComboBox">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
</children>
</grid>
</children>
</grid>
</children>
@@ -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<String>() {
@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;
}
@@ -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<TextBlockTransferableData> myExtraData;
private final RawText myRawText;
private final String myText;
private final DataFlavor[] myTransferDataFlavors;
public TextBlockTransferable(String text, Collection<TextBlockTransferableData> extraData, RawText rawText) {
myText = text;
myExtraData = extraData;
myRawText = rawText;
List<DataFlavor> dataFlavors = new ArrayList<DataFlavor>();
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<DataFlavor> flavors = new ArrayList<DataFlavor>(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
@@ -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<T extends TextBlockTransferableData> implements CopyPastePostProcessor<T>, 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<Boolean> 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);
}
@@ -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<String, String> logicalToPhysicalMapping = new HashMap<String, String>();
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;
}
}
@@ -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<ReaderTransferableData> 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<String> myColors = new TIntObjectHashMap<String>();
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("<div style=\"border:1px inset;padding:2%;\">")
.append("<pre style=\"margin:0;padding:6px;background-color:");
// .append("<pre style='height:30%;overflow:auto;margin:0;padding:6px;background-color:")
appendColor(myResultBuffer, syntaxInfo.getDefaultBackground());
myResultBuffer.append(';');
if (myFontNameRegistry.size() == 1) {
appendFontFamilyRule(myResultBuffer, myFontNameRegistry.getAllIds()[0]);
myFontNameRegistry = null;
}
int fontSize = syntaxInfo.getSingleFontSize();
if (fontSize > 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("</pre></div>");
}
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("<font color=\"");
appendColor(myResultBuffer, id);
myResultBuffer.append("\">");
styleBuffer.append("color:");
appendColor(styleBuffer, id);
styleBuffer.append(";");
closeTagBuffer.insert(0, "</font>");
}
private void defineBackground(int id, @NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) {
myResultBuffer.append("<font bgcolor=\"");
appendColor(myResultBuffer, id);
myResultBuffer.append("\">");
styleBuffer.append("background-color:");
appendColor(styleBuffer, id);
styleBuffer.append(";");
closeTagBuffer.insert(0, "</font>");
}
private void defineBold(@NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) {
myResultBuffer.append("<b>");
styleBuffer.append("font-weight:bold;");
closeTagBuffer.insert(0, "</b>");
}
private void defineItalic(@NotNull StringBuilder styleBuffer, @NotNull StringBuilder closeTagBuffer) {
myResultBuffer.append("<i>");
styleBuffer.append("font-style:italic;");
closeTagBuffer.insert(0, "</i>");
}
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("<span style=\"");
myResultBuffer.append(styleBuffer);
myResultBuffer.append("\">");
escapeAndAdd(text);
myResultBuffer.append("</span>");
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("&lt;"); break;
case '>': myResultBuffer.append("&gt;"); break;
case '&': myResultBuffer.append("&amp;"); 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);
}
}
@@ -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<InputStreamTransferableData> {
@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);
}
}
}
}
@@ -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);
}
@@ -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<TextBlockTransferableData> {
private static final Logger LOG = Logger.getInstance("#" + TextWithMarkupProcessor.class.getName());
private final List<TextWithMarkupBuilder> myBuilders = new ArrayList<TextWithMarkupBuilder>();
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<Integer, Integer> 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<SegmentInfo> 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<Boolean> 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<Integer/* start offset to use */, Integer /* indent symbols to strip */> 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<SegmentInfo> aggregateSyntaxInfo(@NotNull EditorColorsScheme colorsScheme,
@NotNull final DisposableIterator<List<SegmentInfo>>... iterators)
{
final Color defaultForeground = colorsScheme.getDefaultForeground();
final Color defaultBackground = colorsScheme.getDefaultBackground();
return new DisposableIterator<SegmentInfo>() {
@NotNull private final Queue<SegmentInfo> myInfos = new PriorityQueue<SegmentInfo>();
@NotNull private final Map<SegmentInfo, DisposableIterator<List<SegmentInfo>>> myEndMarkers
= new IdentityHashMap<SegmentInfo, DisposableIterator<List<SegmentInfo>>>();
{
for (DisposableIterator<List<SegmentInfo>> iterator : iterators) {
extract(iterator);
}
}
@Override
public boolean hasNext() {
return !myInfos.isEmpty();
}
@Override
public SegmentInfo next() {
SegmentInfo result = myInfos.remove();
DisposableIterator<List<SegmentInfo>> 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<List<SegmentInfo>> 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<List<SegmentInfo>> iterator : iterators) {
iterator.dispose();
}
}
private void extract(@NotNull DisposableIterator<List<SegmentInfo>> iterator) {
while (iterator.hasNext()) {
List<SegmentInfo> infos = iterator.next();
if (infos.isEmpty()) {
continue;
}
myInfos.addAll(infos);
myEndMarkers.put(infos.get(infos.size() - 1), iterator);
break;
}
}
};
}
@NotNull
private static DisposableIterator<List<SegmentInfo>> 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<List<SegmentInfo>>() {
@Nullable private List<SegmentInfo> myCached;
@Override
public boolean hasNext() {
return myCached != null || updateCached();
}
@Override
public List<SegmentInfo> next() {
if (myCached != null) {
List<SegmentInfo> 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<List<SegmentInfo>> 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<RangeHighlighterEx> iterator = ((MarkupModelEx)model).overlappingIterator(startOffset, endOffset);
final Color defaultForeground = colorsScheme.getDefaultForeground();
final Color defaultBackground = colorsScheme.getDefaultBackground();
return new DisposableIterator<List<SegmentInfo>>() {
@Nullable private List<SegmentInfo> myCached;
@Override
public boolean hasNext() {
return myCached != null || updateCached();
}
@Override
public List<SegmentInfo> next() {
if (myCached != null) {
List<SegmentInfo> 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<OutputInfo> 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<SegmentInfo> {
@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<SegmentInfo> produce(@NotNull TextAttributes attribute,
@NotNull CharSequence text,
@NotNull EditorColorsScheme colorsScheme,
int start,
int end)
{
if (end <= start) {
return Collections.emptyList();
}
List<SegmentInfo> 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);
}
}
}
@@ -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;
}
}
@@ -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<T> implements Serializable {
@NotNull private final TIntObjectHashMap<T> myDataById = new TIntObjectHashMap<T>();
private transient TObjectIntHashMap<T> myIdsByData = new TObjectIntHashMap<T>();
@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();
}
}
@@ -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();
}
}
@@ -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<Color> {
}
@@ -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();
}
}
@@ -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<String> {
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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);
}
@@ -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);
}
@@ -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<OutputInfo> 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<OutputInfo> 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<OutputInfo> 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);
}
}
@@ -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);
}
}
@@ -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<RichCopySettings>, 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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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 '}'
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
@@ -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
@@ -880,6 +880,10 @@
<lang.foldingBuilder language="TEXT" implementationClass="com.intellij.ide.highlighter.custom.impl.CustomFileTypeFoldingBuilder"/>
<virtualFileSystem key="scratchpad" implementationClass="com.intellij.ide.scratch.ScratchpadFileSystem"/>
<copyPastePostProcessor implementation="com.intellij.openapi.editor.richcopy.TextWithMarkupProcessor"/>
<copyPastePostProcessor implementation="com.intellij.openapi.editor.richcopy.RtfCopyPasteProcessor"/>
<copyPastePostProcessor implementation="com.intellij.openapi.editor.richcopy.HtmlCopyPasteProcessor"/>
</extensions>
</idea-plugin>
@@ -37,6 +37,10 @@
<component>
<implementation-class>com.intellij.codeInsight.preview.ImageOrColorPreviewManager</implementation-class>
</component>
<component>
<implementation-class>com.intellij.openapi.editor.richcopy.settings.RichCopySettings</implementation-class>
</component>
</application-components>
<project-components>
@@ -0,0 +1,3 @@
<div style="border:1px inset;padding:2%;"><pre style="margin:0;padding:6px;background-color:#ffffff;font-family:'Monospaced';font-size:12;" bgcolor="#ffffff"> <font color="#000080"><b><span style="color:#000080;font-weight:bold;">int </span></b></font><font color="#000000"><span style="color:#000000;">field;</span></font><font color="#000000"><span style="color:#000000;">
</span></font><font color="#000000"><span style="color:#000000;">
</span></font><font color="#000000"><span style="color:#000000;"> </span></font><font color="#000080"><b><span style="color:#000080;font-weight:bold;">public static void </span></b></font><font color="#000000"><span style="color:#000000;">main(String[] args) {</span></font></pre></div>
@@ -0,0 +1,7 @@
public class Basic {
<selection> int field;
public static void main(String[] args) {</selection>
System.out.println("Hello\tworld!");
}
}
@@ -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}
@@ -0,0 +1,8 @@
<div style="border:1px inset;padding:2%;"><pre style="margin:0;padding:6px;background-color:#ffffff;font-family:'Monospaced';font-size:12;" bgcolor="#ffffff"><font color="#000080"><b><span style="color:#000080;font-weight:bold;">public class </span></b></font><font color="#000000"><span style="color:#000000;">Basic {
</span></font><font color="#000000"><span style="color:#000000;"> </span></font><font color="#000080"><b><span style="color:#000080;font-weight:bold;">int </span></b></font><font color="#000000"><span style="color:#000000;">field;
</span></font><font color="#000000"><span style="color:#000000;">
</span></font><font color="#000000"><span style="color:#000000;"> </span></font><font color="#000080"><b><span style="color:#000080;font-weight:bold;">public static void </span></b></font><font color="#000000"><span style="color:#000000;">main(String[] args) {
</span></font><font color="#000000"><span style="color:#000000;"> System.out.println(</span></font><font color="#008000"><b><span style="color:#008000;font-weight:bold;">"Hello</span></b></font><font color="#000080"><b><span style="color:#000080;font-weight:bold;">\t</span></b></font><font color="#008000"><b><span style="color:#008000;font-weight:bold;">world!"</span></b></font><font color="#000000"><span style="color:#000000;">);
</span></font><font color="#000000"><span style="color:#000000;"> }
</span></font><font color="#000000"><span style="color:#000000;">}
</span></font></pre></div>
@@ -0,0 +1,8 @@
<selection>public class Basic {
int field;
public static void main(String[] args) {
System.out.println("Hello\tworld!");
}
}
</selection>
@@ -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}
@@ -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/";
}
}
@@ -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<OutputInfo> 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\" #> <selection>template.statusList.sortBy</selection>(_.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" +
"<selection>/**\n" +
" * Code in <code>here</code>\n" +
" * <strong>Hi</strong> man\n" +
" * @param <T>\n" +
" </selection>*/\n" +
"public interface SampleTest<T> 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" +
"<selection> int field;\n" +
"}</selection>");
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" +
"<selection> int field;\n" +
"</selection>}");
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> syntaxInfo = new Ref<SyntaxInfo>();
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;
}
}
@@ -4327,6 +4327,13 @@
<option name="wraps" hit="Use soft wraps in editor" />
<option name="space" hit="Virtual Space" />
<option name="virtual" hit="Virtual Space" />
<option name="rich" hit="Rich-text copy"/>
<option name="copy" hit="Rich-text copy"/>
<option name="color" hit="Color scheme"/>
<option name="scheme" hit="Color scheme"/>
<option name="strip" hit="Strip common indent"/>
<option name="common" hit="Strip common indent"/>
<option name="indent" hit="Strip common indent"/>
</configurable>
<configurable id="vcs.Subversion" configurable_name="Subversion">
<option name="" path="General" hit="" />