/* * Copyright 2022 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at * https://opensource.org/licenses/MIT. */ package com.maddyhome.idea.vim.group; import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.CapturingProcessHandler; import com.intellij.execution.process.ProcessAdapter; import com.intellij.execution.process.ProcessEvent; import com.intellij.execution.process.ProcessOutput; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressIndicatorProvider; import com.intellij.openapi.progress.ProgressManager; import com.intellij.util.execution.ParametersListUtil; import com.intellij.util.text.CharSequenceReader; import com.maddyhome.idea.vim.KeyHandler; import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.api.*; import com.maddyhome.idea.vim.command.Command; import com.maddyhome.idea.vim.command.VimStateMachine; import com.maddyhome.idea.vim.ex.ExException; import com.maddyhome.idea.vim.ex.InvalidCommandException; import com.maddyhome.idea.vim.helper.EngineStringHelperKt; import com.maddyhome.idea.vim.helper.UiHelper; import com.maddyhome.idea.vim.newapi.IjExecutionContext; import com.maddyhome.idea.vim.newapi.IjVimEditor; import com.maddyhome.idea.vim.options.OptionConstants; import com.maddyhome.idea.vim.options.OptionScope; import com.maddyhome.idea.vim.ui.ex.ExEntryPanel; import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext; import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.io.*; import java.util.ArrayList; import static com.maddyhome.idea.vim.api.VimInjectorKt.injector; public class ProcessGroup extends VimProcessGroupBase { public String getLastCommand() { return lastCommand; } @Override public void startSearchCommand(@NotNull VimEditor editor, ExecutionContext context, int count, char leader) { if (((IjVimEditor)editor).getEditor().isOneLineMode()) // Don't allow searching in one line editors { return; } String initText = ""; String label = String.valueOf(leader); ExEntryPanel panel = ExEntryPanel.getInstance(); panel.activate(((IjVimEditor)editor).getEditor(), ((DataContext)context.getContext()), label, initText, count); } @Override public @NotNull String endSearchCommand() { ExEntryPanel panel = ExEntryPanel.getInstance(); panel.deactivate(true); return panel.getText(); } public void startExCommand(@NotNull VimEditor editor, ExecutionContext context, @NotNull Command cmd) { // Don't allow ex commands in one line editors if (editor.isOneLineMode()) return; String initText = getRange(((IjVimEditor) editor).getEditor(), cmd); VimStateMachine.getInstance(editor).pushModes(VimStateMachine.Mode.CMD_LINE, VimStateMachine.SubMode.NONE); ExEntryPanel panel = ExEntryPanel.getInstance(); panel.activate(((IjVimEditor) editor).getEditor(), ((IjExecutionContext) context).getContext(), ":", initText, 1); } @Override public boolean processExKey(@NotNull VimEditor editor, @NotNull KeyStroke stroke) { // This will only get called if somehow the key focus ended up in the editor while the ex entry window // is open. So I'll put focus back in the editor and process the key. ExEntryPanel panel = ExEntryPanel.getInstance(); if (panel.isActive()) { UiHelper.requestFocus(panel.getEntry()); panel.handleKey(stroke); return true; } else { VimStateMachine.getInstance(editor).popModes(); KeyHandler.getInstance().reset(editor); return false; } } public boolean processExEntry(final @NotNull VimEditor editor, final @NotNull ExecutionContext context) { ExEntryPanel panel = ExEntryPanel.getInstance(); panel.deactivate(true); boolean res = true; try { VimStateMachine.getInstance(editor).popModes(); logger.debug("processing command"); final String text = panel.getText(); if (!panel.getLabel().equals(":")) { // Search is handled via Argument.Type.EX_STRING. Although ProcessExEntryAction is registered as the handler for // <CR> in both command and search modes, it's only invoked for command mode (see KeyHandler.handleCommandNode). // We should never be invoked for anything other than an actual ex command. throw new InvalidCommandException("Expected ':' command. Got '" + panel.getLabel() + "'", text); } if (logger.isDebugEnabled()) logger.debug("swing=" + SwingUtilities.isEventDispatchThread()); VimInjectorKt.getInjector().getVimscriptExecutor().execute(text, editor, context, skipHistory(editor), true, CommandLineVimLContext.INSTANCE); } catch (ExException e) { VimPlugin.showMessage(e.getMessage()); VimPlugin.indicateError(); res = false; } catch (Exception bad) { ProcessGroup.logger.error(bad); VimPlugin.indicateError(); res = false; } return res; } // commands executed from map command / macro should not be added to history private boolean skipHistory(VimEditor editor) { return VimStateMachine.getInstance(editor).getMappingState().isExecutingMap() || injector.getMacro().isExecutingMacro(); } public void cancelExEntry(final @NotNull VimEditor editor, boolean resetCaret) { VimStateMachine.getInstance(editor).popModes(); KeyHandler.getInstance().reset(editor); ExEntryPanel panel = ExEntryPanel.getInstance(); panel.deactivate(true, resetCaret); } @Override public void startFilterCommand(@NotNull VimEditor editor, ExecutionContext context, @NotNull Command cmd) { String initText = getRange(((IjVimEditor) editor).getEditor(), cmd) + "!"; VimStateMachine.getInstance(editor).pushModes(VimStateMachine.Mode.CMD_LINE, VimStateMachine.SubMode.NONE); ExEntryPanel panel = ExEntryPanel.getInstance(); panel.activate(((IjVimEditor) editor).getEditor(), ((IjExecutionContext) context).getContext(), ":", initText, 1); } private @NotNull String getRange(Editor editor, @NotNull Command cmd) { String initText = ""; if (VimStateMachine.getInstance(new IjVimEditor(editor)).getMode() == VimStateMachine.Mode.VISUAL) { initText = "'<,'>"; } else if (cmd.getRawCount() > 0) { if (cmd.getCount() == 1) { initText = "."; } else { initText = ".,.+" + (cmd.getCount() - 1); } } return initText; } public @Nullable String executeCommand(@NotNull VimEditor editor, @NotNull String command, @Nullable CharSequence input, @Nullable String currentDirectoryPath) throws ExecutionException, ProcessCanceledException { // This is a much simplified version of how Vim does this. We're using stdin/stdout directly, while Vim will // redirect to temp files ('shellredir' and 'shelltemp') or use pipes. We don't support 'shellquote', because we're // not handling redirection, but we do use 'shellxquote' and 'shellxescape', because these have defaults that work // better with Windows. We also don't bother using ShellExecute for Windows commands beginning with `start`. // Finally, we're also not bothering with the crazy space and backslash handling of the 'shell' options content. return ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { final String shell = ((VimString) VimPlugin.getOptionService().getOptionValue(OptionScope.GLOBAL.INSTANCE, OptionConstants.shellName, OptionConstants.shellName)).getValue(); final String shellcmdflag = ((VimString) VimPlugin.getOptionService().getOptionValue(OptionScope.GLOBAL.INSTANCE, OptionConstants.shellcmdflagName, OptionConstants.shellcmdflagName)).getValue(); final String shellxescape = ((VimString) VimPlugin.getOptionService().getOptionValue(OptionScope.GLOBAL.INSTANCE, OptionConstants.shellxescapeName, OptionConstants.shellxescapeName)).getValue(); final String shellxquote = ((VimString) VimPlugin.getOptionService().getOptionValue(OptionScope.GLOBAL.INSTANCE, OptionConstants.shellxquoteName, OptionConstants.shellxquoteName)).getValue(); // For Win32. See :help 'shellxescape' final String escapedCommand = shellxquote.equals("(") ? doEscape(command, shellxescape, "^") : command; // Required for Win32+cmd.exe, defaults to "(". See :help 'shellxquote' final String quotedCommand = shellxquote.equals("(") ? "(" + escapedCommand + ")" : (shellxquote.equals("\"(") ? "\"(" + escapedCommand + ")\"" : shellxquote + escapedCommand + shellxquote); final ArrayList<String> commands = new ArrayList<>(); commands.add(shell); if (!shellcmdflag.isEmpty()) { // Note that Vim also does a simple whitespace split for multiple parameters commands.addAll(ParametersListUtil.parse(shellcmdflag)); } commands.add(quotedCommand); if (logger.isDebugEnabled()) { logger.debug(String.format("shell=%s shellcmdflag=%s command=%s", shell, shellcmdflag, quotedCommand)); } final GeneralCommandLine commandLine = new GeneralCommandLine(commands); if (currentDirectoryPath != null) { commandLine.setWorkDirectory(currentDirectoryPath); } final CapturingProcessHandler handler = new CapturingProcessHandler(commandLine); if (input != null) { handler.addProcessListener(new ProcessAdapter() { @Override public void startNotified(@NotNull ProcessEvent event) { try { final CharSequenceReader charSequenceReader = new CharSequenceReader(input); final BufferedWriter outputStreamWriter = new BufferedWriter(new OutputStreamWriter(handler.getProcessInput())); copy(charSequenceReader, outputStreamWriter); outputStreamWriter.close(); } catch (IOException e) { logger.error(e); } } }); } final ProgressIndicator progressIndicator = ProgressIndicatorProvider.getInstance().getProgressIndicator(); final ProcessOutput output = handler.runProcessWithProgressIndicator(progressIndicator); lastCommand = command; if (output.isCancelled()) { // TODO: Vim will use whatever text has already been written to stdout // For whatever reason, we're not getting any here, so just throw an exception throw new ProcessCanceledException(); } final Integer exitCode = handler.getExitCode(); if (exitCode != null && exitCode != 0) { VimPlugin.showMessage("shell returned " + exitCode); VimPlugin.indicateError(); } return EngineStringHelperKt.removeAsciiColorCodes(output.getStderr() + output.getStdout()); }, "IdeaVim - !" + command, true, ((IjVimEditor) editor).getEditor().getProject()); } private String doEscape(String original, String charsToEscape, String escapeChar) { String result = original; for (char c : charsToEscape.toCharArray()) { result = result.replace("" + c, escapeChar + c); } return result; } // TODO: Java 10 has a transferTo method we could use instead private void copy(@NotNull Reader from, @NotNull Writer to) throws IOException { char[] buf = new char[2048]; int cnt; while ((cnt = from.read(buf)) != -1) { to.write(buf, 0, cnt); } } private String lastCommand; private static final Logger logger = Logger.getInstance(ProcessGroup.class.getName()); }