scripting: take PSI into consideration on command detection
[idea/community.git] / platform / lang-impl / src / com / intellij / execution / console / RunIdeConsoleAction.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.execution.console;
17
18 import com.intellij.execution.ExecutionManager;
19 import com.intellij.execution.Executor;
20 import com.intellij.execution.executors.DefaultRunExecutor;
21 import com.intellij.execution.filters.TextConsoleBuilderFactory;
22 import com.intellij.execution.impl.ConsoleViewImpl;
23 import com.intellij.execution.ui.ConsoleView;
24 import com.intellij.execution.ui.ConsoleViewContentType;
25 import com.intellij.execution.ui.RunContentDescriptor;
26 import com.intellij.execution.ui.actions.CloseAction;
27 import com.intellij.ide.scratch.ScratchFileService;
28 import com.intellij.ide.script.IdeScriptBindings;
29 import com.intellij.openapi.actionSystem.*;
30 import com.intellij.openapi.diagnostic.Logger;
31 import com.intellij.openapi.editor.Document;
32 import com.intellij.openapi.editor.Editor;
33 import com.intellij.openapi.editor.ex.util.EditorUtil;
34 import com.intellij.openapi.fileEditor.FileEditor;
35 import com.intellij.openapi.fileEditor.FileEditorManager;
36 import com.intellij.openapi.fileEditor.TextEditor;
37 import com.intellij.openapi.project.DumbAwareAction;
38 import com.intellij.openapi.project.Project;
39 import com.intellij.openapi.ui.popup.JBPopupFactory;
40 import com.intellij.openapi.util.Key;
41 import com.intellij.openapi.util.TextRange;
42 import com.intellij.openapi.util.text.StringUtil;
43 import com.intellij.openapi.vfs.VfsUtilCore;
44 import com.intellij.openapi.vfs.VirtualFile;
45 import com.intellij.psi.*;
46 import com.intellij.psi.impl.source.tree.LeafPsiElement;
47 import com.intellij.psi.util.PsiTreeUtil;
48 import com.intellij.util.ExceptionUtil;
49 import com.intellij.util.NotNullFunction;
50 import com.intellij.util.ObjectUtils;
51 import com.intellij.util.PathUtil;
52 import com.intellij.util.containers.ContainerUtil;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55 import org.jetbrains.ide.script.IdeScriptEngine;
56 import org.jetbrains.ide.script.IdeScriptEngineManager;
57
58 import javax.swing.*;
59 import java.awt.*;
60 import java.io.IOException;
61 import java.io.Writer;
62 import java.lang.ref.WeakReference;
63 import java.util.List;
64
65 /**
66  * @author gregsh
67  */
68 public class RunIdeConsoleAction extends DumbAwareAction {
69   private static final String DEFAULT_FILE_NAME = "ide-scripting";
70
71   private static final Key<WeakReference<RunContentDescriptor>> DESCRIPTOR_KEY = Key.create("DESCRIPTOR_KEY");
72   private static final Logger LOG = Logger.getInstance(RunIdeConsoleAction.class);
73
74   @Override
75   public void update(AnActionEvent e) {
76     IdeScriptEngineManager manager = IdeScriptEngineManager.getInstance();
77     e.getPresentation().setVisible(e.getProject() != null);
78     e.getPresentation().setEnabled(manager.isInitialized() && !manager.getLanguages().isEmpty());
79   }
80
81   @Override
82   public void actionPerformed(AnActionEvent e) {
83     List<String> languages = IdeScriptEngineManager.getInstance().getLanguages();
84     if (languages.size() == 1) {
85       runConsole(e, languages.iterator().next());
86       return;
87     }
88
89     DefaultActionGroup actions = new DefaultActionGroup(
90       ContainerUtil.map(languages, (NotNullFunction<String, AnAction>)language -> new DumbAwareAction(language) {
91         @Override
92         public void actionPerformed(@NotNull AnActionEvent e1) {
93           runConsole(e1, language);
94         }
95       })
96     );
97     JBPopupFactory.getInstance().createActionGroupPopup("Script Engine", actions, e.getDataContext(), JBPopupFactory.ActionSelectionAid.NUMBERING, false).
98       showInBestPositionFor(e.getDataContext());
99   }
100
101   protected void runConsole(@NotNull AnActionEvent e, @NotNull String language) {
102     Project project = e.getProject();
103     if (project == null) return;
104
105     List<String> extensions = IdeScriptEngineManager.getInstance().getFileExtensions(language);
106     try {
107       String pathName = PathUtil.makeFileName(DEFAULT_FILE_NAME, ContainerUtil.getFirstItem(extensions));
108       VirtualFile virtualFile = IdeConsoleRootType.getInstance().findFile(project, pathName, ScratchFileService.Option.create_if_missing);
109       if (virtualFile != null) {
110         FileEditorManager.getInstance(project).openFile(virtualFile, true);
111       }
112     }
113     catch (IOException ex) {
114       LOG.error(ex);
115     }
116   }
117
118   public static void configureConsole(@NotNull VirtualFile file, @NotNull FileEditorManager source) {
119     MyRunAction runAction = new MyRunAction();
120     for (FileEditor fileEditor : source.getEditors(file)) {
121       if (!(fileEditor instanceof TextEditor)) continue;
122       Editor editor = ((TextEditor)fileEditor).getEditor();
123       runAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, editor.getComponent());
124     }
125   }
126
127   private static void executeQuery(@NotNull Project project,
128                                    @NotNull VirtualFile file,
129                                    @NotNull Editor editor,
130                                    @NotNull IdeScriptEngine engine) {
131     String command = getCommandText(project, editor);
132     if (StringUtil.isEmptyOrSpaces(command)) return;
133     String profile = getProfileText(file);
134     RunContentDescriptor descriptor = getConsoleView(project, file);
135     ConsoleViewImpl consoleView = (ConsoleViewImpl)descriptor.getExecutionConsole();
136
137     prepareEngine(project, engine, descriptor);
138     try {
139       long ts = System.currentTimeMillis();
140       //myHistoryController.getModel().addToHistory(command);
141       consoleView.print("> " + command, ConsoleViewContentType.USER_INPUT);
142       consoleView.print("\n", ConsoleViewContentType.USER_INPUT);
143       String script = profile == null ? command : profile + "\n" + command;
144       Object o = engine.eval(script);
145       String prefix = "["+(StringUtil.formatDuration(System.currentTimeMillis() - ts))+"]";
146       consoleView.print(prefix + "=> " + o, ConsoleViewContentType.NORMAL_OUTPUT);
147       consoleView.print("\n", ConsoleViewContentType.NORMAL_OUTPUT);
148     }
149     catch (Throwable e) {
150       //noinspection ThrowableResultOfMethodCallIgnored
151       Throwable ex = ExceptionUtil.getRootCause(e);
152       consoleView.print(ex.getClass().getSimpleName() + ": " + ex.getMessage(), ConsoleViewContentType.ERROR_OUTPUT);
153       consoleView.print("\n", ConsoleViewContentType.ERROR_OUTPUT);
154     }
155     selectContent(descriptor);
156   }
157
158   private static void prepareEngine(@NotNull Project project, @NotNull IdeScriptEngine engine, @NotNull RunContentDescriptor descriptor) {
159     IdeScriptBindings.ensureIdeIsBound(project, engine);
160     ensureOutputIsRedirected(engine, descriptor);
161   }
162
163   @Nullable
164   private static String getProfileText(@NotNull VirtualFile file) {
165     try {
166       VirtualFile folder = file.getParent();
167       VirtualFile profileChild = folder == null ? null : folder.findChild(".profile." + file.getExtension());
168       return profileChild == null ? null : StringUtil.nullize(VfsUtilCore.loadText(profileChild));
169     }
170     catch (IOException ignored) {
171     }
172     return null;
173   }
174
175   @NotNull
176   private static String getCommandText(@NotNull Project project, @NotNull Editor editor) {
177     TextRange selectedRange = EditorUtil.getSelectionInAnyMode(editor);
178     Document document = editor.getDocument();
179     if (selectedRange.isEmpty()) {
180       int line = document.getLineNumber(selectedRange.getStartOffset());
181       selectedRange = TextRange.create(document.getLineStartOffset(line), document.getLineEndOffset(line));
182
183       // try detect a non-trivial composite PSI element if there's a PSI file
184       PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
185       if (file != null && file.getFirstChild() != null && file.getFirstChild() != file.getLastChild()) {
186         PsiElement e1 = file.findElementAt(selectedRange.getStartOffset());
187         PsiElement e2 = file.findElementAt(selectedRange.getEndOffset());
188         while (e1 != e2 && (e1 instanceof PsiWhiteSpace || e1 != null && StringUtil.isEmptyOrSpaces(e1.getText()))) {
189           e1 = ObjectUtils.chooseNotNull(e1.getNextSibling(), PsiTreeUtil.getDeepestFirst(e1.getParent()));
190         }
191         while (e1 != e2 && (e2 instanceof PsiWhiteSpace || e2 != null && StringUtil.isEmptyOrSpaces(e2.getText()))) {
192           e2 = ObjectUtils.chooseNotNull(e2.getPrevSibling(), PsiTreeUtil.getDeepestLast(e2.getParent()));
193         }
194         if (e1 instanceof LeafPsiElement) e1 = e1.getParent();
195         if (e2 instanceof LeafPsiElement) e2 = e2.getParent();
196         PsiElement parent = e1 == null ? e2 : e2 == null ? e1 : PsiTreeUtil.findCommonParent(e1, e2);
197         if (parent != null && parent != file) {
198           selectedRange = parent.getTextRange();
199         }
200       }
201     }
202     return document.getText(selectedRange);
203   }
204
205   private static void selectContent(RunContentDescriptor descriptor) {
206     Executor executor = DefaultRunExecutor.getRunExecutorInstance();
207     ConsoleViewImpl consoleView = ObjectUtils.assertNotNull((ConsoleViewImpl)descriptor.getExecutionConsole());
208     ExecutionManager.getInstance(consoleView.getProject()).getContentManager().toFrontRunContent(executor, descriptor);
209   }
210
211   @NotNull
212   private static RunContentDescriptor getConsoleView(@NotNull Project project, @NotNull VirtualFile file) {
213     PsiFile psiFile = ObjectUtils.assertNotNull(PsiManager.getInstance(project).findFile(file));
214     WeakReference<RunContentDescriptor> ref = psiFile.getCopyableUserData(DESCRIPTOR_KEY);
215     RunContentDescriptor descriptor = ref == null ? null : ref.get();
216     if (descriptor == null || descriptor.getExecutionConsole() == null) {
217       descriptor = createConsoleView(project, psiFile);
218       psiFile.putCopyableUserData(DESCRIPTOR_KEY, new WeakReference<RunContentDescriptor>(descriptor));
219     }
220     return descriptor;
221   }
222
223   @NotNull
224   private static RunContentDescriptor createConsoleView(@NotNull Project project, @NotNull PsiFile psiFile) {
225     ConsoleView consoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).getConsole();
226
227     DefaultActionGroup toolbarActions = new DefaultActionGroup();
228     JComponent panel = new JPanel(new BorderLayout());
229     panel.add(consoleView.getComponent(), BorderLayout.CENTER);
230     ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, toolbarActions, false);
231     toolbar.setTargetComponent(consoleView.getComponent());
232     panel.add(toolbar.getComponent(), BorderLayout.WEST);
233
234     final RunContentDescriptor descriptor = new RunContentDescriptor(consoleView, null, panel, psiFile.getName()) {
235       @Override
236       public boolean isContentReuseProhibited() {
237         return true;
238       }
239     };
240     Executor executor = DefaultRunExecutor.getRunExecutorInstance();
241     toolbarActions.addAll(consoleView.createConsoleActions());
242     toolbarActions.add(new CloseAction(executor, descriptor, project));
243     ExecutionManager.getInstance(project).getContentManager().showRunContent(executor, descriptor);
244
245     return descriptor;
246   }
247
248   private static class MyRunAction extends DumbAwareAction {
249
250     private IdeScriptEngine engine;
251
252     @Override
253     public void update(AnActionEvent e) {
254       Project project = e.getProject();
255       Editor editor = CommonDataKeys.EDITOR.getData(e.getDataContext());
256       VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(e.getDataContext());
257       e.getPresentation().setEnabledAndVisible(project != null && editor != null && virtualFile != null);
258     }
259
260     @Override
261     public void actionPerformed(AnActionEvent e) {
262       Project project = e.getProject();
263       Editor editor = CommonDataKeys.EDITOR.getData(e.getDataContext());
264       VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(e.getDataContext());
265       if (project == null || editor == null || virtualFile == null) return;
266       PsiDocumentManager.getInstance(project).commitAllDocuments();
267
268       String extension = virtualFile.getExtension();
269       if (extension != null && (engine == null || !engine.getFileExtensions().contains(extension))) {
270         engine = IdeScriptEngineManager.getInstance().getEngineForFileExtension(extension, null);
271       }
272       if (engine == null) {
273         LOG.warn("Script engine not found for: " + virtualFile.getName());
274       }
275       else {
276         executeQuery(project, virtualFile, editor, engine);
277       }
278     }
279   }
280
281   private static void ensureOutputIsRedirected(@NotNull IdeScriptEngine engine, @NotNull RunContentDescriptor descriptor) {
282     ConsoleWriter stdOutWriter = ObjectUtils.tryCast(engine.getStdOut(), ConsoleWriter.class);
283     ConsoleWriter stdErrWriter = ObjectUtils.tryCast(engine.getStdErr(), ConsoleWriter.class);
284     if (stdOutWriter != null && stdOutWriter.getDescriptor() == descriptor &&
285         stdErrWriter != null && stdErrWriter.getDescriptor() == descriptor) {
286       return;
287     }
288
289     WeakReference<RunContentDescriptor> ref = new WeakReference<RunContentDescriptor>(descriptor);
290     engine.setStdOut(new ConsoleWriter(ref, ConsoleViewContentType.NORMAL_OUTPUT));
291     engine.setStdErr(new ConsoleWriter(ref, ConsoleViewContentType.ERROR_OUTPUT));
292   }
293
294   private static class ConsoleWriter extends Writer {
295     private final WeakReference<RunContentDescriptor> myDescriptor;
296     private final ConsoleViewContentType myOutputType;
297
298     private ConsoleWriter(@NotNull WeakReference<RunContentDescriptor> descriptor, @NotNull ConsoleViewContentType outputType) {
299       myDescriptor = descriptor;
300       myOutputType = outputType;
301     }
302
303     @Nullable
304     public RunContentDescriptor getDescriptor() {
305       return myDescriptor.get();
306     }
307
308     @Override
309     public void write(char[] cbuf, int off, int len) throws IOException {
310       RunContentDescriptor descriptor = myDescriptor.get();
311       ConsoleViewImpl console = ObjectUtils.tryCast(descriptor != null ? descriptor.getExecutionConsole() : null, ConsoleViewImpl.class);
312       if (console == null) {
313         //TODO ignore ?
314         throw new IOException("The console is not available.");
315       }
316       console.print(new String(cbuf, off, len), myOutputType);
317     }
318
319     @Override
320     public void flush() throws IOException {
321     }
322
323     @Override
324     public void close() throws IOException {
325     }
326   }
327 }