/*
* Copyright 2000-2015 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.impl;
import com.intellij.ide.DataManager;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.ShutDownTracker;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.util.ArrayUtil;
import com.intellij.util.text.CharArrayUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public final class TrailingSpacesStripper extends FileDocumentManagerAdapter {
public static final Key<String> OVERRIDE_STRIP_TRAILING_SPACES_KEY = Key.create("OVERRIDE_TRIM_TRAILING_SPACES_KEY");
public static final Key<Boolean> OVERRIDE_ENSURE_NEWLINE_KEY = Key.create("OVERRIDE_ENSURE_NEWLINE_KEY");
private static final Key<Boolean> DISABLE_FOR_FILE_KEY = Key.create("DISABLE_TRAILING_SPACE_STRIPPER_FOR_FILE_KEY");
private final Set<Document> myDocumentsToStripLater = new THashSet<>();
@Override
public void beforeAllDocumentsSaving() {
Set<Document> documentsToStrip = new THashSet<>(myDocumentsToStripLater);
myDocumentsToStripLater.clear();
for (Document document : documentsToStrip) {
strip(document);
}
}
@Override
public void beforeDocumentSaving(@NotNull Document document) {
strip(document);
}
private void strip(@NotNull final Document document) {
if (!document.isWritable()) return;
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
VirtualFile file = fileDocumentManager.getFile(document);
if (file == null || !file.isValid() || Boolean.TRUE.equals(DISABLE_FOR_FILE_KEY.get(file))) return;
final EditorSettingsExternalizable settings = EditorSettingsExternalizable.getInstance();
if (settings == null) return;
final String overrideStripTrailingSpacesData = file.getUserData(OVERRIDE_STRIP_TRAILING_SPACES_KEY);
final Boolean overrideEnsureNewlineData = file.getUserData(OVERRIDE_ENSURE_NEWLINE_KEY);
@EditorSettingsExternalizable.StripTrailingSpaces
String stripTrailingSpaces = overrideStripTrailingSpacesData != null ? overrideStripTrailingSpacesData : settings.getStripTrailingSpaces();
final boolean doStrip = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_NONE);
final boolean ensureEOL = overrideEnsureNewlineData != null ? overrideEnsureNewlineData.booleanValue() : settings.isEnsureNewLineAtEOF();
if (doStrip) {
final boolean inChangedLinesOnly = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_WHOLE);
boolean success = strip(document, inChangedLinesOnly, settings.isKeepTrailingSpacesOnCaretLine());
if (!success) {
myDocumentsToStripLater.add(document);
}
}
final int lines = document.getLineCount();
if (ensureEOL && lines > 0) {
final int start = document.getLineStartOffset(lines - 1);
final int end = document.getLineEndOffset(lines - 1);
if (start != end) {
final CharSequence content = document.getCharsSequence();
ApplicationManager.getApplication().runWriteAction(new DocumentRunnable(document, null) {
@Override
public void run() {
CommandProcessor.getInstance().runUndoTransparentAction(() -> {
if (CharArrayUtil.containsOnlyWhiteSpaces(content.subSequence(start, end)) && doStrip) {
document.deleteString(start, end);
}
else {
document.insertString(end, "\n");
}
});
}
});
}
}
}
// clears line modification flags except lines which was not stripped because the caret was in the way
public void clearLineModificationFlags(@NotNull Document document) {
if (document instanceof DocumentWindow) {
document = ((DocumentWindow)document).getDelegate();
}
if (!(document instanceof DocumentImpl)) {
return;
}
Editor activeEditor = getActiveEditor(document);
// when virtual space enabled, we can strip whitespace anywhere
boolean isVirtualSpaceEnabled = activeEditor == null || activeEditor.getSettings().isVirtualSpace();
final EditorSettingsExternalizable settings = EditorSettingsExternalizable.getInstance();
if (settings == null) return;
boolean enabled = !Boolean.TRUE.equals(DISABLE_FOR_FILE_KEY.get(FileDocumentManager.getInstance().getFile(document)));
if (!enabled) return;
String stripTrailingSpaces = settings.getStripTrailingSpaces();
final boolean doStrip = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_NONE);
final boolean inChangedLinesOnly = !stripTrailingSpaces.equals(EditorSettingsExternalizable.STRIP_TRAILING_SPACES_WHOLE);
int[] caretLines;
if (activeEditor != null && inChangedLinesOnly && doStrip && !isVirtualSpaceEnabled) {
List<Caret> carets = activeEditor.getCaretModel().getAllCarets();
caretLines = new int[carets.size()];
for (int i = 0; i < carets.size(); i++) {
Caret caret = carets.get(i);
caretLines[i] = caret.getLogicalPosition().line;
}
}
else {
caretLines = ArrayUtil.EMPTY_INT_ARRAY;
}
((DocumentImpl)document).clearLineModificationFlagsExcept(caretLines);
}
private static Editor getActiveEditor(@NotNull Document document) {
Component focusOwner = IdeFocusManager.getGlobalInstance().getFocusOwner();
DataContext dataContext = DataManager.getInstance().getDataContext(focusOwner);
boolean isDisposeInProgress = ApplicationManager.getApplication().isDisposeInProgress(); // ignore caret placing when exiting
Editor activeEditor = isDisposeInProgress ? null : CommonDataKeys.EDITOR.getData(dataContext);
if (activeEditor != null && activeEditor.getDocument() != document) {
activeEditor = null;
}
return activeEditor;
}
public static boolean strip(@NotNull Document document, boolean inChangedLinesOnly, boolean skipCaretLines) {
if (document instanceof DocumentWindow) {
document = ((DocumentWindow)document).getDelegate();
}
if (!(document instanceof DocumentImpl)) {
return true;
}
Editor activeEditor = getActiveEditor(document);
final List<Caret> carets = activeEditor == null ? Collections.emptyList() : activeEditor.getCaretModel().getAllCarets();
final List<VisualPosition> visualCarets = new ArrayList<>(carets.size());
int[] caretOffsets = new int[carets.size()];
for (int i = 0; i < carets.size(); i++) {
Caret caret = carets.get(i);
visualCarets.add(caret.getVisualPosition());
caretOffsets[i] = caret.getOffset();
}
boolean markAsNeedsStrippingLater =
((DocumentImpl)document).stripTrailingSpaces(getProject(document, activeEditor),
inChangedLinesOnly, skipCaretLines, caretOffsets);
if (activeEditor != null && !ShutDownTracker.isShutdownHookRunning()) {
activeEditor.getCaretModel().runBatchCaretOperation(() -> {
for (int i = 0; i < carets.size(); i++) {
Caret caret = carets.get(i);
if (caret.isValid()) {
caret.moveToVisualPosition(visualCarets.get(i));
}
}
});
}
return !markAsNeedsStrippingLater;
}
@Nullable
private static Project getProject(@NotNull Document document, @Nullable Editor editor) {
if (editor != null) return editor.getProject();
VirtualFile file = FileDocumentManager.getInstance().getFile(document);
if (file != null) {
return ProjectUtil.guessProjectForFile(file);
}
return null;
}
public void documentDeleted(@NotNull Document doc) {
myDocumentsToStripLater.remove(doc);
}
@Override
public void unsavedDocumentsDropped() {
myDocumentsToStripLater.clear();
}
public static void setEnabled(@NotNull VirtualFile file, boolean enabled) {
DISABLE_FOR_FILE_KEY.set(file, enabled ? null : Boolean.TRUE);
}
public static boolean isEnabled(@NotNull VirtualFile file) {
return !Boolean.TRUE.equals(DISABLE_FOR_FILE_KEY.get(file));
}
}