gdb: fix filename completion in the middle of a line

I noticed that filename completion in the middle of a line doesn't
work as I would expect it too.  For example, assuming '/tmp/filename'
exists, and is the only file in '/tmp/' then when I do the following:

  (gdb) file "/tmp/filen<TAB>

GDB completes to:

  (gdb) file "/tmp/filename"

But, if I type this:

  (gdb) file "/tmp/filen "xxx"

Then move the cursor to the end of '/tmp/filen' and press <TAB>, GDB
will complete the line to:

  (gdb) file "/tmp/filename "xxx"

But GDB will not insert the trailing double quote character.

The reason for this is found in readline/readline/complete.c in the
function append_to_match.  This is the function that appends the
trailing closing quote character, however, the closing quote is only
inserted if the cursor (rl_point) is at the end (rl_end) of the line
being completed.

In this patch, what I do instead is add the closing quote in the
function gdb_completer_file_name_quote, which is called from readline
through the rl_filename_quoting_function hook.  The docs for
rl_filename_quoting_function say (see 'info readline'):

  "... The MATCH_TYPE is either 'SINGLE_MATCH', if there is only one
  completion match, or 'MULT_MATCH'.  Some functions use this to
  decide whether or not to insert a closing quote character. ..."

This is exactly what I'm doing in this patch, and clearly this is not
an unusual choice.  Now after completing a filename that is not at the
end of the line GDB will add the closing quote character if
appropriate.

I have managed to write some tests for this.  I send a line of text to
GDB which includes a partial filename followed by a trailing string, I
then send the escape sequence to move the cursor left, and finally I
send the tab character.

Obviously, expect doesn't actually see the complete output with the
extra text "in place", instead expect sees the original line followed
by some escape sequences to reflect the cursor movement, then an
escape sequence to indicate that text is being inserted in the middle
of a line, followed by the new characters ... it's a bit messy, but I
think it holds together.

Reviewed-By: Tom Tromey <tom@tromey.com>
This commit is contained in:
Andrew Burgess 2024-09-12 10:56:54 +01:00
parent 9dedc2ac71
commit 4339a3ffc3
2 changed files with 189 additions and 4 deletions

View File

