4cfd88f3035fe28a24974b7bd3a54722700cac9f
[idea/community.git] / platform / diff-impl / src / com / intellij / diff / DiffContentFactoryImpl.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.diff;
17
18 import com.intellij.diff.actions.DocumentFragmentContent;
19 import com.intellij.diff.contents.*;
20 import com.intellij.diff.tools.util.DiffNotifications;
21 import com.intellij.diff.util.DiffUserDataKeysEx;
22 import com.intellij.diff.util.DiffUtil;
23 import com.intellij.ide.highlighter.ArchiveFileType;
24 import com.intellij.openapi.application.ReadAction;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.editor.Document;
27 import com.intellij.openapi.editor.EditorFactory;
28 import com.intellij.openapi.fileEditor.FileDocumentManager;
29 import com.intellij.openapi.fileTypes.BinaryFileTypeDecompilers;
30 import com.intellij.openapi.fileTypes.FileType;
31 import com.intellij.openapi.fileTypes.FileTypes;
32 import com.intellij.openapi.fileTypes.PlainTextFileType;
33 import com.intellij.openapi.ide.CopyPasteManager;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.util.TextRange;
36 import com.intellij.openapi.util.io.FileUtil;
37 import com.intellij.openapi.util.registry.Registry;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.vcs.FilePath;
40 import com.intellij.openapi.vfs.CharsetToolkit;
41 import com.intellij.openapi.vfs.VfsUtil;
42 import com.intellij.openapi.vfs.VirtualFile;
43 import com.intellij.psi.PsiDocumentManager;
44 import com.intellij.testFramework.BinaryLightVirtualFile;
45 import com.intellij.testFramework.LightVirtualFile;
46 import com.intellij.ui.LightColors;
47 import com.intellij.util.LineSeparator;
48 import com.intellij.util.PathUtil;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import java.awt.datatransfer.DataFlavor;
53 import java.io.File;
54 import java.io.IOException;
55 import java.nio.charset.CharacterCodingException;
56 import java.nio.charset.Charset;
57
58 public class DiffContentFactoryImpl extends DiffContentFactoryEx {
59   public static final Logger LOG = Logger.getInstance(DiffContentFactoryImpl.class);
60
61   @NotNull
62   @Override
63   public EmptyContent createEmpty() {
64     return new EmptyContent();
65   }
66
67
68   @NotNull
69   @Override
70   public DocumentContent create(@NotNull String text) {
71     return create(null, text);
72   }
73
74   @NotNull
75   @Override
76   public DocumentContent create(@NotNull String text, @Nullable FileType type) {
77     return create(null, text, type);
78   }
79
80   @NotNull
81   @Override
82   public DocumentContent create(@NotNull String text, @Nullable FileType type, boolean respectLineSeparators) {
83     return create(null, text, type, respectLineSeparators);
84   }
85
86   @NotNull
87   @Override
88   public DocumentContent create(@NotNull String text, @Nullable VirtualFile highlightFile) {
89     return create(null, text, highlightFile);
90   }
91
92   @NotNull
93   @Override
94   public DocumentContent create(@NotNull String text, @Nullable DocumentContent referent) {
95     return create(null, text, referent);
96   }
97
98
99   @NotNull
100   @Override
101   public DocumentContent create(@Nullable Project project, @NotNull String text) {
102     return create(project, text, (FileType)null);
103   }
104
105   @NotNull
106   @Override
107   public DocumentContent create(@Nullable Project project, @NotNull String text, @Nullable FileType type) {
108     return create(project, text, type, true);
109   }
110
111   @NotNull
112   @Override
113   public DocumentContent create(@Nullable Project project, @NotNull String text, @Nullable FileType type, boolean respectLineSeparators) {
114     return createImpl(project, text, type, null, null, respectLineSeparators, true);
115   }
116
117   @NotNull
118   @Override
119   public DocumentContent create(@Nullable Project project, @NotNull String text, @NotNull FilePath filePath) {
120     return createImpl(project, text, filePath.getFileType(), filePath.getName(), filePath.getVirtualFile(), true, true);
121   }
122
123   @NotNull
124   @Override
125   public DocumentContent create(@Nullable Project project, @NotNull String text, @Nullable VirtualFile highlightFile) {
126     FileType fileType = highlightFile != null ? highlightFile.getFileType() : null;
127     String fileName = highlightFile != null ? highlightFile.getName() : null;
128     return createImpl(project, text, fileType, fileName, highlightFile, true, true);
129   }
130
131   @NotNull
132   @Override
133   public DocumentContent create(@Nullable Project project, @NotNull String text, @Nullable DocumentContent referent) {
134     if (referent == null) return create(text);
135     return createImpl(project, text, referent.getContentType(), null, referent.getHighlightFile(), false, true);
136   }
137
138
139   @NotNull
140   @Override
141   public DocumentContent create(@NotNull Document document, @Nullable DocumentContent referent) {
142     return create(null, document, referent);
143   }
144
145
146   @NotNull
147   @Override
148   public DocumentContent create(@Nullable Project project, @NotNull Document document) {
149     return create(project, document, (FileType)null);
150   }
151
152   @NotNull
153   @Override
154   public DocumentContent create(@Nullable Project project, @NotNull Document document, @Nullable FileType fileType) {
155     VirtualFile file = FileDocumentManager.getInstance().getFile(document);
156     if (file == null) return new DocumentContentImpl(project, document, fileType, null, null, null, null);
157     return create(project, document, file);
158   }
159
160   @NotNull
161   @Override
162   public DocumentContent create(@Nullable Project project, @NotNull Document document, @Nullable VirtualFile file) {
163     if (file != null) return new FileDocumentContentImpl(project, document, file);
164     return new DocumentContentImpl(document);
165   }
166
167   @NotNull
168   @Override
169   public DocumentContent create(@Nullable Project project, @NotNull Document document, @Nullable DocumentContent referent) {
170     if (referent == null) return new DocumentContentImpl(document);
171     return new DocumentContentImpl(project, document, referent.getContentType(), referent.getHighlightFile(), null, null, null);
172   }
173
174
175   @NotNull
176   @Override
177   public DiffContent create(@Nullable Project project, @NotNull VirtualFile file) {
178     if (file.isDirectory()) return new DirectoryContentImpl(project, file);
179     DocumentContent content = createDocument(project, file);
180     if (content != null) return content;
181     return new FileContentImpl(project, file);
182   }
183
184   @Nullable
185   @Override
186   public DocumentContent createDocument(@Nullable Project project, @NotNull final VirtualFile file) {
187     // TODO: add notification, that file is decompiled ?
188     if (file.isDirectory()) return null;
189     Document document = ReadAction.compute(() -> {
190       return FileDocumentManager.getInstance().getDocument(file);
191     });
192     if (document == null) return null;
193     return new FileDocumentContentImpl(project, document, file);
194   }
195
196   @Nullable
197   @Override
198   public FileContent createFile(@Nullable Project project, @NotNull VirtualFile file) {
199     if (file.isDirectory()) return null;
200     return (FileContent)create(project, file);
201   }
202
203
204   @NotNull
205   @Override
206   public DocumentContent createFragment(@Nullable Project project, @NotNull Document document, @NotNull TextRange range) {
207     DocumentContent content = create(project, document);
208     return new DocumentFragmentContent(project, content, range);
209   }
210
211   @NotNull
212   @Override
213   public DocumentContent createFragment(@Nullable Project project, @NotNull DocumentContent content, @NotNull TextRange range) {
214     return new DocumentFragmentContent(project, content, range);
215   }
216
217
218   @NotNull
219   @Override
220   public DiffContent createClipboardContent() {
221     return createClipboardContent(null, null);
222   }
223
224   @NotNull
225   @Override
226   public DocumentContent createClipboardContent(@Nullable DocumentContent referent) {
227     return createClipboardContent(null, referent);
228   }
229
230   @NotNull
231   @Override
232   public DiffContent createClipboardContent(@Nullable Project project) {
233     return createClipboardContent(project, null);
234   }
235
236   @NotNull
237   @Override
238   public DocumentContent createClipboardContent(@Nullable Project project, @Nullable DocumentContent referent) {
239     String text = CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor);
240
241     FileType type = referent != null ? referent.getContentType() : null;
242     VirtualFile highlightFile = referent != null ? referent.getHighlightFile() : null;
243
244     return createImpl(project, StringUtil.notNullize(text), type, "Clipboard.txt", highlightFile, true, false);
245   }
246
247
248   @NotNull
249   @Override
250   public DiffContent createFromBytes(@Nullable Project project,
251                                      @NotNull byte[] content,
252                                      @NotNull FilePath filePath) throws IOException {
253     if (filePath.getFileType().isBinary()) {
254       return createBinary(project, content, filePath.getFileType(), filePath.getName());
255     }
256
257     return createDocumentFromBytes(project, content, filePath);
258   }
259
260   @NotNull
261   @Override
262   public DiffContent createFromBytes(@Nullable Project project,
263                                      @NotNull byte[] content,
264                                      @NotNull VirtualFile highlightFile) throws IOException {
265     // TODO: check if FileType.UNKNOWN is actually a text ?
266     if (highlightFile.getFileType().isBinary()) {
267       return createBinary(project, content, highlightFile.getFileType(), highlightFile.getName());
268     }
269
270     return createDocumentFromBytes(project, content, highlightFile);
271   }
272
273   @NotNull
274   @Override
275   public DocumentContent createDocumentFromBytes(@Nullable Project project, @NotNull byte[] content, @NotNull FilePath filePath) {
276     return createFromBytesImpl(project, content, filePath.getFileType(), filePath.getName(), filePath.getVirtualFile(),
277                                filePath.getCharset());
278   }
279
280   @NotNull
281   @Override
282   public DocumentContent createDocumentFromBytes(@Nullable Project project, @NotNull byte[] content, @NotNull VirtualFile highlightFile) {
283     return createFromBytesImpl(project, content, highlightFile.getFileType(), highlightFile.getName(), highlightFile,
284                                highlightFile.getCharset());
285   }
286
287   @NotNull
288   @Override
289   public DiffContent createBinary(@Nullable Project project,
290                                   @NotNull byte[] content,
291                                   @NotNull FileType type,
292                                   @NotNull String fileName) throws IOException {
293     // workaround - our JarFileSystem and decompilers can't process non-local files
294     boolean useTemporalFile = type instanceof ArchiveFileType || BinaryFileTypeDecompilers.INSTANCE.forFileType(type) != null;
295
296     VirtualFile file;
297     if (useTemporalFile) {
298       file = createTemporalFile(project, "tmp", fileName, content);
299     }
300     else {
301       file = new BinaryLightVirtualFile(fileName, type, content);
302       file.setWritable(false);
303     }
304
305     return create(project, file);
306   }
307
308   @NotNull
309   private static DocumentContent createImpl(@Nullable Project project,
310                                             @NotNull String text,
311                                             @Nullable FileType fileType,
312                                             @Nullable String fileName,
313                                             @Nullable VirtualFile highlightFile,
314                                             boolean respectLineSeparators,
315                                             boolean readOnly) {
316     return createImpl(project, text, fileType, fileName, highlightFile, null, null, respectLineSeparators, readOnly);
317   }
318
319   @NotNull
320   private static DocumentContent createImpl(@Nullable Project project,
321                                             @NotNull String text,
322                                             @Nullable FileType fileType,
323                                             @Nullable String fileName,
324                                             @Nullable VirtualFile highlightFile,
325                                             @Nullable Charset charset,
326                                             @Nullable Boolean bom,
327                                             boolean respectLineSeparators,
328                                             boolean readOnly) {
329     if (FileTypes.UNKNOWN.equals(fileType)) fileType = PlainTextFileType.INSTANCE;
330
331     // TODO: detect invalid (different across the file) separators ?
332     LineSeparator separator = respectLineSeparators ? StringUtil.detectSeparators(text) : null;
333     String correctedContent = StringUtil.convertLineSeparators(text);
334
335     Document document = createDocument(project, correctedContent, fileType, fileName, readOnly);
336     DocumentContent content = new DocumentContentImpl(project, document, fileType, highlightFile, separator, charset, bom);
337
338     if (fileName != null) content.putUserData(DiffUserDataKeysEx.FILE_NAME, fileName);
339
340     return content;
341   }
342
343   @NotNull
344   private static DocumentContent createFromBytesImpl(@Nullable Project project,
345                                                      @NotNull byte[] content,
346                                                      @NotNull FileType fileType,
347                                                      @NotNull String fileName,
348                                                      @Nullable VirtualFile highlightFile,
349                                                      @NotNull Charset charset) {
350     Charset bomCharset = CharsetToolkit.guessFromBOM(content);
351     boolean isBOM = bomCharset != null;
352     if (isBOM) charset = bomCharset;
353
354     boolean malformedContent = false;
355     String text;
356     try {
357       text = CharsetToolkit.tryDecodeString(content, charset);
358     }
359     catch (CharacterCodingException e) {
360       text = CharsetToolkit.decodeString(content, charset);
361       malformedContent = true;
362     }
363
364     LineSeparator separator = StringUtil.detectSeparators(text);
365     String correctedContent = StringUtil.convertLineSeparators(text);
366
367     DocumentContent documentContent = createImpl(project, correctedContent, fileType, fileName, highlightFile, charset, isBOM, true, true);
368
369     if (malformedContent) {
370       String notificationText = "Content was decoded with errors (using " + "'" + charset.name() + "' charset)";
371       DiffUtil.addNotification(DiffNotifications.createNotification(notificationText, LightColors.RED), documentContent);
372     }
373
374     return documentContent;
375   }
376
377   @NotNull
378   private static VirtualFile createTemporalFile(@Nullable Project project,
379                                                 @NotNull String prefix,
380                                                 @NotNull String suffix,
381                                                 @NotNull byte[] content) throws IOException {
382     File tempFile = FileUtil.createTempFile(PathUtil.suggestFileName(prefix + "_", true, false),
383                                             PathUtil.suggestFileName("_" + suffix, true, false), true);
384     if (content.length != 0) {
385       FileUtil.writeToFile(tempFile, content);
386     }
387     if (!tempFile.setWritable(false, false)) LOG.warn("Can't set writable attribute of temporal file");
388
389     VirtualFile file = VfsUtil.findFileByIoFile(tempFile, true);
390     if (file == null) {
391       throw new IOException("Can't create temp file for revision content");
392     }
393     VfsUtil.markDirtyAndRefresh(true, true, true, file);
394     return file;
395   }
396
397   @NotNull
398   private static Document createDocument(@Nullable Project project,
399                                          @NotNull String content,
400                                          @Nullable FileType fileType,
401                                          @Nullable String fileName,
402                                          boolean readOnly) {
403     if (project != null && !project.isDefault() &&
404         fileType != null && !fileType.isBinary() &&
405         Registry.is("diff.enable.psi.highlighting")) {
406       if (fileName == null) {
407         fileName = "diff." + StringUtil.defaultIfEmpty(fileType.getDefaultExtension(), "txt");
408       }
409
410       Document document = createPsiDocument(project, content, fileType, fileName, readOnly);
411       if (document != null) return document;
412     }
413
414     Document document = EditorFactory.getInstance().createDocument(content);
415     document.setReadOnly(readOnly);
416     return document;
417   }
418
419   @Nullable
420   private static Document createPsiDocument(@NotNull Project project,
421                                             @NotNull String content,
422                                             @NotNull FileType fileType,
423                                             @NotNull String fileName,
424                                             boolean readOnly) {
425     return ReadAction.compute(() -> {
426       LightVirtualFile file = new LightVirtualFile(fileName, fileType, content);
427       file.setWritable(!readOnly);
428
429       file.putUserData(DiffPsiFileSupport.KEY, true);
430
431       Document document = FileDocumentManager.getInstance().getDocument(file);
432       if (document == null) return null;
433
434       PsiDocumentManager.getInstance(project).getPsiFile(document);
435
436       return document;
437     });
438   }
439 }