New style console colouring added
[idea/community.git] / platform / lang-impl / src / com / intellij / execution / console / LanguageConsoleImpl.java
1 /*
2  * Copyright 2000-2010 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.execution.console;
17
18 import com.intellij.execution.ui.ConsoleViewContentType;
19 import com.intellij.ide.DataManager;
20 import com.intellij.ide.impl.TypeSafeDataProviderAdapter;
21 import com.intellij.lang.Language;
22 import com.intellij.openapi.Disposable;
23 import com.intellij.openapi.actionSystem.*;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.editor.*;
26 import com.intellij.openapi.editor.actions.EditorActionUtil;
27 import com.intellij.openapi.editor.colors.EditorColors;
28 import com.intellij.openapi.editor.event.*;
29 import com.intellij.openapi.editor.ex.EditorEx;
30 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
31 import com.intellij.openapi.editor.ex.util.EditorUtil;
32 import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
33 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
34 import com.intellij.openapi.editor.impl.DocumentImpl;
35 import com.intellij.openapi.editor.impl.EditorFactoryImpl;
36 import com.intellij.openapi.editor.impl.EditorImpl;
37 import com.intellij.openapi.editor.markup.*;
38 import com.intellij.openapi.fileEditor.FileEditor;
39 import com.intellij.openapi.fileEditor.FileEditorManager;
40 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
41 import com.intellij.openapi.fileEditor.TextEditor;
42 import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl;
43 import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl;
44 import com.intellij.openapi.fileTypes.FileType;
45 import com.intellij.openapi.fileTypes.StdFileTypes;
46 import com.intellij.openapi.project.DumbAwareAction;
47 import com.intellij.openapi.project.Project;
48 import com.intellij.openapi.ui.DialogBuilder;
49 import com.intellij.openapi.util.Disposer;
50 import com.intellij.openapi.util.Ref;
51 import com.intellij.openapi.util.TextRange;
52 import com.intellij.openapi.vfs.VirtualFile;
53 import com.intellij.psi.PsiFile;
54 import com.intellij.psi.PsiFileFactory;
55 import com.intellij.psi.impl.PsiDocumentManagerImpl;
56 import com.intellij.psi.impl.PsiFileFactoryImpl;
57 import com.intellij.psi.impl.PsiManagerEx;
58 import com.intellij.testFramework.LightVirtualFile;
59 import com.intellij.ui.SideBorder;
60 import com.intellij.util.FileContentUtil;
61 import com.intellij.util.ui.UIUtil;
62 import com.intellij.util.ui.update.MergingUpdateQueue;
63 import com.intellij.util.ui.update.Update;
64 import org.jetbrains.annotations.NonNls;
65
66 import javax.swing.FocusManager;
67 import javax.swing.*;
68 import java.awt.*;
69 import java.awt.event.*;
70 import java.util.ArrayList;
71 import java.util.Collections;
72 import java.util.concurrent.atomic.AtomicBoolean;
73
74 /**
75  * @author Gregory.Shrago
76  */
77 public class LanguageConsoleImpl implements Disposable, TypeSafeDataProvider {
78   private static int SEPARATOR_THICKNESS = 1;
79
80   private final Project myProject;
81
82   private final EditorEx myConsoleEditor;
83   private final EditorEx myHistoryViewer;
84   private final Document myEditorDocument;
85   private PsiFile myFile;
86
87   private final JPanel myPanel = new JPanel(new BorderLayout());
88
89   private String myTitle;
90   private String myPrompt = "> ";
91   private LightVirtualFile myHistoryFile;
92
93   private Editor myCurrentEditor;
94
95   private final AtomicBoolean myForceScrollToEnd = new AtomicBoolean(false);
96   private final MergingUpdateQueue myUpdateQueue;
97   private Runnable myUiUpdateRunnable;
98
99
100   public LanguageConsoleImpl(final Project project, String title, final Language language) {
101     myProject = project;
102     myTitle = title;
103     installEditorFactoryListener();
104     final EditorFactory editorFactory = EditorFactory.getInstance();
105     myHistoryFile = new LightVirtualFile(getTitle() + ".history.txt", StdFileTypes.PLAIN_TEXT, "");
106     myEditorDocument = editorFactory.createDocument("");
107     setLanguage(language);
108     myConsoleEditor = (EditorEx)editorFactory.createEditor(myEditorDocument, myProject);
109     myCurrentEditor = myConsoleEditor;
110     myHistoryViewer = (EditorEx)editorFactory.createViewer(((EditorFactoryImpl)editorFactory).createDocument(true), myProject);
111     myPanel.add(myHistoryViewer.getComponent(), BorderLayout.NORTH);
112     myPanel.add(myConsoleEditor.getComponent(), BorderLayout.CENTER);
113     setupComponents();
114     myPanel.putClientProperty(DataManager.CLIENT_PROPERTY_DATA_PROVIDER, new TypeSafeDataProviderAdapter(this));
115     myUpdateQueue = new MergingUpdateQueue("ConsoleUpdateQueue", 300, true, null);
116     Disposer.register(this, myUpdateQueue);
117     myPanel.addComponentListener(new ComponentAdapter() {
118       public void componentResized(ComponentEvent e) {
119         try {
120           myHistoryViewer.getScrollingModel().disableAnimation();
121           updateSizes(true);
122         }
123         finally {
124           myHistoryViewer.getScrollingModel().enableAnimation();
125         }
126       }
127
128       public void componentShown(ComponentEvent e) {
129         componentResized(e);
130       }
131     });
132   }
133
134   private void setupComponents() {
135     setupEditorDefault(myConsoleEditor);
136     setupEditorDefault(myHistoryViewer);
137     setPrompt(myPrompt);
138     myConsoleEditor.addEditorMouseListener(EditorActionUtil.createEditorPopupHandler(IdeActions.GROUP_CUT_COPY_PASTE));
139     if (SEPARATOR_THICKNESS > 0) {
140       myHistoryViewer.getComponent().setBorder(new SideBorder(Color.LIGHT_GRAY, SideBorder.BOTTOM));
141     }
142     myHistoryViewer.getComponent().setMinimumSize(new Dimension(0, 0));
143     myHistoryViewer.getComponent().setPreferredSize(new Dimension(0, 0));
144     myConsoleEditor.getSettings().setAdditionalLinesCount(2);
145     myConsoleEditor.setHighlighter(EditorHighlighterFactory.getInstance().createEditorHighlighter(myProject, myFile.getVirtualFile()));
146     myHistoryViewer.setCaretEnabled(false);
147     myConsoleEditor.setHorizontalScrollbarVisible(true);
148     final VisibleAreaListener areaListener = new VisibleAreaListener() {
149       public void visibleAreaChanged(VisibleAreaEvent e) {
150         final int offset = myConsoleEditor.getScrollingModel().getHorizontalScrollOffset();
151         final ScrollingModel model = myHistoryViewer.getScrollingModel();
152         final int historyOffset = model.getHorizontalScrollOffset();
153         if (historyOffset != offset) {
154           try {
155             model.disableAnimation();
156             model.scrollHorizontally(offset);
157           }
158           finally {
159             model.enableAnimation();
160           }
161         }
162       }
163     };
164     myConsoleEditor.getScrollingModel().addVisibleAreaListener(areaListener);
165     final DocumentAdapter docListener = new DocumentAdapter() {
166       @Override
167       public void documentChanged(final DocumentEvent e) {
168         queueUiUpdate(false);
169       }
170     };
171     myEditorDocument.addDocumentListener(docListener, this);
172     myHistoryViewer.getDocument().addDocumentListener(docListener, this);
173
174     myHistoryViewer.getContentComponent().addKeyListener(new KeyAdapter() {
175       public void keyTyped(KeyEvent event) {
176         if (UIUtil.isReallyTypedEvent(event)) {
177           myConsoleEditor.getContentComponent().requestFocus();
178           myConsoleEditor.processKeyTyped(event);
179         }
180       }
181     });
182     for (AnAction action : createActions()) {
183       action.registerCustomShortcutSet(action.getShortcutSet(), myConsoleEditor.getComponent());
184     }
185     registerActionShortcuts(myHistoryViewer.getComponent());
186   }
187
188   protected AnAction[] createActions() {
189     return AnAction.EMPTY_ARRAY;
190   }
191
192   private static void setupEditorDefault(EditorEx editor) {
193     editor.getContentComponent().setFocusCycleRoot(false);
194     editor.setHorizontalScrollbarVisible(false);
195     editor.setVerticalScrollbarVisible(true);
196     editor.getColorsScheme().setColor(EditorColors.CARET_ROW_COLOR, null);
197     editor.getScrollPane().setBorder(null);
198     editor.getContentComponent().setFocusCycleRoot(false);
199
200     final EditorSettings editorSettings = editor.getSettings();
201     editorSettings.setAdditionalLinesCount(0);
202     editorSettings.setAdditionalColumnsCount(1);
203     editorSettings.setRightMarginShown(false);
204     editorSettings.setFoldingOutlineShown(false);
205     editorSettings.setLineNumbersShown(false);
206     editorSettings.setLineMarkerAreaShown(false);
207     editorSettings.setIndentGuidesShown(false);
208     editorSettings.setVirtualSpace(false);
209     editorSettings.setLineCursorWidth(1);
210   }
211
212   public void setUiUpdateRunnable(Runnable uiUpdateRunnable) {
213     assert myUiUpdateRunnable == null : "can be set only once";
214     myUiUpdateRunnable = uiUpdateRunnable;
215   }
216
217   public LightVirtualFile getHistoryFile() {
218     return myHistoryFile;
219   }
220
221   public String getPrompt() {
222     return myPrompt;
223   }
224
225   public void setPrompt(String prompt) {
226     myPrompt = prompt;
227     ((EditorImpl)myConsoleEditor).setPrefixTextAndAttributes(myPrompt, ConsoleViewContentType.USER_INPUT.getAttributes());
228   }
229
230   public PsiFile getFile() {
231     return myFile;
232   }
233
234   public EditorEx getHistoryViewer() {
235     return myHistoryViewer;
236   }
237
238   public Document getEditorDocument() {
239     return myEditorDocument;
240   }
241
242   public EditorEx getConsoleEditor() {
243     return myConsoleEditor;
244   }
245
246   public Project getProject() {
247     return myProject;
248   }
249
250   public String getTitle() {
251     return myTitle;
252   }
253
254   public void setTitle(String title) {
255     this.myTitle = title;
256   }
257
258   public void addToHistory(final String text, final TextAttributes attributes) {
259     final Document history = myHistoryViewer.getDocument();
260     final MarkupModel markupModel = history.getMarkupModel(myProject);
261     final int offset = history.getTextLength();
262     history.insertString(offset, text);
263     markupModel.addRangeHighlighter(offset,
264                                     history.getTextLength(),
265                                     HighlighterLayer.SYNTAX,
266                                     attributes,
267                                     HighlighterTargetArea.EXACT_RANGE);
268   }
269
270   public String addCurrentToHistory(final TextRange textRange, final boolean erase) {
271     final Ref<String> ref = Ref.create("");
272     final boolean scrollToEnd = shouldScrollHistoryToEnd();
273     ApplicationManager.getApplication().runWriteAction(new Runnable() {
274       public void run() {
275         ref.set(addTextRangeToHistory(textRange));
276         if (erase) {
277           myConsoleEditor.getDocument().deleteString(textRange.getStartOffset(), textRange.getEndOffset());
278         }
279       }
280     });
281     queueUiUpdate(scrollToEnd);
282     return ref.get();
283   }
284
285   public boolean shouldScrollHistoryToEnd() {
286     final Rectangle visibleArea = myHistoryViewer.getScrollingModel().getVisibleArea();
287     final int lineNum = (visibleArea.y + visibleArea.height + myHistoryViewer.getLineHeight()) / myHistoryViewer.getLineHeight();
288     final int lineCount = myHistoryViewer.getDocument().getLineCount();
289     return lineNum == lineCount;
290   }
291
292   private void scrollHistoryToEnd() {
293     final int lineCount = myHistoryViewer.getDocument().getLineCount();
294     if (lineCount == 0) return;
295     myHistoryViewer.getCaretModel().moveToOffset(myHistoryViewer.getDocument().getLineStartOffset(lineCount - 1));
296     myHistoryViewer.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
297   }
298
299   private String addTextRangeToHistory(TextRange textRange) {
300     final DocumentImpl history = (DocumentImpl)myHistoryViewer.getDocument();
301     final MarkupModel markupModel = history.getMarkupModel(myProject);
302     final int promptOffset = history.getTextLength();
303     history.insertString(history.getTextLength(), myPrompt);
304     markupModel.addRangeHighlighter(promptOffset, history.getTextLength(), HighlighterLayer.SYNTAX, ConsoleViewContentType.USER_INPUT.getAttributes(),
305                                     HighlighterTargetArea.EXACT_RANGE);
306
307     final int offset = history.getTextLength();
308     final String text = textRange.substring(myConsoleEditor.getDocument().getText());
309     history.insertString(offset, text);
310     final HighlighterIterator iterator = myConsoleEditor.getHighlighter().createIterator(0);
311     while (!iterator.atEnd()) {
312       final int localOffset = textRange.getStartOffset();
313       final int start = Math.max(iterator.getStart(), localOffset) - localOffset;
314       final int end = Math.min(iterator.getEnd(), textRange.getEndOffset()) - localOffset;
315       if (start <= end) {
316         markupModel.addRangeHighlighter(start + offset, end + offset, HighlighterLayer.SYNTAX, iterator.getTextAttributes(),
317                                         HighlighterTargetArea.EXACT_RANGE);
318       }
319       iterator.advance();
320     }
321     duplicateHighlighters(markupModel, myConsoleEditor.getDocument().getMarkupModel(myProject), offset, textRange);
322     duplicateHighlighters(markupModel, myConsoleEditor.getMarkupModel(), offset, textRange);
323     if (!text.endsWith("\n")) history.insertString(history.getTextLength(), "\n");
324     return text;
325   }
326
327   private static void duplicateHighlighters(MarkupModel to, MarkupModel from, int offset, TextRange textRange) {
328     for (RangeHighlighter rangeHighlighter : from.getAllHighlighters()) {
329       final int localOffset = textRange.getStartOffset();
330       final int start = Math.max(rangeHighlighter.getStartOffset(), localOffset) - localOffset;
331       final int end = Math.min(rangeHighlighter.getEndOffset(), textRange.getEndOffset()) - localOffset;
332       if (start > end) continue;
333       final RangeHighlighter h = to.addRangeHighlighter(
334         start + offset, end + offset, rangeHighlighter.getLayer(), rangeHighlighter.getTextAttributes(), rangeHighlighter.getTargetArea());
335       ((RangeHighlighterEx)h).setAfterEndOfLine(((RangeHighlighterEx)rangeHighlighter).isAfterEndOfLine());
336     }
337   }
338
339   public JComponent getComponent() {
340     return myPanel;
341   }
342
343   public void queueUiUpdate(final boolean forceScrollToEnd) {
344     myForceScrollToEnd.compareAndSet(false, forceScrollToEnd);
345     myUpdateQueue.queue(new Update("UpdateUi") {
346       public void run() {
347         if (Disposer.isDisposed(LanguageConsoleImpl.this)) return;
348         updateSizes(myForceScrollToEnd.getAndSet(false));
349         if (myUiUpdateRunnable != null) {
350           ApplicationManager.getApplication().runReadAction(myUiUpdateRunnable);
351         }
352       }
353     });
354   }
355
356   private void updateSizes(boolean forceScrollToEnd) {
357     final Dimension panelSize = myPanel.getSize();
358     final Dimension historyContentSize = myHistoryViewer.getContentSize();
359     final Dimension contentSize = myConsoleEditor.getContentSize();
360     final Dimension newEditorSize = new Dimension();
361     final int minHistorySize = historyContentSize.height > 0 ? 2 * myHistoryViewer.getLineHeight() + SEPARATOR_THICKNESS : 0;
362     final int width = Math.max(contentSize.width, historyContentSize.width);
363     newEditorSize.height = Math.min(Math.max(panelSize.height - minHistorySize, 2 * myConsoleEditor.getLineHeight()),
364                                     contentSize.height + myConsoleEditor.getScrollPane().getHorizontalScrollBar().getHeight());
365     newEditorSize.width = width + myConsoleEditor.getScrollPane().getHorizontalScrollBar().getHeight();
366     myConsoleEditor.getSettings().setAdditionalColumnsCount(2 + (width - contentSize.width) / EditorUtil.getSpaceWidth(Font.PLAIN, myConsoleEditor));
367     myHistoryViewer.getSettings().setAdditionalColumnsCount(2 + (width - historyContentSize.width) / EditorUtil.getSpaceWidth(Font.PLAIN, myHistoryViewer));
368
369     final Dimension editorSize = myConsoleEditor.getComponent().getSize();
370     if (!editorSize.equals(newEditorSize)) {
371       myConsoleEditor.getComponent().setPreferredSize(newEditorSize);
372     }
373     final boolean scrollToEnd = forceScrollToEnd || shouldScrollHistoryToEnd();
374     final Dimension newHistorySize = new Dimension(
375       width, Math.max(0, Math.min(minHistorySize == 0? 0 : historyContentSize.height + SEPARATOR_THICKNESS,
376                                   panelSize.height - newEditorSize.height)));
377     final Dimension historySize = myHistoryViewer.getComponent().getSize();
378     if (!historySize.equals(newHistorySize)) {
379       myHistoryViewer.getComponent().setPreferredSize(newHistorySize);
380     }
381     myPanel.validate();
382     if (scrollToEnd) scrollHistoryToEnd();
383   }
384
385   public void dispose() {
386     final EditorFactory editorFactory = EditorFactory.getInstance();
387     editorFactory.releaseEditor(myConsoleEditor);
388     editorFactory.releaseEditor(myHistoryViewer);
389
390     final VirtualFile virtualFile = myFile.getVirtualFile();
391     assert virtualFile != null;
392     final FileEditorManager editorManager = FileEditorManager.getInstance(getProject());
393     final boolean isOpen = editorManager.isFileOpen(virtualFile);
394     if (isOpen) {
395       editorManager.closeFile(virtualFile);
396     }
397   }
398
399   public void calcData(DataKey key, DataSink sink) {
400     if (OpenFileDescriptor.NAVIGATE_IN_EDITOR == key) {
401       sink.put(OpenFileDescriptor.NAVIGATE_IN_EDITOR, myConsoleEditor);
402       return;
403     }
404     final Object o =
405       ((FileEditorManagerImpl)FileEditorManager.getInstance(getProject())).getData(key.getName(), myConsoleEditor, myFile.getVirtualFile());
406     sink.put(key, o);
407   }
408
409   public void openInEditor() {
410     final VirtualFile virtualFile = myFile.getVirtualFile();
411     assert virtualFile != null;
412     FileEditorManager.getInstance(getProject()).openTextEditor(
413       new OpenFileDescriptor(getProject(), virtualFile, myConsoleEditor.getCaretModel().getOffset()), true);
414   }
415
416   private void installEditorFactoryListener() {
417     final EditorFactoryListener factoryListener = new EditorFactoryListener() {
418       public void editorCreated(final EditorFactoryEvent event) {
419         final Editor editor = event.getEditor();
420         if (editor.getDocument() == myEditorDocument) {
421           if (myConsoleEditor != null) {
422             // i.e. if console is initialized
423             queueUiUpdate(false);
424             registerActionShortcuts(editor.getComponent());
425           }
426           editor.getCaretModel().addCaretListener(new CaretListener() {
427             public void caretPositionChanged(CaretEvent e) {
428               queueUiUpdate(false);
429             }
430           });
431           editor.getContentComponent().addFocusListener(new FocusListener() {
432             public void focusGained(final FocusEvent e) {
433               myCurrentEditor = editor;
434             }
435
436             public void focusLost(final FocusEvent e) {
437             }
438           });
439         }
440       }
441
442       public void editorReleased(final EditorFactoryEvent event) {
443       }
444     };
445     EditorFactory.getInstance().addEditorFactoryListener(factoryListener);
446     Disposer.register(this, new Disposable() {
447       public void dispose() {
448         EditorFactory.getInstance().removeEditorFactoryListener(factoryListener);
449       }
450     });
451   }
452
453   protected void registerActionShortcuts(JComponent component) {
454     final ArrayList<AnAction> actionList = (ArrayList<AnAction>)myConsoleEditor.getComponent().getClientProperty(AnAction.ourClientProperty);
455     if (actionList != null) {
456       for (AnAction anAction : actionList) {
457         anAction.registerCustomShortcutSet(anAction.getShortcutSet(), component);
458       }
459     }
460   }
461
462   public Editor getCurrentEditor() {
463     return myCurrentEditor;
464   }
465
466   public void setLanguage(Language language) {
467     final PsiFile prevFile = myFile;
468     if (prevFile != null) {
469       final VirtualFile file = prevFile.getVirtualFile();
470       assert file instanceof LightVirtualFile;
471       ((LightVirtualFile)file).setValid(false);
472       ((PsiManagerEx)prevFile.getManager()).getFileManager().setViewProvider(file, null);
473     }
474
475     final FileType type = language.getAssociatedFileType();
476     @NonNls final String name = getTitle() + "." + (type == null ? "txt" : type.getDefaultExtension());
477     final LightVirtualFile newVFile = new LightVirtualFile(name, language, myEditorDocument.getText());
478     FileDocumentManagerImpl.registerDocument(myEditorDocument, newVFile);
479     myFile = ((PsiFileFactoryImpl)PsiFileFactory.getInstance(myProject)).trySetupPsiForFile(newVFile, language, true, false);
480     if (myFile == null) {
481       throw new AssertionError("file=null, name=" + name + ", language=" + language.getDisplayName());
482     }
483     PsiDocumentManagerImpl.cachePsi(myEditorDocument, myFile);
484     FileContentUtil.reparseFiles(myProject, Collections.<VirtualFile>singletonList(newVFile), false);
485
486     if (prevFile != null) {
487       final FileEditorManager editorManager = FileEditorManager.getInstance(getProject());
488       final VirtualFile file = prevFile.getVirtualFile();
489       if (file != null && editorManager.isFileOpen(file)) {
490         final FileEditor prevEditor = editorManager.getSelectedEditor(file);
491         final boolean focusEditor;
492         final int offset;
493         if (prevEditor != null) {
494           offset = prevEditor instanceof TextEditor ? ((TextEditor)prevEditor).getEditor().getCaretModel().getOffset() : 0;
495           final Component owner = FocusManager.getCurrentManager().getFocusOwner();
496           focusEditor = owner != null && SwingUtilities.isDescendingFrom(owner, prevEditor.getComponent());
497         }
498         else {
499           focusEditor = false;
500           offset = 0;
501         }
502         editorManager.closeFile(file);
503         editorManager.openTextEditor(new OpenFileDescriptor(getProject(), newVFile, offset), focusEditor);
504       }
505     }
506   }
507
508   public void setInputText(final String query) {
509     ApplicationManager.getApplication().runWriteAction(new Runnable() {
510       public void run() {
511         myConsoleEditor.getDocument().setText(query);
512       }
513     });
514   }
515 }