@ -31,6 +31,7 @@
#include "linespec.h"
#include "cli/cli-decode.h"
#include "gdbsupport/gdb_tilde_expand.h"
#include "readline/readline.h"
/* FIXME: This is needed because of lookup_cmd_1 (). We should be
calling a hook instead so we eliminate the CLI dependency. */
@ -393,13 +394,34 @@ gdb_completer_file_name_quote_1 (const char *text, char quote_char)
the quote character surrounding TEXT, or points to the null-character if
there are no quotes around TEXT. MATCH_TYPE will be one of the readline
constants SINGLE_MATCH or MULTI_MATCH depending on if there is one or
many completions. */
many completions.
We also add a trailing character, either a '/' of closing quote, if
MATCH_TYPE is 'SINGLE_MATCH'. We do this because readline will only
add this trailing character when completing at the end of a line. */
static char *
gdb_completer_file_name_quote (char *text, int match_type ATTRIBUTE_UNUSED,
char *quote_ptr)
gdb_completer_file_name_quote (char *text, int match_type, char *quote_ptr)
{
return gdb_completer_file_name_quote_1 (text, *quote_ptr);
char *result = gdb_completer_file_name_quote_1 (text, *quote_ptr);
if (match_type == SINGLE_MATCH)
{
/* Add trailing '/' if TEXT is a directory, otherwise add a closing
quote character matching *QUOTE_PTR. */
char c = (gdb_path_isdir (gdb_tilde_expand (text).c_str ())
? '/' : *quote_ptr);
/* Reallocate RESULT adding C to the end. But only if C is
interesting, otherwise we can save the reallocation. */
if (c != '\0')
{
char buf[2] = { c, '\0' };
result = reconcat (result, result, buf, nullptr);
}
}
return result;
}
/* The function is used to update the completion word MATCH before

View File

@ -113,6 +113,148 @@ proc test_gdb_complete_filename_multiple {
$testname
}
# Helper proc. Returns a string containing the escape sequence to move the
# cursor COUNT characters to the left. There's no sanity checking performed
# on COUNT, so the user of this proc must ensure there are more than COUNT
# characters on the current line.
proc c_left { count } {
string repeat "\033\[D" $count
}
# This proc is based off of test_gdb_complete_tab_multiple in
# completion-support.exp library. This proc however tests completing a
# filename in the middle of a command line.
#
# INPUT_LINE is the line to complete, BACK_COUNT is the number of characters
# to move the cursor left before sending tab to complete the line.
# ADD_COMPLETED_LINE is what we expect to be unconditionally added the first
# time tab is sent. On additional tabs COMPLETION_LIST will be displayed.
# TESTNAME is used as expected.
proc test_tab_complete_within_line_multiple { input_line back_count \
add_completed_line \
completion_list \
testname } {
global gdb_prompt
# After displaying the completion list the line will be reprinted, but
# now with ADD_COMPLETED_LINE inserted. Build the regexp to match
# against this expanded line. The new content will be inserted
# BACK_COUNT character from the end of the line.
set expanded_line \
[join [list \
[string range $input_line 0 end-$back_count] \
${add_completed_line} \
[string range $input_line end-[expr $back_count - 1] end]] \
""]
set expanded_line_re [string_to_regexp $expanded_line]
# Convert input arguments into regexp.
set input_line_re [string_to_regexp $input_line]
set add_completed_line_re [string_to_regexp $add_completed_line]
set completion_list_re [make_tab_completion_list_re $completion_list]
# Similar to test_tab_complete_within_line_unique, build two
# regexp for matching the line after the first tab. Which regexp
# matches will depend on the version and/or configuration of
# readline. This first regexp moves the cursor backwards and then
# inserts new content into the line.
set after_tab_re1 "^$input_line_re"
set after_tab_re1 "$after_tab_re1\\\x08{$back_count}"
set after_tab_re1 "$after_tab_re1${completion::bell_re}"
set after_tab_re1 "$after_tab_re1\\\x1b\\\x5b[string length $add_completed_line]\\\x40"
set after_tab_re1 "$after_tab_re1$add_completed_line_re\$"
# This second regexp moves the cursor backwards and overwrites the
# end of the line, then moves the cursor backwards again to the
# correct position within the line.
set after_tab_re2 "^$input_line_re"
set after_tab_re2 "$after_tab_re2\\\x08{$back_count}"
set after_tab_re2 "$after_tab_re2${completion::bell_re}"
set tail [string range $input_line end-[expr $back_count - 1] end]
set after_tab_re2 "$after_tab_re2$add_completed_line_re"
set after_tab_re2 "$after_tab_re2[string_to_regexp $tail]"
set after_tab_re2 "$after_tab_re2\\\x08{$back_count}"
send_gdb "$input_line[c_left $back_count]\t"
gdb_test_multiple "" "$testname (first tab)" {
-re "(?:(?:$after_tab_re1)|(?:$after_tab_re2))" {
send_gdb "\t"
# If we auto-completed to an ambiguous prefix, we need an
# extra tab to show the matches list.
if {$add_completed_line != ""} {
send_gdb "\t"
set maybe_bell ${completion::bell_re}
} else {
set maybe_bell ""
}
gdb_test_multiple "" "$testname (second tab)" {
-re "^${maybe_bell}\r\n$completion_list_re\r\n$gdb_prompt " {
gdb_test_multiple "" "$testname (second tab)" {
-re "^$expanded_line_re\\\x08{$back_count}$" {
pass $gdb_test_name
}
}
}
-re "${maybe_bell}\r\n.+\r\n$gdb_prompt $" {
fail $gdb_test_name
}
}
}
}
clear_input_line $testname
}
# Wrapper around test_gdb_complete_tab_unique to test completing a unique
# item in the middle of a line. INPUT_LINE is the line to complete.
# BACK_COUNT is the number of characters to move left within INPUT_LINE
# before sending tab to perform completion. INSERT_STR is what we expect to
# see inserted by the completion engine in GDB.
proc test_tab_complete_within_line_unique { input_line back_count insert_str } {
# Build regexp for the line after completion has occurred. As
# completion is being performed in the middle of the line the
# sequence of characters we see can vary depending on which
# version of readline is in use, and/or how readline is
# configured. Currently two different approaches are covered as
# RE1 and RE2. Both of these regexp cover the complete possible
# output.
#
# In the first case we see the input line followed by some number
# of characters to move the cursor backwards. After this we see a
# control sequence that tells the terminal that some characters
# are going to be inserted into the middle of the line, the new
# characters are then emitted. The terminal itself is responsible
# for preserving the tail of the line, so these characters are not
# re-emitted.
set re1 [string_to_regexp $input_line]
set re1 $re1\\\x08{$back_count}
set re1 $re1\\\x1b\\\x5b[string length $insert_str]\\\x40
set re1 $re1[string_to_regexp $insert_str]
# In this second regexp we again start with the input line
# followed by the control characters to move the cursor backwards.
# This time though readline emits the new characters and then
# re-emits the tail of the original line. This new content will
# overwrite the original output on the terminal. Finally, control
# characters are emitted to move the cursor back to the correct
# place in the middle of the line.
set re2 [string_to_regexp $input_line]
set re2 $re2\\\x08{$back_count}
set re2 $re2[string_to_regexp $insert_str]
set tail [string range $input_line end-[expr $back_count - 1] end]
set re2 $re2[string_to_regexp $tail]
set re2 $re2\\\x08{$back_count}
# We can now perform the tab-completion, we check for either of
# the possible output regexp patterns.
test_gdb_complete_tab_unique \
"${input_line}[c_left $back_count]" \
"(?:(?:$re1)|(?:$re2))" \
"" \
"complete unique file within command line"
}
# Run filename completetion tests for those command that accept quoting and
# escaping of the filename argument. CMD is the inital part of the command
# line, paths to complete will be added after CMD.
@ -226,6 +368,25 @@ proc run_quoting_and_escaping_tests_1 { root cmd } {
gdb_exit
}
# Tests for completing a filename in the middle of a command line. This
# represents a user typing out a command line then moving the cursor left
# (e.g. with the left arrow key), editing a filename argument, and then
# using tab completion to try and complete the filename even though there is
# other content on the command line after the filename.
proc run_mid_line_completion_tests { root cmd } {
gdb_start
test_tab_complete_within_line_unique \
"$cmd \"$root/bb2/dir 1/unique fi \"xxx\"" 6 "le\""
test_tab_complete_within_line_multiple \
"$cmd \"$root/aaa/a \"xxx\"" 6 "a " \
[list "aa bb" "aa cc"] \
"complete filename mid-line with multiple possibilities"
gdb_exit
}
# Run filename completetion tests for those command that accept quoting and
# escaping of the filename argument.
#
@ -248,6 +409,8 @@ proc run_quoting_and_escaping_tests { root } {
foreach_with_prefix filler { "" " \"xxx\"" " 'zzz'" " yyy"} {
run_quoting_and_escaping_tests_1 $root "$cmd$filler"
}
run_mid_line_completion_tests $root $cmd
}
}