/* * 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.jetbrains.lang.dart.assists; import com.google.common.collect.Maps; import com.intellij.codeInsight.FileModificationService; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.codeInsight.lookup.LookupElementPresentation; import com.intellij.codeInsight.lookup.LookupElementRenderer; import com.intellij.codeInsight.template.*; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.ScrollType; import com.intellij.openapi.fileEditor.*; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.util.PlatformIcons; import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService; import org.dartlang.analysis.server.protocol.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; public class AssistUtils { /** * @return <code>true</code> if file contents changed, <code>false</code> otherwise */ public static boolean applyFileEdit(@NotNull final Project project, @NotNull final SourceFileEdit fileEdit) { final VirtualFile file = findVirtualFile(fileEdit); final Document document = file == null ? null : FileDocumentManager.getInstance().getDocument(file); if (document == null) return false; final long initialModStamp = document.getModificationStamp(); applySourceEdits(project, file, document, fileEdit.getEdits(), Collections.emptySet()); return document.getModificationStamp() != initialModStamp; } public static void applySourceChange(@NotNull final Project project, @NotNull final SourceChange sourceChange, final boolean withLinkedEdits) throws DartSourceEditException { applySourceChange(project, sourceChange, withLinkedEdits, Collections.emptySet()); } public static void applySourceChange(@NotNull final Project project, @NotNull final SourceChange sourceChange, final boolean withLinkedEdits, @NotNull final Set<String> excludedIds) throws DartSourceEditException { final Map<VirtualFile, SourceFileEdit> changeMap = getContentFilesChanges(project, sourceChange, excludedIds); // ensure not read-only { final Set<VirtualFile> files = changeMap.keySet(); final boolean okToWrite = FileModificationService.getInstance().prepareVirtualFilesForWrite(project, files); if (!okToWrite) { return; } } // do apply the change CommandProcessor.getInstance().executeCommand(project, () -> { final ChangeTarget linkedEditTarget = withLinkedEdits ? findChangeTarget(project, sourceChange) : null; List<SourceEditInfo> sourceEditInfos = null; for (Map.Entry<VirtualFile, SourceFileEdit> entry : changeMap.entrySet()) { final VirtualFile file = entry.getKey(); final SourceFileEdit fileEdit = entry.getValue(); final Document document = FileDocumentManager.getInstance().getDocument(file); if (document != null) { final List<SourceEditInfo> infos = applySourceEdits(project, file, document, fileEdit.getEdits(), excludedIds); if (linkedEditTarget != null && linkedEditTarget.virtualFile.equals(file)) { sourceEditInfos = infos; } } } if (withLinkedEdits && sourceEditInfos != null) { runLinkedEdits(project, sourceChange, linkedEditTarget, sourceEditInfos); } }, sourceChange.getMessage(), null); } public static List<SourceEditInfo> applySourceEdits(@NotNull final Project project, @NotNull final VirtualFile file, @NotNull final Document document, @NotNull final List<SourceEdit> edits, @NotNull final Set<String> excludedIds) { final List<SourceEditInfo> result = new ArrayList<>(edits.size()); final DartAnalysisServerService service = DartAnalysisServerService.getInstance(project); for (SourceEdit edit : edits) { if (excludedIds.contains(edit.getId())) { continue; } final int offset = service.getConvertedOffset(file, edit.getOffset()); final int length = service.getConvertedOffset(file, edit.getOffset() + edit.getLength()) - offset; final String replacement = StringUtil.convertLineSeparators(edit.getReplacement()); for (SourceEditInfo info : result) { if (info.resultingOriginalOffset > edit.getOffset()) { info.resultingOriginalOffset -= edit.getLength(); info.resultingOriginalOffset += edit.getReplacement().length(); info.resultingConvertedOffset -= length; info.resultingConvertedOffset += replacement.length(); } } result.add(new SourceEditInfo(edit.getOffset(), offset, edit.getLength(), length, edit.getReplacement(), replacement)); if (length != replacement.length() || !replacement.equals(document.getText(TextRange.create(offset, offset + length)))) { document.replaceString(offset, offset + length, replacement); } } return result; } @NotNull public static Map<VirtualFile, SourceFileEdit> getContentFilesChanges(@NotNull final Project project, @NotNull final SourceChange sourceChange, @NotNull final Set<String> excludedIds) throws DartSourceEditException { final Map<VirtualFile, SourceFileEdit> map = Maps.newHashMap(); final List<SourceFileEdit> fileEdits = sourceChange.getEdits(); for (SourceFileEdit fileEdit : fileEdits) { boolean allEditsExcluded = true; for (SourceEdit edit : fileEdit.getEdits()) { if (!excludedIds.contains(edit.getId())) { allEditsExcluded = false; break; } } if (allEditsExcluded) { continue; } final VirtualFile file = findVirtualFile(fileEdit); if (file == null) { throw new DartSourceEditException("Failed to edit file, file not found: " + fileEdit.getFile()); } if (!isInContent(project, file)) { throw new DartSourceEditException("Can't edit file outside of the project content: " + fileEdit.getFile()); } map.put(file, fileEdit); } return map; } @Nullable private static ChangeTarget findChangeTarget(@NotNull final Project project, @NotNull final SourceChange sourceChange) { for (LinkedEditGroup group : sourceChange.getLinkedEditGroups()) { final List<Position> positions = group.getPositions(); if (!positions.isEmpty()) { final Position position = positions.get(0); // find VirtualFile VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(position.getFile())); if (virtualFile == null) { return null; } // find PsiFile final PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile); if (psiFile == null) { return null; } return new ChangeTarget(project, virtualFile, psiFile, position.getOffset()); } } return null; } private static int getLinkedEditConvertedOffset(@NotNull final Project project, @NotNull final VirtualFile file, final int linkedEditOffset, @NotNull final List<SourceEditInfo> editInfos) { // first check if linkedEditOffset is inside of some SourceEdit for (SourceEditInfo info : editInfos) { if (linkedEditOffset >= info.resultingOriginalOffset && linkedEditOffset <= info.resultingOriginalOffset + info.originalReplacement.length()) { final String substring = info.originalReplacement.substring(0, linkedEditOffset - info.resultingOriginalOffset); final int crlfCount = StringUtil.getOccurrenceCount(substring, "\r\n"); return info.resultingConvertedOffset + linkedEditOffset - info.resultingOriginalOffset - crlfCount; } } // if we are here, it means that linkedEditOffset is outside of all SourceEdits int leOffset = linkedEditOffset; // 1. find offset before any SourceEdits applied for (int i = editInfos.size() - 1; i >= 0; i--) { final SourceEditInfo info = editInfos.get(i); if (linkedEditOffset >= info.originalOffset) { leOffset -= info.originalReplacement.length(); leOffset += info.originalLength; } } // 2. convert offset leOffset = DartAnalysisServerService.getInstance(project).getConvertedOffset(file, leOffset); // 3. find offset after all SourceEdits applied for (SourceEditInfo info : editInfos) { if (leOffset >= info.convertedOffset) { leOffset -= info.convertedLength; leOffset += info.normalizedReplacement.length(); } } return leOffset; } @Nullable public static VirtualFile findVirtualFile(@NotNull final SourceFileEdit fileEdit) { return LocalFileSystem.getInstance().findFileByPath(FileUtil.toSystemIndependentName(fileEdit.getFile())); } private static boolean isInContent(@NotNull Project project, @NotNull VirtualFile file) { return ProjectRootManager.getInstance(project).getFileIndex().isInContent(file); } @Nullable public static Editor navigate(@NotNull final Project project, @NotNull final VirtualFile file, final int offset) { final OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, offset); descriptor.setScrollType(ScrollType.MAKE_VISIBLE); descriptor.navigate(true); final FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(file); return fileEditor instanceof TextEditor ? ((TextEditor)fileEditor).getEditor() : null; } private static void runLinkedEdits(@NotNull final Project project, @NotNull final SourceChange sourceChange, @NotNull final ChangeTarget target, @NotNull final List<SourceEditInfo> sourceEditInfos) { final int caretOffset = getLinkedEditConvertedOffset(project, target.virtualFile, target.originalOffset, sourceEditInfos); final Editor editor = navigate(project, target.virtualFile, caretOffset); if (editor == null) { return; } // Commit changes, otherwise TemplateBuilderImpl#buildTemplate() sees the old psiFile text. PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument()); final TemplateBuilderImpl builder = (TemplateBuilderImpl)TemplateBuilderFactory.getInstance().createTemplateBuilder(target.psiFile); boolean hasTextRanges = false; // fill the builder with ranges int groupIndex = 0; for (LinkedEditGroup group : sourceChange.getLinkedEditGroups()) { String mainVar = "group_" + groupIndex++; boolean firstPosition = true; groupIndex++; for (Position position : group.getPositions()) { if (FileUtil.toSystemIndependentName(position.getFile()).equals(target.virtualFile.getPath())) { hasTextRanges = true; final int offset = getLinkedEditConvertedOffset(project, target.virtualFile, position.getOffset(), sourceEditInfos); final int end = offset + group.getLength(); final TextRange range = new TextRange(offset, end); if (firstPosition) { firstPosition = false; final String text = editor.getDocument().getText(range); DartLookupExpression expression = new DartLookupExpression(text, group.getSuggestions()); builder.replaceRange(range, mainVar, expression, true); } else { final String positionVar = mainVar + "_" + offset; builder.replaceElement(range, positionVar, mainVar, false); } } } } // run the template if (hasTextRanges) { builder.run(editor, true); } } private static class SourceEditInfo { private final int originalOffset; private int resultingOriginalOffset; private final int convertedOffset; private int resultingConvertedOffset; private final int originalLength; private final int convertedLength; private final String originalReplacement; private final String normalizedReplacement; public SourceEditInfo(int originalOffset, int convertedOffset, int originalLength, int convertedLength, String originalReplacement, String normalizedReplacement) { this.originalOffset = originalOffset; resultingOriginalOffset = originalOffset; this.convertedOffset = convertedOffset; resultingConvertedOffset = convertedOffset; this.originalLength = originalLength; this.convertedLength = convertedLength; this.originalReplacement = originalReplacement; this.normalizedReplacement = normalizedReplacement; } } } class DartLookupExpression extends Expression { private final @NotNull String myText; private final @NotNull List<LinkedEditSuggestion> mySuggestions; DartLookupExpression(@NotNull String text, @NotNull List<LinkedEditSuggestion> suggestions) { myText = text; mySuggestions = suggestions; } @Nullable @Override public LookupElement[] calculateLookupItems(final ExpressionContext context) { final int length = mySuggestions.size(); final LookupElement[] elements = new LookupElement[length]; for (int i = 0; i < length; i++) { final LinkedEditSuggestion suggestion = mySuggestions.get(i); final String value = suggestion.getValue(); elements[i] = LookupElementBuilder.create(value).withRenderer(new LookupElementRenderer<LookupElement>() { @Override public void renderElement(final LookupElement element, final LookupElementPresentation presentation) { final Icon icon = getIcon(suggestion.getKind()); presentation.setIcon(icon); presentation.setItemText(value); } }); } return elements; } @Override public Result calculateQuickResult(ExpressionContext context) { return new TextResult(myText); } @Override public Result calculateResult(ExpressionContext context) { return calculateQuickResult(context); } private static Icon getIcon(String suggestionKind) { if (LinkedEditSuggestionKind.METHOD.equals(suggestionKind)) { return PlatformIcons.METHOD_ICON; } if (LinkedEditSuggestionKind.PARAMETER.equals(suggestionKind)) { return PlatformIcons.PARAMETER_ICON; } if (LinkedEditSuggestionKind.TYPE.equals(suggestionKind)) { return PlatformIcons.CLASS_ICON; } if (LinkedEditSuggestionKind.VARIABLE.equals(suggestionKind)) { return PlatformIcons.VARIABLE_ICON; } return null; } } class ChangeTarget { @NotNull final Project project; @NotNull final VirtualFile virtualFile; @NotNull final PsiFile psiFile; final int originalOffset; ChangeTarget(@NotNull final Project project, @NotNull final VirtualFile virtualFile, @NotNull final PsiFile psiFile, final int originalOffset) { this.project = project; this.originalOffset = originalOffset; this.virtualFile = virtualFile; this.psiFile = psiFile; } }