/* * Copyright 2000-2009 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 org.community.intellij.plugins.communitycase.ui; import com.intellij.codeStyle.CodeStyleFacade; import com.intellij.ide.presentation.VirtualFilePresentation; import com.intellij.openapi.fileEditor.impl.LoadTextUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.*; import com.intellij.util.Processor; import com.intellij.util.containers.HashMap; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import org.community.intellij.plugins.communitycase.Util; import org.community.intellij.plugins.communitycase.Vcs; import org.community.intellij.plugins.communitycase.commands.Command; import org.community.intellij.plugins.communitycase.commands.SimpleHandler; import org.community.intellij.plugins.communitycase.commands.StringScanner; import org.community.intellij.plugins.communitycase.config.VcsSettings; import org.community.intellij.plugins.communitycase.config.Version; import org.community.intellij.plugins.communitycase.i18n.Bundle; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.*; import java.util.List; /** * This dialog allows converting the specified files before committing them. */ public class ConvertFilesDialog extends DialogWrapper { /** * The version when option --stdin was added */ private static final Version CHECK_ATTR_STDIN_SUPPORTED = new Version(1, 6, 1, 0); /** * Do not convert exit code */ public static final int DO_NOT_CONVERT = NEXT_USER_EXIT_CODE; /** * The checkbox used to indicate that dialog should not be shown */ private JCheckBox myDoNotShowCheckBox; /** * The root panel of the dialog */ private JPanel myRootPanel; /** * The tree of files to convert */ private CheckboxTreeBase myFilesToConvert; /** * The root node in the tree */ private CheckedTreeNode myRootNode; /** * The constructor * * @param project the project to which this dialog is related * @param filesToShow the files to show sorted by vcs root */ ConvertFilesDialog(Project project, Map<VirtualFile, Set<VirtualFile>> filesToShow) { super(project, true); ArrayList<VirtualFile> roots = new ArrayList<VirtualFile>(filesToShow.keySet()); Collections.sort(roots, Util.VIRTUAL_FILE_COMPARATOR); for (VirtualFile root : roots) { CheckedTreeNode vcsRoot = new CheckedTreeNode(root); myRootNode.add(vcsRoot); ArrayList<VirtualFile> files = new ArrayList<VirtualFile>(filesToShow.get(root)); Collections.sort(files, Util.VIRTUAL_FILE_COMPARATOR); for (VirtualFile file : files) { vcsRoot.add(new CheckedTreeNode(file)); } } TreeUtil.expandAll(myFilesToConvert); setTitle(Bundle.getString("crlf.convert.title")); setOKButtonText(Bundle.getString("crlf.convert.convert")); init(); } /** * {@inheritDoc} */ @Override protected Action[] createActions() { return new Action[]{getOKAction(), new DoNotConvertAction(), getCancelAction()}; } /** * Create custom UI components */ private void createUIComponents() { myRootNode = new CheckedTreeNode("ROOT"); myFilesToConvert = new CheckboxTree(new FileTreeCellRenderer(), myRootNode) { protected void onNodeStateChanged(CheckedTreeNode node) { VirtualFile[] files = myFilesToConvert.getCheckedNodes(VirtualFile.class, null); setOKActionEnabled(files != null && files.length > 0); super.onNodeStateChanged(node); } }; } /** * {@inheritDoc} */ @Override protected JComponent createCenterPanel() { return myRootPanel; } /** * {@inheritDoc} */ @Override protected String getDimensionServiceKey() { return getClass().getName(); } /** * Check if files need to be converted to other line separator. The method could be invoked from non-UI thread. * * @param project the project to use * @param settings the vcs settings * @param sortedChanges sorted changes * @param exceptions the collection with exceptions * @return true if conversion completed successfully, false if process was cancelled or there were errors */ public static boolean showDialogIfNeeded(final Project project, final VcsSettings settings, Map<VirtualFile, List<Change>> sortedChanges, final List<VcsException> exceptions) { try { if (settings.getAskBeforeLineSeparatorConversion() || settings.getLineSeparatorsConversion() == VcsSettings.ConversionPolicy.PROJECT_LINE_SEPARATORS) { LocalFileSystem lfs = LocalFileSystem.getInstance(); final String nl = CodeStyleFacade.getInstance(project).getLineSeparator(); final Map<VirtualFile, Set<VirtualFile>> files = new HashMap<VirtualFile, Set<VirtualFile>>(); // preliminary screening of files for (Map.Entry<VirtualFile, List<Change>> entry : sortedChanges.entrySet()) { final VirtualFile root = entry.getKey(); final Set<VirtualFile> added = new HashSet<VirtualFile>(); for (Change change : entry.getValue()) { switch (change.getType()) { case NEW: case MODIFICATION: case MOVED: VirtualFile f = lfs.findFileByPath(change.getAfterRevision().getFile().getPath()); if (f != null && !f.getFileType().isBinary() && !nl.equals(LoadTextUtil.detectLineSeparator(f, false))) { added.add(f); } break; case DELETED: } } if (!added.isEmpty()) { files.put(root, added); } } // ignore files with CRLF unset ignoreFilesWithCrlfUnset(project, files); // check crlf for real for (Iterator<Map.Entry<VirtualFile, Set<VirtualFile>>> i = files.entrySet().iterator(); i.hasNext();) { Map.Entry<VirtualFile, Set<VirtualFile>> e = i.next(); Set<VirtualFile> fs = e.getValue(); for (Iterator<VirtualFile> j = fs.iterator(); j.hasNext();) { VirtualFile f = j.next(); String detectedLineSeparator = LoadTextUtil.detectLineSeparator(f, true); if (detectedLineSeparator == null || nl.equals(detectedLineSeparator)) { j.remove(); } } if (fs.isEmpty()) { i.remove(); } } if (files.isEmpty()) { return true; } UIUtil.invokeAndWaitIfNeeded(new Runnable() { public void run() { VirtualFile[] selectedFiles = null; if (settings.getAskBeforeLineSeparatorConversion()) { ConvertFilesDialog d = new ConvertFilesDialog(project, files); d.show(); if (d.isOK()) { settings.setAskBeforeLineSeparatorConversion(!d.myDoNotShowCheckBox.isSelected()); settings.setLineSeparatorsConversion(VcsSettings.ConversionPolicy.PROJECT_LINE_SEPARATORS); selectedFiles = d.myFilesToConvert.getCheckedNodes(VirtualFile.class, null); } else if (d.getExitCode() == DO_NOT_CONVERT) { settings.setAskBeforeLineSeparatorConversion(!d.myDoNotShowCheckBox.isSelected()); settings.setLineSeparatorsConversion(VcsSettings.ConversionPolicy.NONE); } else { //noinspection ThrowableInstanceNeverThrown exceptions.add(new VcsException("Commit was cancelled in file conversion dialog")); } } else { ArrayList<VirtualFile> fileList = new ArrayList<VirtualFile>(); for (Set<VirtualFile> fileSet : files.values()) { fileList.addAll(fileSet); } selectedFiles = VfsUtil.toVirtualFileArray(fileList); } if (selectedFiles != null) { for (VirtualFile f : selectedFiles) { if (f == null) { continue; } try { LoadTextUtil.changeLineSeparator(project, ConvertFilesDialog.class.getName(), f, nl); } catch (IOException e) { //noinspection ThrowableInstanceNeverThrown exceptions.add(new VcsException("Failed to change line separators for the file: " + f.getPresentableUrl(), e)); } } } } }); } } catch (VcsException e) { exceptions.add(e); } return exceptions.isEmpty(); } /** * Remove files that have -crlf attribute specified * * @param project the context project * @param files the files to check (map from vcs roots to the set of files under root) * @throws VcsException if there is problem with running */ private static void ignoreFilesWithCrlfUnset(Project project, Map<VirtualFile, Set<VirtualFile>> files) throws VcsException { boolean stdin = CHECK_ATTR_STDIN_SUPPORTED.isLessOrEqual(Vcs.getInstance(project).version()); for (final Map.Entry<VirtualFile, Set<VirtualFile>> e : files.entrySet()) { final VirtualFile r = e.getKey(); SimpleHandler h = new SimpleHandler(project, r, Command.CHECK_ATTR); if (stdin) { h.addParameters("--stdin", "-z"); } h.addParameters("crlf"); h.setSilent(true); h.setRemote(true); final HashMap<String, VirtualFile> filesToCheck = new HashMap<String, VirtualFile>(); Set<VirtualFile> fileSet = e.getValue(); for (VirtualFile file : fileSet) { filesToCheck.put(Util.relativePath(r, file), file); } if (stdin) { h.setInputProcessor(new Processor<OutputStream>() { public boolean process(OutputStream outputStream) { try { OutputStreamWriter out = new OutputStreamWriter(outputStream, Util.UTF8_CHARSET); try { for (String file : filesToCheck.keySet()) { out.write(file); out.write("\u0000"); } } finally { out.close(); } } catch (IOException ex) { try { outputStream.close(); } catch (IOException ioe) { // ignore exception } } return true; } }); } else { h.endOptions(); h.addRelativeFiles(filesToCheck.values()); } StringScanner output = new StringScanner(h.run()); String unsetIndicator = ": crlf: unset"; while (output.hasMoreData()) { String l = output.line(); if (l.endsWith(unsetIndicator)) { fileSet.remove(filesToCheck.get(Util.unescapePath(l.substring(0, l.length() - unsetIndicator.length())))); } } } } /** * Action used to indicate that no conversion should be performed */ class DoNotConvertAction extends AbstractAction { private static final long serialVersionUID = 1931383640152023206L; /** * The constructor */ DoNotConvertAction() { putValue(NAME, Bundle.getString("crlf.convert.leave")); putValue(DEFAULT_ACTION, Boolean.FALSE); } /** * {@inheritDoc} */ public void actionPerformed(ActionEvent e) { if (myPerformAction) return; try { myPerformAction = true; close(DO_NOT_CONVERT); } finally { myPerformAction = false; } } } /** * The cell renderer for the tree */ static class FileTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer { /** * {@inheritDoc} */ @Override public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { // Fix GTK background if (UIUtil.isUnderGTKLookAndFeel()){ final Color background = selected ? UIUtil.getTreeSelectionBackground() : UIUtil.getTreeTextBackground(); UIUtil.changeBackGround(this, background); } ColoredTreeCellRenderer r = getTextRenderer(); if (!(value instanceof CheckedTreeNode)) { // unknown node type renderUnknown(r, value); return; } CheckedTreeNode node = (CheckedTreeNode)value; if (!(node.getUserObject() instanceof VirtualFile)) { // unknown node type renderUnknown(r, node.getUserObject()); return; } VirtualFile file = (VirtualFile)node.getUserObject(); if (leaf) { VirtualFile parent = (VirtualFile)((CheckedTreeNode)node.getParent()).getUserObject(); // the real file Icon i = VirtualFilePresentation.getIcon(file); if (i != null) { r.setIcon(i); } r.append(Util.getRelativeFilePath(file, parent), SimpleTextAttributes.REGULAR_ATTRIBUTES, true); } else { // the vcs root node r.append(file.getPresentableUrl(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES, true); } } /** * Render unknown node * * @param r a renderer to use * @param value the unknown value */ private static void renderUnknown(ColoredTreeCellRenderer r, Object value) { r.append("UNSUPPORTED NODE TYPE: " + (value == null ? "null" : value.getClass().getName()), SimpleTextAttributes.ERROR_ATTRIBUTES); } } }