mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 10:19:23 +02:00
produce code completions at the caret's position
This commit is contained in:
parent
0bd1c24b94
commit
7317db025b
7 changed files with 122 additions and 27 deletions
|
@ -45,6 +45,23 @@ public interface InterpreterConnection {
|
||||||
*
|
*
|
||||||
* @param cmd The command to get code completions for
|
* @param cmd The command to get code completions for
|
||||||
* @return A {@link List} of {@link CodeCompletion code completions} for the given command
|
* @return A {@link List} of {@link CodeCompletion code completions} for the given command
|
||||||
|
* @deprecated Additionally implement {@link #getCompletions(String, int)}
|
||||||
|
* and consider generating completions relative to the caret position
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public List<CodeCompletion> getCompletions(String cmd);
|
public List<CodeCompletion> getCompletions(String cmd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link List} of {@link CodeCompletion code completions} for the given command
|
||||||
|
* relative to the given caret position.
|
||||||
|
*
|
||||||
|
* @param cmd The command to get code completions for
|
||||||
|
* @param caretPos The position of the caret in the input string 'cmd'.
|
||||||
|
* It should satisfy the constraint {@literal "0 <= caretPos <= cmd.length()"}
|
||||||
|
* @return A {@link List} of {@link CodeCompletion code completions} for the given command
|
||||||
|
*/
|
||||||
|
public default List<CodeCompletion> getCompletions(String cmd, int caretPos) {
|
||||||
|
// to preserve backward compatibility with existent implementations
|
||||||
|
return getCompletions(cmd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
|
||||||
|
|
||||||
private CompletionWindowTrigger completionWindowTrigger = CompletionWindowTrigger.TAB;
|
private CompletionWindowTrigger completionWindowTrigger = CompletionWindowTrigger.TAB;
|
||||||
private boolean highlightCompletion = false;
|
private boolean highlightCompletion = false;
|
||||||
|
private int completionInsertionPosition;
|
||||||
|
|
||||||
private boolean caretGuard = true;
|
private boolean caretGuard = true;
|
||||||
private PluginTool tool;
|
private PluginTool tool;
|
||||||
|
@ -482,9 +483,13 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We save the position of the caret here in advance because the user can move it
|
||||||
|
// later (but before the insertion takes place) and make the completions invalid.
|
||||||
|
completionInsertionPosition = inputTextPane.getCaretPosition();
|
||||||
|
|
||||||
String text = getInputTextPaneText();
|
String text = getInputTextPaneText();
|
||||||
List<CodeCompletion> completions =
|
List<CodeCompletion> completions = InterpreterPanel.this.interpreter.getCompletions(
|
||||||
InterpreterPanel.this.interpreter.getCompletions(text);
|
text, completionInsertionPosition);
|
||||||
completionWindow.updateCompletionList(completions);
|
completionWindow.updateCompletionList(completions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -620,11 +625,11 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
String text = getInputTextPaneText();
|
String text = getInputTextPaneText();
|
||||||
int position = inputTextPane.getCaretPosition();
|
int position = completionInsertionPosition;
|
||||||
String insertion = completion.getInsertion();
|
String insertion = completion.getInsertion();
|
||||||
|
|
||||||
/* insert completion string */
|
/* insert completion string */
|
||||||
int insertedTextStart = position - completion.getCharsToRemove();
|
int insertedTextStart = Math.max(0, position - completion.getCharsToRemove());
|
||||||
int insertedTextEnd = insertedTextStart + insertion.length();
|
int insertedTextEnd = insertedTextStart + insertion.length();
|
||||||
String inputText =
|
String inputText =
|
||||||
text.substring(0, insertedTextStart) + insertion + text.substring(position);
|
text.substring(0, insertedTextStart) + insertion + text.substring(position);
|
||||||
|
|
|
@ -162,27 +162,35 @@ public class InterpreterPanelTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
// a part of the input near the caret
|
// a part of the input near the caret
|
||||||
|
|
||||||
// case 1: "simple." => "simple.completion"
|
// case 1: "simple." => "simple.completion"
|
||||||
testingCodeCompletions = List.of(new CodeCompletion("test", "completion", null, 0));
|
// @formatter:off
|
||||||
triggerText(inputTextPane, "simple.");
|
doCompletionInsertionTest(
|
||||||
insertFirstCodeCompletion();
|
"simple.", // an initial piece of code
|
||||||
assertEquals("simple.completion", inputTextPane.getText());
|
null, // a function to call before the popup opens
|
||||||
triggerEnter(inputTextPane);
|
0, // the number of characters to delete
|
||||||
|
null, // a function to call when the popup is opened
|
||||||
|
"completion", // a completion to insert
|
||||||
|
"simple.completion"); // an expected result
|
||||||
|
// @formatter:on
|
||||||
|
|
||||||
// case 2: "simple." => "not.so.simple.completion"
|
// case 2: "simple." => "not.so.simple.completion"
|
||||||
testingCodeCompletions = List.of(
|
doCompletionInsertionTest("simple.", null, "simple.".length(), null,
|
||||||
new CodeCompletion("test", "not.so.simple.completion", null, "simple.".length()));
|
"not.so.simple.completion", "not.so.simple.completion");
|
||||||
triggerText(inputTextPane, "simple.");
|
|
||||||
insertFirstCodeCompletion();
|
|
||||||
assertEquals("not.so.simple.completion", inputTextPane.getText());
|
|
||||||
triggerEnter(inputTextPane);
|
|
||||||
|
|
||||||
// case 3: "( check.both.sides.of<caret> )" => "( check.is.ok )"
|
// case 3: "( check.both.sides.of<caret> )" => "( check.is.ok )"
|
||||||
testingCodeCompletions =
|
doCompletionInsertionTest("( check.both.sides.of )",
|
||||||
List.of(new CodeCompletion("test", "is.ok", null, "both.sides.of".length()));
|
() -> inputTextPane.setCaretPosition("( check.both.sides.of".length()),
|
||||||
triggerText(inputTextPane, "( check.both.sides.of )");
|
"both.sides.of".length(), null, "is.ok", "( check.is.ok )");
|
||||||
inputTextPane.setCaretPosition("( check.both.sides.of".length());
|
|
||||||
insertFirstCodeCompletion();
|
// case 4: Completions are inserted at the place where they were last updated.
|
||||||
assertEquals("( check.is.ok )", inputTextPane.getText());
|
// Moving the caret after that point should not affect the result.
|
||||||
|
var possibleCaretPositions = List.of(0, "some".length(), "some initial text".length());
|
||||||
|
for (int caretPos : possibleCaretPositions) {
|
||||||
|
doCompletionInsertionTest("some initial text",
|
||||||
|
() -> inputTextPane.setCaretPosition("some initial".length()),
|
||||||
|
"initial".length(),
|
||||||
|
() -> inputTextPane.setCaretPosition(caretPos), "expected",
|
||||||
|
"some expected text");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InterpreterPanel createIP() {
|
private InterpreterPanel createIP() {
|
||||||
|
@ -304,10 +312,26 @@ public class InterpreterPanelTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void insertFirstCodeCompletion() {
|
private void doCompletionInsertionTest(String codeBefore, Runnable callBeforePopup,
|
||||||
|
int charsToRemove, Runnable callWhenPopupOpens, String completion,
|
||||||
|
String expectedResult) {
|
||||||
|
CodeCompletion c = new CodeCompletion("t", completion, null, charsToRemove);
|
||||||
|
testingCodeCompletions = List.of(c);
|
||||||
|
triggerText(inputTextPane, codeBefore);
|
||||||
|
if (callBeforePopup != null) {
|
||||||
|
runSwing(() -> callBeforePopup.run());
|
||||||
|
}
|
||||||
|
|
||||||
|
// open the completion popup and insert the first and only suggestion
|
||||||
KeyStroke defaultCompletionTrigger = CompletionWindowTrigger.TAB.getKeyStroke();
|
KeyStroke defaultCompletionTrigger = CompletionWindowTrigger.TAB.getKeyStroke();
|
||||||
triggerKey(inputTextPane, defaultCompletionTrigger);
|
triggerKey(inputTextPane, defaultCompletionTrigger);
|
||||||
triggerActionKey(inputTextPane, 0, KeyEvent.VK_DOWN);
|
triggerActionKey(inputTextPane, 0, KeyEvent.VK_DOWN);
|
||||||
|
if (callWhenPopupOpens != null) {
|
||||||
|
runSwing(() -> callWhenPopupOpens.run());
|
||||||
|
}
|
||||||
|
triggerEnter(inputTextPane);
|
||||||
|
|
||||||
|
assertEquals(expectedResult, inputTextPane.getText());
|
||||||
triggerEnter(inputTextPane);
|
triggerEnter(inputTextPane);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,7 +166,7 @@ def getCallTipJava(command='', locals=None):
|
||||||
for args in object.argslist:
|
for args in object.argslist:
|
||||||
if args is not None:
|
if args is not None:
|
||||||
# for now
|
# for now
|
||||||
tipList.append(str(args.data))
|
tipList.append(str(args.method))
|
||||||
# elif callable(object):
|
# elif callable(object):
|
||||||
# argspec = str(object.__call__)
|
# argspec = str(object.__call__)
|
||||||
# # these don't seem to be very accurate
|
# # these don't seem to be very accurate
|
||||||
|
|
|
@ -442,10 +442,18 @@ public class GhidraPythonInterpreter extends InteractiveInterpreter {
|
||||||
*
|
*
|
||||||
* @param cmd The command line.
|
* @param cmd The command line.
|
||||||
* @param includeBuiltins True if we should include python built-ins; otherwise, false.
|
* @param includeBuiltins True if we should include python built-ins; otherwise, false.
|
||||||
|
* @param caretPos The position of the caret in the input string 'cmd'
|
||||||
* @return A list of possible command completions. Could be empty if there aren't any.
|
* @return A list of possible command completions. Could be empty if there aren't any.
|
||||||
* @see PythonPlugin#getCompletions
|
* @see PythonPlugin#getCompletions
|
||||||
*/
|
*/
|
||||||
List<CodeCompletion> getCommandCompletions(String cmd, boolean includeBuiltins) {
|
List<CodeCompletion> getCommandCompletions(String cmd, boolean includeBuiltins, int caretPos) {
|
||||||
|
// At this point the caret is assumed to be positioned right after the value we need to
|
||||||
|
// complete (example: "[complete.Me<caret>, rest, code]"). To make the completion work
|
||||||
|
// in our case, it's sufficient (albeit naive) to just remove the text on the right side
|
||||||
|
// of our caret. The later code (on the python's side) will parse the rest properly
|
||||||
|
// and will generate the completions.
|
||||||
|
cmd = cmd.substring(0, caretPos);
|
||||||
|
|
||||||
if ((cmd.length() > 0) && (cmd.charAt(cmd.length() - 1) == '(')) {
|
if ((cmd.length() > 0) && (cmd.charAt(cmd.length() - 1) == '(')) {
|
||||||
return getMethodCommandCompletions(cmd);
|
return getMethodCommandCompletions(cmd);
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,6 +263,18 @@ public class PythonPlugin extends ProgramPlugin
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<CodeCompletion> getCompletions(String cmd) {
|
public List<CodeCompletion> getCompletions(String cmd) {
|
||||||
|
return getCompletions(cmd, cmd.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of possible command completion values at the given position.
|
||||||
|
*
|
||||||
|
* @param cmd current command line (without prompt)
|
||||||
|
* @param caretPos The position of the caret in the input string 'cmd'
|
||||||
|
* @return A list of possible command completion values. Could be empty if there aren't any.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<CodeCompletion> getCompletions(String cmd, int caretPos) {
|
||||||
// Refresh the environment
|
// Refresh the environment
|
||||||
interactiveScript.setSourceFile(new ResourceFile(new File("python")));
|
interactiveScript.setSourceFile(new ResourceFile(new File("python")));
|
||||||
interactiveScript.set(
|
interactiveScript.set(
|
||||||
|
@ -270,7 +282,7 @@ public class PythonPlugin extends ProgramPlugin
|
||||||
currentSelection, currentHighlight),
|
currentSelection, currentHighlight),
|
||||||
interactiveTaskMonitor, console.getOutWriter());
|
interactiveTaskMonitor, console.getOutWriter());
|
||||||
|
|
||||||
return interpreter.getCommandCompletions(cmd, includeBuiltins);
|
return interpreter.getCommandCompletions(cmd, includeBuiltins, caretPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -111,8 +111,26 @@ public class PythonCodeCompletionTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
assertCharsToRemoveEqualsTo("employee.getId(", 0);
|
assertCharsToRemoveEqualsTo("employee.getId(", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCompletionsFromArbitraryCaretPositions() {
|
||||||
|
// '<caret>' designates the position of the caret
|
||||||
|
|
||||||
|
String testCase;
|
||||||
|
testCase = "MY_TUP<caret>";
|
||||||
|
assertCompletionsInclude(testCase, List.of("my_tuple"));
|
||||||
|
assertCharsToRemoveEqualsTo(testCase, "my_tup".length());
|
||||||
|
|
||||||
|
testCase = "employee.Get<caret>; (4, 2+2)";
|
||||||
|
assertCompletionsInclude(testCase, List.of("getId", "getName"));
|
||||||
|
assertCharsToRemoveEqualsTo(testCase, "Get".length());
|
||||||
|
|
||||||
|
testCase = "bar = e<caret> if 2+2==4 else 'baz'";
|
||||||
|
assertCompletionsInclude(testCase, List.of("error_function", "employee"));
|
||||||
|
assertCharsToRemoveEqualsTo(testCase, "e".length());
|
||||||
|
}
|
||||||
|
|
||||||
private void assertCompletionsInclude(String command, Collection<String> expectedCompletions) {
|
private void assertCompletionsInclude(String command, Collection<String> expectedCompletions) {
|
||||||
Set<String> completions = interpreter.getCommandCompletions(command, false)
|
Set<String> completions = getCodeCompletions(command)
|
||||||
.stream()
|
.stream()
|
||||||
.map(c -> c.getInsertion())
|
.map(c -> c.getInsertion())
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
@ -125,12 +143,23 @@ public class PythonCodeCompletionTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertCharsToRemoveEqualsTo(String command, int expectedCharsToRemove) {
|
private void assertCharsToRemoveEqualsTo(String command, int expectedCharsToRemove) {
|
||||||
for (CodeCompletion comp : interpreter.getCommandCompletions(command, false)) {
|
for (CodeCompletion comp : getCodeCompletions(command)) {
|
||||||
assertEquals(String.format("%s; field 'charsToRemove' ", comp), expectedCharsToRemove,
|
assertEquals(String.format("%s; field 'charsToRemove' ", comp), expectedCharsToRemove,
|
||||||
comp.getCharsToRemove());
|
comp.getCharsToRemove());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<CodeCompletion> getCodeCompletions(String command) {
|
||||||
|
// extract the caret position and generate completions relative to that place
|
||||||
|
int caretPos = command.indexOf("<caret>");
|
||||||
|
command.replace("<caret>", "");
|
||||||
|
if (caretPos == -1) {
|
||||||
|
caretPos = command.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpreter.getCommandCompletions(command, false, caretPos);
|
||||||
|
}
|
||||||
|
|
||||||
private void executePythonProgram(String code) {
|
private void executePythonProgram(String code) {
|
||||||
try {
|
try {
|
||||||
File tempFile = tempScriptFolder.newFile();
|
File tempFile = tempScriptFolder.newFile();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue