mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-04-16 11:15:41 +02:00
283 lines
12 KiB
Java
283 lines
12 KiB
Java
/*
|
|
* 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());
|
|
}
|