blob: b40cfcddf8b70b421b0de6f567f9ce95da8a7eee [file] [log] [blame]
/*
* 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.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
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.openapi.util.Pair;
import com.intellij.util.Consumer;
import com.jetbrains.commandInterface.command.Command;
import com.jetbrains.commandInterface.command.CommandExecutor;
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 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;
import java.util.List;
/**
* <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 (to be injected into {@link CommandLineFile}) if any
* and executor to be used when user executes unknown command
*/
@Nullable
private final Pair<List<Command>, CommandExecutor> myCommandsAndDefaultExecutor;
@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<Runnable>();
/**
* 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 commandsAndDefaultExecutor List of commands (to be injected into {@link CommandLineFile}) if any
* and executor to be used when user executes unknown command
*/
private CommandConsole(@NotNull final Module module,
@NotNull final String title,
@Nullable final Pair<List<Command>, CommandExecutor> commandsAndDefaultExecutor) {
super(module.getProject(), title, CommandLineLanguage.INSTANCE);
myCommandsAndDefaultExecutor = commandsAndDefaultExecutor;
myModule = module;
}
/**
* @param module module console runs on
* @param title console title
* @param commandList List of commands (to be injected into {@link CommandLineFile}) if any
* and executor to be used when user executes unknown command
* @return console
*/
@NotNull
static CommandConsole createConsole(@NotNull final Module module,
@NotNull final String title,
@Nullable final Pair<List<Command>, CommandExecutor> commandList) {
final CommandConsole console = new CommandConsole(module, title, commandList);
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
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(new Runnable() {
@Override
public void run() {
notifyStateChangeListeners();
configureLeftBorder(true, getConsoleEditor(), getHistoryViewer());
setLanguage(CommandLineLanguage.INSTANCE);
final CommandLineFile file = PyUtil.as(getFile(), CommandLineFile.class);
resetConsumer(null);
if (file == null || myCommandsAndDefaultExecutor == null) {
return;
}
file.setCommandsAndDefaultExecutor(myCommandsAndDefaultExecutor);
final CommandConsole console = CommandConsole.this;
resetConsumer(new CommandModeConsumer(myCommandsAndDefaultExecutor.first, myModule, console, myCommandsAndDefaultExecutor.second));
}
}, 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(new Runnable() {
@Override
public void run() {
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());
}
}
@Override
protected void setupEditorDefault(@NotNull final EditorEx editor) {
super.setupEditorDefault(editor);
// We do not need spaces here, because it leads to PY-15557
final EditorSettings editorSettings = editor.getSettings();
editorSettings.setAdditionalLinesCount(0);
editorSettings.setAdditionalColumnsCount(0);
}
}