/* * 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.commandInterface.console; import com.intellij.execution.console.LanguageConsoleBuilder; import com.intellij.execution.console.LanguageConsoleImpl; import com.intellij.execution.console.LanguageConsoleView; import com.intellij.execution.filters.UrlFilter; import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.ui.ConsoleViewContentType; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.editor.EditorSettings; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.fileTypes.PlainTextLanguage; import com.intellij.openapi.module.Module; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Disposer; import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.Consumer; import com.jetbrains.commandInterface.commandLine.CommandLineLanguage; import com.jetbrains.commandInterface.commandLine.psi.CommandLineFile; import com.jetbrains.python.psi.PyUtil; import com.jetbrains.toolWindowWithActions.ConsoleWithProcess; import kotlin.jvm.functions.Function1; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.util.ArrayList; import java.util.Collection; /** * <h1>Command line console</h1> * <p> * Console that allows user to type commands and execute them. * </p> * <h2>2 modes of console</h2> * <p>There are 2 types of consoles: First one is based on document: it simply allows user to type something there. It also has prompt. * Second one is connected to process, hence it bridges all 3 streams between process and console. * This console, how ever, should support both modes and switch between them. So, it supports 2 modes: * <dl> * <dt>Command-mode</dt> * <dd>Console accepts user-input, treats it as commands and allows to execute em with aid of {@link CommandModeConsumer}. * In this mode it also has prompt and {@link CommandLineLanguage}. * </dd> * <dt>Process-mode</dt> * <dd>Activated when {@link #attachToProcess(ProcessHandler)} is called. * Console hides prompt, disables language and connects itself to process with aid of {@link ProcessModeConsumer}. * When process terminates, console switches back to command-mode (restoring prompt, etc)</dd> * </dl> * </p> * * @author Ilya.Kazakevich */ @SuppressWarnings({"DeserializableClassInSecureContext", "SerializableClassInSecureContext"}) // Nobody will serialize console final class CommandConsole extends LanguageConsoleImpl implements Consumer<String>, Condition<LanguageConsoleView>, ConsoleWithProcess { /** * Width of border to create around console */ static final int BORDER_SIZE_PX = 3; /** * List of commands, executor, filter and other stuff to be injected into {@link CommandLineFile}) if any. * See {@link CommandsInfo} doc */ @Nullable private final CommandsInfo myCommandsInfo; @NotNull private final Module myModule; /** * {@link CommandModeConsumer} or {@link ProcessModeConsumer} to delegate execution to. * It also may be null if exection is not available. */ @Nullable private Consumer<String> myCurrentConsumer; /** * One to sync action access to consumer field because it may be changed by callback when process is terminated */ @NotNull private final Object myConsumerSemaphore = new Object(); /** * Listener that will be notified when console state (mode?) changed. */ @NotNull private final Collection<Runnable> myStateChangeListeners = new ArrayList<>(); /** * Process handler currently running on console (if any) * * @see #switchToProcessMode(ProcessHandler) */ @Nullable private volatile ProcessHandler myProcessHandler; /** * @param module module console runs on * @param title console title * @param commandsInfo See {@link CommandsInfo} */ private CommandConsole(@NotNull final Module module, @NotNull final String title, @Nullable final CommandsInfo commandsInfo) { super(new Helper(module.getProject(), new LightVirtualFile(title, CommandLineLanguage.INSTANCE, "")) { @Override public void setupEditor(@NotNull EditorEx editor) { super.setupEditor(editor); // We do not need spaces here, because it leads to PY-15557 EditorSettings editorSettings = editor.getSettings(); editorSettings.setAdditionalLinesCount(0); editorSettings.setAdditionalColumnsCount(0); } }); myCommandsInfo = commandsInfo; myModule = module; } @Override public void print(@NotNull String text, @NotNull final ConsoleViewContentType contentType) { if (myCommandsInfo != null) { final Function1<String, String> outputFilter = myCommandsInfo.getOutputFilter(); if (outputFilter != null) { // Pass text through filter if is provided //noinspection AssignmentToMethodParameter text = outputFilter.invoke(text); } } super.print(text, contentType); } /** * @param module module console runs on * @param title console title * @param commandsInfo See {@link CommandsInfo} */ @NotNull static CommandConsole createConsole(@NotNull final Module module, @NotNull final String title, @Nullable final CommandsInfo commandsInfo) { final CommandConsole console = new CommandConsole(module, title, commandsInfo); console.setEditable(true); LanguageConsoleBuilder.registerExecuteAction(console, console, title, title, console); console.switchToCommandMode(); console.getComponent(); // For some reason console does not have component until this method is called which leads to some errros. console.getConsoleEditor().getSettings().setAdditionalLinesCount(2); // to prevent PY-15583 Disposer.register(module.getProject(), console); // To dispose console when project disposes console.addMessageFilter(new UrlFilter()); return console; } /** * Enables/disables left border {@link #BORDER_SIZE_PX} width for certain editors. * * @param editors editors to enable/disable border * @param enable whether border should be enabled */ private static void configureLeftBorder(final boolean enable, @NotNull final EditorEx... editors) { for (final EditorEx editor : editors) { final Color backgroundColor = editor.getBackgroundColor(); // Border have the same color console background has final int thickness = enable ? BORDER_SIZE_PX : 0; final Border border = BorderFactory.createMatteBorder(0, thickness, 0, 0, backgroundColor); editor.getComponent().setBorder(border); } } @Override public void attachToProcess(final ProcessHandler processHandler) { super.attachToProcess(processHandler); processHandler.addProcessListener(new MyProcessListener()); } /** * Switches console to "command-mode" (see class doc for details) */ private void switchToCommandMode() { // "upper" and "bottom" parts of console both need padding in command mode myProcessHandler = null; setPrompt(getTitle() + " > "); ApplicationManager.getApplication().invokeAndWait(() -> { notifyStateChangeListeners(); configureLeftBorder(true, getConsoleEditor(), getHistoryViewer()); setLanguage(CommandLineLanguage.INSTANCE); final CommandLineFile file = PyUtil.as(getFile(), CommandLineFile.class); resetConsumer(null); if (file == null || myCommandsInfo == null) { return; } file.setCommands(myCommandsInfo.getCommands()); final CommandConsole console = this; resetConsumer(new CommandModeConsumer(myCommandsInfo.getCommands(), myModule, console, myCommandsInfo.getUnknownCommandsExecutor())); }, ModalityState.NON_MODAL); } /** * Switches console to "process-mode" (see class doc for details) * * @param processHandler process to attach to */ private void switchToProcessMode(@NotNull final ProcessHandler processHandler) { myProcessHandler = processHandler; ApplicationManager.getApplication().invokeAndWait(() -> { configureLeftBorder(false, getConsoleEditor()); // "bottom" part of console do not need padding now because it is used for user inputA notifyStateChangeListeners(); resetConsumer(new ProcessModeConsumer(processHandler)); // In process mode we do not need prompt and highlighting setLanguage(PlainTextLanguage.INSTANCE); setPrompt(""); }, ModalityState.NON_MODAL); } /** * Notify listeners that state has been changed */ private void notifyStateChangeListeners() { for (final Runnable listener : myStateChangeListeners) { listener.run(); } } /** * @return process handler currently running on console (if any) or null if in {@link #switchToCommandMode() command mode} * @see #switchToProcessMode(ProcessHandler) */ @Override @Nullable public ProcessHandler getProcessHandler() { return myProcessHandler; } /** * Chooses consumer to delegate execute action to. * * @param newConsumer new consumer to register to delegate execution to * or null if just reset consumer */ private void resetConsumer(@Nullable final Consumer<String> newConsumer) { synchronized (myConsumerSemaphore) { myCurrentConsumer = newConsumer; } } @Override public boolean value(final LanguageConsoleView t) { // Is execution available? synchronized (myConsumerSemaphore) { return myCurrentConsumer != null; } } @Override public void consume(final String t) { // User requested execution (enter clicked) synchronized (myConsumerSemaphore) { if (myCurrentConsumer != null) { myCurrentConsumer.consume(t); } } } /** * Adds listener that will be notified when console state (mode?) changed. * <strong>Called on EDT</strong> * * @param listener listener to notify */ void addStateChangeListener(@NotNull final Runnable listener) { myStateChangeListeners.add(listener); } /** * Listens for process to switch between modes */ private final class MyProcessListener extends ProcessAdapter { @Override public void processTerminated(@NotNull final ProcessEvent event) { super.processTerminated(event); switchToCommandMode(); } @Override public void startNotified(@NotNull final ProcessEvent event) { super.startNotified(event); switchToProcessMode(event.getProcessHandler()); } } }