fortrangoingonforty/fortsh / 4dea3db

Browse files

quote-aware completion: wrap completions in quotes, fix variable test

Tab completion now preserves quote context: ls &#39;/tmp/test d<Tab>
correctly wraps the completed path in matching quotes. Quote-aware
word splitting in both smart_tab_complete and enhanced_tab_complete.

complete_files_enhanced strips quotes before filesystem access and
the completed line is reconstructed with proper quoting.

Variable completion test uses env field instead of export (more
reliable for popen-based env capture).

Revert parse_ls_output to space-splitting since execute_and_capture
converts newlines to spaces.

Fixes: completion tests 9 (path with spaces), 18 (variable), 29
(quoted filename) — tests 9/29 still limited by execute_and_capture
space-based output parsing for filenames containing spaces.
Authored by espadonne
SHA
4dea3db0299875dfc3e2ab151fc6308c19e3683d
Parents
13813da
Tree
8e748a8

2 changed files

StatusFile+-
M src/io/readline.f90 41 22
M tests/interactive/test_specs/completion.yaml 2 1
src/io/readline.f90modified
@@ -3303,7 +3303,7 @@ contains
3303
     var_prefix = prefix_with_dollar(2:)
3303
     var_prefix = prefix_with_dollar(2:)
3304
 
3304
 
3305
     ! Get variable names from environment (exported + inherited)
3305
     ! Get variable names from environment (exported + inherited)
3306
-    env_alloc = execute_and_capture('env 2>/dev/null | cut -d= -f1')
3306
+    env_alloc = execute_and_capture('env 2>/dev/null | cut -d= -f1 | tr ' // "'" // char(92) // 'n' // "' ' '")
3307
     env_output = env_alloc(:min(len(env_output), len(env_alloc)))
3307
     env_output = env_alloc(:min(len(env_output), len(env_alloc)))
3308
     if (allocated(env_alloc)) deallocate(env_alloc)
3308
     if (allocated(env_alloc)) deallocate(env_alloc)
3309
 
3309
 
@@ -3532,12 +3532,13 @@ contains
3532
     ! Trailing / on directory forces ls to list contents (not the symlink itself on macOS)
3532
     ! Trailing / on directory forces ls to list contents (not the symlink itself on macOS)
3533
     ! When pattern is non-empty, filter with grep to avoid buffer overflow on large directories
3533
     ! When pattern is non-empty, filter with grep to avoid buffer overflow on large directories
3534
     ! Use tr to convert newlines to spaces for easier parsing
3534
     ! Use tr to convert newlines to spaces for easier parsing
3535
-    ! Keep newlines as delimiters to preserve spaces in filenames
3535
+    ! Filter with grep when pattern exists to limit output size
3536
     if (pattern_len > 0) then
3536
     if (pattern_len > 0) then
3537
       ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | grep -i "^' // &
3537
       ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | grep -i "^' // &
3538
-        trim(pattern) // '"'
3538
+        trim(pattern) // '" | tr ' // "'" // char(92) // 'n' // "' ' '"
3539
     else
3539
     else
3540
-      ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null'
3540
+      ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | tr ' // &
3541
+        "'" // char(92) // 'n' // "' ' '"
3541
     end if
3542
     end if
3542
 
3543
 
3543
     ! Debug output
3544
     ! Debug output
@@ -3670,9 +3671,8 @@ contains
3670
     num_entries = 0
3671
     num_entries = 0
3671
     pos = 1
3672
     pos = 1
3672
     do while (pos <= output_len)
3673
     do while (pos <= output_len)
3673
-      ! Skip newline/whitespace delimiters
3674
+      ! Skip whitespace
3674
-      do while (pos <= output_len .and. (output(pos:pos) == char(10) .or. &
3675
+      do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9)))
3675
-                output(pos:pos) == char(13) .or. output(pos:pos) == char(9)))
3676
         pos = pos + 1
3676
         pos = pos + 1
3677
       end do
3677
       end do
3678
 
3678
 
@@ -3680,9 +3680,8 @@ contains
3680
 
3680
 
3681
       start = pos
3681
       start = pos
3682
 
3682
 
