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
33033303
     var_prefix = prefix_with_dollar(2:)
33043304
 
33053305
     ! 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' // "' ' '")
33073307
     env_output = env_alloc(:min(len(env_output), len(env_alloc)))
33083308
     if (allocated(env_alloc)) deallocate(env_alloc)
33093309
 
@@ -3532,12 +3532,13 @@ contains
35323532
     ! Trailing / on directory forces ls to list contents (not the symlink itself on macOS)
35333533
     ! When pattern is non-empty, filter with grep to avoid buffer overflow on large directories
35343534
     ! 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
35363536
     if (pattern_len > 0) then
35373537
       ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | grep -i "^' // &
3538
-        trim(pattern) // '"'
3538
+        trim(pattern) // '" | tr ' // "'" // char(92) // 'n' // "' ' '"
35393539
     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' // "' ' '"
35413542
     end if
35423543
 
35433544
     ! Debug output
@@ -3670,9 +3671,8 @@ contains
36703671
     num_entries = 0
36713672
     pos = 1
36723673
     do while (pos <= output_len)
3673
-      ! Skip newline/whitespace delimiters
3674
-      do while (pos <= output_len .and. (output(pos:pos) == char(10) .or. &
3675
-                output(pos:pos) == char(13) .or. output(pos:pos) == char(9)))
3674
+      ! Skip whitespace
3675
+      do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9)))
36763676
         pos = pos + 1
36773677
       end do
36783678
 
@@ -3680,9 +3680,8 @@ contains
36803680
 
36813681
       start = pos
36823682
 
3683
-      ! Find end of entry (newline delimiter preserves spaces in filenames)
3684
-      do while (pos <= output_len .and. output(pos:pos) /= char(10) .and. &
3685
-                output(pos:pos) /= char(13) .and. output(pos:pos) /= char(9))
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) /= ' ')
36863685
         pos = pos + 1
36873686
       end do
36883687
 
@@ -3701,9 +3700,8 @@ contains
37013700
       count_pass = 0
37023701
       pos = 1
37033702
       do while (pos <= output_len .and. count_pass < num_entries)
3704
-        ! Skip newline/whitespace delimiters
3705
-        do while (pos <= output_len .and. (output(pos:pos) == char(10) .or. &
3706
-                  output(pos:pos) == char(13) .or. output(pos:pos) == char(9)))
3703
+        ! Skip whitespace
3704
+        do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9)))
37073705
           pos = pos + 1
37083706
         end do
37093707
 
@@ -3711,9 +3709,8 @@ contains
37113709
 
37123710
         start = pos
37133711
 
3714
-        ! Find end of entry (newline delimiter)
3715
-        do while (pos <= output_len .and. output(pos:pos) /= char(10) .and. &
3716
-                  output(pos:pos) /= char(13) .and. output(pos:pos) /= char(9))
3712
+        ! Find end of entry
3713
+        do while (pos <= output_len .and. output(pos:pos) /= ' ')
37173714
           pos = pos + 1
37183715
         end do
37193716
 
@@ -3894,12 +3891,34 @@ contains
38943891
       ! No completions found
38953892
       return
38963893
     else if (num_completions == 1) then
3897
-      ! Single completion - add prefix back (preserve spacing)
3898
-      if (last_space_pos > 0) then
3899
-        completed_line = prefix_part(:last_space_pos) // trim(completions(1))
3900
-      else
3901
-        completed_line = trim(completions(1))
3902
-      end if
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
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
3917
+          completed_line = prefix_part(:last_space_pos) // trim(completions(1))
3918
+        else
3919
+          completed_line = trim(completions(1))
3920
+        end if
3921
+      end block
39033922
       completed = .true.
39043923
     else
39053924
       ! Multiple completions
tests/interactive/test_specs/completion.yamlmodified
@@ -184,8 +184,9 @@ tests:
184184
     match_type: "contains"
185185
 
186186
   - name: "Tab completes custom variable"
187
+    env:
188
+      MYVAR: "testvalue"
187189
     steps:
188
-      - send_line: "export MYVAR=testvalue"
189190
       - send: "echo $MYV"
190191
       - send_key: "Tab"
191192
       - send_key: "Enter"