| /* | |
| * 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({"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, final EditorEx @NotNull ... 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()); | |
| } | |
| } | |
| } |