mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-04 18:29:37 +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
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* 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 boolean highlightCompletion = false;
|
||||
private int completionInsertionPosition;
|
||||
|
||||
private boolean caretGuard = true;
|
||||
private PluginTool tool;
|
||||
|
@ -482,9 +483,13 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
|
|||
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();
|
||||
List<CodeCompletion> completions =
|
||||
InterpreterPanel.this.interpreter.getCompletions(text);
|
||||
List<CodeCompletion> completions = InterpreterPanel.this.interpreter.getCompletions(
|
||||
text, completionInsertionPosition);
|
||||
completionWindow.updateCompletionList(completions);
|
||||
});
|
||||
}
|
||||
|
@ -620,11 +625,11 @@ public class InterpreterPanel extends JPanel implements OptionsChangeListener {
|
|||
}
|
||||
|
||||
String text = getInputTextPaneText();
|
||||
int position = inputTextPane.getCaretPosition();
|
||||
int position = completionInsertionPosition;
|
||||
String insertion = completion.getInsertion();
|
||||
|
||||
/* insert completion string */
|
||||
int insertedTextStart = position - completion.getCharsToRemove();
|
||||
int insertedTextStart = Math.max(0, position - completion.getCharsToRemove());
|
||||
int insertedTextEnd = insertedTextStart + insertion.length();
|
||||
String inputText =
|
||||
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
|
||||
|
||||
// case 1: "simple." => "simple.completion"
|
||||
testingCodeCompletions = List.of(new CodeCompletion("test", "completion", null, 0));
|
||||
triggerText(inputTextPane, "simple.");
|
||||
insertFirstCodeCompletion();
|
||||
assertEquals("simple.completion", inputTextPane.getText());
|
||||
triggerEnter(inputTextPane);
|
||||
// @formatter:off
|
||||
doCompletionInsertionTest(
|
||||
"simple.", // an initial piece of code
|
||||
null, // a function to call before the popup opens
|
||||
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"
|
||||
testingCodeCompletions = List.of(
|
||||
new CodeCompletion("test", "not.so.simple.completion", null, "simple.".length()));
|
||||
triggerText(inputTextPane, "simple.");
|
||||
insertFirstCodeCompletion();
|
||||
assertEquals("not.so.simple.completion", inputTextPane.getText());
|
||||
triggerEnter(inputTextPane);
|
||||
doCompletionInsertionTest("simple.", null, "simple.".length(), null,
|
||||
"not.so.simple.completion", "not.so.simple.completion");
|
||||
|
||||
// case 3: "( check.both.sides.of<caret> )" => "( check.is.ok )"
|
||||
testingCodeCompletions =
|
||||
List.of(new CodeCompletion("test", "is.ok", null, "both.sides.of".length()));
|
||||
triggerText(inputTextPane, "( check.both.sides.of )");
|
||||
inputTextPane.setCaretPosition("( check.both.sides.of".length());
|
||||
insertFirstCodeCompletion();
|
||||
assertEquals("( check.is.ok )", inputTextPane.getText());
|
||||
doCompletionInsertionTest("( check.both.sides.of )",
|
||||
() -> inputTextPane.setCaretPosition("( check.both.sides.of".length()),
|
||||
"both.sides.of".length(), null, "is.ok", "( check.is.ok )");
|
||||
|
||||
// case 4: Completions are inserted at the place where they were last updated.
|
||||
// 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() {
|
||||
|
@ -304,10 +312,26 @@ public class InterpreterPanelTest extends AbstractGhidraHeadedIntegrationTest {
|
|||
}).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();
|
||||
triggerKey(inputTextPane, defaultCompletionTrigger);
|
||||
triggerActionKey(inputTextPane, 0, KeyEvent.VK_DOWN);
|
||||
if (callWhenPopupOpens != null) {
|
||||
runSwing(() -> callWhenPopupOpens.run());
|
||||
}
|
||||
triggerEnter(inputTextPane);
|
||||
|
||||
assertEquals(expectedResult, inputTextPane.getText());
|
||||
triggerEnter(inputTextPane);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,7 +166,7 @@ def getCallTipJava(command='', locals=None):
|
|||
for args in object.argslist:
|
||||
if args is not None:
|
||||
# for now
|
||||
tipList.append(str(args.data))
|
||||
tipList.append(str(args.method))
|
||||
# elif callable(object):
|
||||
# argspec = str(object.__call__)
|
||||
# # these don't seem to be very accurate
|
||||
|
|
|
@ -442,10 +442,18 @@ public class GhidraPythonInterpreter extends InteractiveInterpreter {
|
|||
*
|
||||
* @param cmd The command line.
|
||||
* @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.
|
||||
* @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) == '(')) {
|
||||
return getMethodCommandCompletions(cmd);
|
||||
}
|
||||
|
|
|
@ -263,6 +263,18 @@ public class PythonPlugin extends ProgramPlugin
|
|||
*/
|
||||
@Override
|
||||
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
|
||||
interactiveScript.setSourceFile(new ResourceFile(new File("python")));
|
||||
interactiveScript.set(
|
||||
|
@ -270,7 +282,7 @@ public class PythonPlugin extends ProgramPlugin
|
|||
currentSelection, currentHighlight),
|
||||
interactiveTaskMonitor, console.getOutWriter());
|
||||
|
||||
return interpreter.getCommandCompletions(cmd, includeBuiltins);
|
||||
return interpreter.getCommandCompletions(cmd, includeBuiltins, caretPos);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -111,8 +111,26 @@ public class PythonCodeCompletionTest extends AbstractGhidraHeadedIntegrationTes
|
|||
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) {
|
||||
Set<String> completions = interpreter.getCommandCompletions(command, false)
|
||||
Set<String> completions = getCodeCompletions(command)
|
||||
.stream()
|
||||
.map(c -> c.getInsertion())
|
||||
.collect(Collectors.toSet());
|
||||
|
@ -125,12 +143,23 @@ public class PythonCodeCompletionTest extends AbstractGhidraHeadedIntegrationTes
|
|||
}
|
||||
|
||||
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,
|
||||
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) {
|
||||
try {
|
||||
File tempFile = tempScriptFolder.newFile();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue