From 4339a3ffc39af599305bd992536dd379ac8390ca Mon Sep 17 00:00:00 2001 From: Andrew Burgess Date: Thu, 12 Sep 2024 10:56:54 +0100 Subject: [PATCH] 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 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 , 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 --- gdb/completer.c | 30 +++- .../gdb.base/filename-completion.exp | 163 ++++++++++++++++++ 2 files changed, 189 insertions(+), 4 deletions(-) diff --git a/gdb/completer.c b/gdb/completer.c index 64c6611f636..4735aa5ed10 100644 --- a/gdb/completer.c +++ b/gdb/completer.c @@ -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 diff --git a/gdb/testsuite/gdb.base/filename-completion.exp b/gdb/testsuite/gdb.base/filename-completion.exp index cdd597f689d..85fac35d7c8 100644 --- a/gdb/testsuite/gdb.base/filename-completion.exp +++ b/gdb/testsuite/gdb.base/filename-completion.exp @@ -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 } }