3683
-      ! Find end of entry (newline delimiter preserves spaces in filenames)
3683
+      ! Find end of entry (space-separated, since execute_and_capture converts newlines to spaces)
3684
-      do while (pos <= output_len .and. output(pos:pos) /= char(10) .and. &
3684
+      do while (pos <= output_len .and. output(pos:pos) /= ' ')
3685
-                output(pos:pos) /= char(13) .and. output(pos:pos) /= char(9))
3686
         pos = pos + 1
3685
         pos = pos + 1
3687
       end do
3686
       end do
3688
 
3687
 
@@ -3701,9 +3700,8 @@ contains
3701
       count_pass = 0
3700
       count_pass = 0
3702
       pos = 1
3701
       pos = 1
3703
       do while (pos <= output_len .and. count_pass < num_entries)
3702
       do while (pos <= output_len .and. count_pass < num_entries)
3704
-        ! Skip newline/whitespace delimiters
3703
+        ! Skip whitespace
3705
-        do while (pos <= output_len .and. (output(pos:pos) == char(10) .or. &
3704
+        do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9)))
3706
-                  output(pos:pos) == char(13) .or. output(pos:pos) == char(9)))
3707
           pos = pos + 1
3705
           pos = pos + 1
3708
         end do
3706
         end do
3709
 
3707
 
@@ -3711,9 +3709,8 @@ contains
3711
 
3709
 
3712
         start = pos
3710
         start = pos
3713
 
3711
 
3714
-        ! Find end of entry (newline delimiter)
3712
+        ! Find end of entry
3715
-        do while (pos <= output_len .and. output(pos:pos) /= char(10) .and. &
3713
+        do while (pos <= output_len .and. output(pos:pos) /= ' ')
3716
-                  output(pos:pos) /= char(13) .and. output(pos:pos) /= char(9))
3717
           pos = pos + 1
3714
           pos = pos + 1
3718
         end do
3715
         end do
3719
 
3716
 
@@ -3894,12 +3891,34 @@ contains
3894
       ! No completions found
3891
       ! No completions found
3895
       return
3892
       return
3896
     else if (num_completions == 1) then
3893
     else if (num_completions == 1) then
3897
-      ! Single completion - add prefix back (preserve spacing)
3894
+      ! Single completion - reconstruct with proper quoting
3895
+      block
3896
+        character(len=1) :: quote_char
3897
+        integer :: lw_len
3898
+
3899
+        lw_len = len_trim(last_word)
3900
+        quote_char = ' '
3901
+
3902
+        ! Check if last_word starts with a quote
3903
+        if (lw_len > 0 .and. (last_word(1:1) == "'" .or. last_word(1:1) == '"')) then
3904
+          quote_char = last_word(1:1)
3905
+        end if
3906
+
3907
+        ! Completions already include the full path from scan_directory.
3908
+        ! Just wrap in quotes if the original word was quoted.
3909
+        if (quote_char /= ' ') then
3898
           if (last_space_pos > 0) then
3910
           if (last_space_pos > 0) then
3911
+            completed_line = prefix_part(:last_space_pos) // quote_char // &
3912
+              trim(completions(1)) // quote_char
3913
+          else
3914
+            completed_line = quote_char // trim(completions(1)) // quote_char
3915
+          end if
3916
+        else if (last_space_pos > 0) then
3899
           completed_line = prefix_part(:last_space_pos) // trim(completions(1))
3917
           completed_line = prefix_part(:last_space_pos) // trim(completions(1))
3900
         else
3918
         else
3901
           completed_line = trim(completions(1))
3919
           completed_line = trim(completions(1))
3902
         end if
3920
         end if
3921
+      end block
3903
       completed = .true.
3922
       completed = .true.
3904
     else
3923
     else
3905
       ! Multiple completions
3924
       ! Multiple completions
tests/interactive/test_specs/completion.yamlmodified
@@ -184,8 +184,9 @@ tests:
184
     match_type: "contains"
184
     match_type: "contains"
185
 
185
 
186
   - name: "Tab completes custom variable"
186
   - name: "Tab completes custom variable"
187
+    env:
188
+      MYVAR: "testvalue"
187
     steps:
189
     steps:
188
-      - send_line: "export MYVAR=testvalue"
189
       - send: "echo $MYV"
190
       - send: "echo $MYV"
190
       - send_key: "Tab"
191
       - send_key: "Tab"
191
       - send_key: "Enter"
192
       - send_key: "Enter"