@@ -2967,14 +2967,22 @@ contains |
| 2967 | 2967 | num_completions = 0 |
| 2968 | 2968 | used_programmable_completion = .false. |
| 2969 | 2969 | |
| 2970 | | - ! Find the last word to complete |
| 2970 | + ! Find the last word to complete (respect quotes) |
| 2971 | 2971 | last_space_pos = 0 |
| 2972 | | - do i = actual_len, 1, -1 |
| 2973 | | - if (partial_input(i:i) == ' ') then |
| 2974 | | - last_space_pos = i |
| 2975 | | - exit |
| 2976 | | - end if |
| 2977 | | - end do |
| 2972 | + block |
| 2973 | + logical :: in_sq, in_dq |
| 2974 | + in_sq = .false. |
| 2975 | + in_dq = .false. |
| 2976 | + do i = 1, actual_len |
| 2977 | + if (partial_input(i:i) == "'" .and. .not. in_dq) then |
| 2978 | + in_sq = .not. in_sq |
| 2979 | + else if (partial_input(i:i) == '"' .and. .not. in_sq) then |
| 2980 | + in_dq = .not. in_dq |
| 2981 | + else if (partial_input(i:i) == ' ' .and. .not. in_sq .and. .not. in_dq) then |
| 2982 | + last_space_pos = i |
| 2983 | + end if |
| 2984 | + end do |
| 2985 | + end block |
| 2978 | 2986 | |
| 2979 | 2987 | if (last_space_pos == 0) then |
| 2980 | 2988 | last_word = trim(partial_input) |
@@ -3295,7 +3303,7 @@ contains |
| 3295 | 3303 | var_prefix = prefix_with_dollar(2:) |
| 3296 | 3304 | |
| 3297 | 3305 | ! Get variable names from environment (exported + inherited) |
| 3298 | | - env_alloc = execute_and_capture('env 2>/dev/null | cut -d= -f1 | tr ' // "'" // char(92) // 'n' // "' ' '") |
| 3306 | + env_alloc = execute_and_capture('env 2>/dev/null | cut -d= -f1') |
| 3299 | 3307 | env_output = env_alloc(:min(len(env_output), len(env_alloc))) |
| 3300 | 3308 | if (allocated(env_alloc)) deallocate(env_alloc) |
| 3301 | 3309 | |
@@ -3422,9 +3430,9 @@ contains |
| 3422 | 3430 | character(len=MAX_LINE_LEN), intent(out) :: completions(MAX_LOCAL_COMPLETIONS) |
| 3423 | 3431 | integer, intent(out) :: num_completions |
| 3424 | 3432 | |
| 3425 | | - character(len=MAX_LINE_LEN) :: dir_path, file_pattern |
| 3433 | + character(len=MAX_LINE_LEN) :: dir_path, file_pattern, clean_prefix |
| 3426 | 3434 | character(len=:), allocatable :: debug_mode |
| 3427 | | - integer :: last_slash_pos, i |
| 3435 | + integer :: last_slash_pos, i, cp_len |
| 3428 | 3436 | logical :: debug_enabled |
| 3429 | 3437 | |
| 3430 | 3438 | ! Check if debug mode is enabled |
@@ -3432,29 +3440,43 @@ contains |
| 3432 | 3440 | debug_enabled = (allocated(debug_mode) .and. trim(debug_mode) == '1') |
| 3433 | 3441 | |
| 3434 | 3442 | num_completions = 0 |
| 3435 | | - |
| 3443 | + |
| 3444 | + ! Strip leading/trailing quotes from prefix for filesystem access |
| 3445 | + clean_prefix = trim(prefix) |
| 3446 | + cp_len = len_trim(clean_prefix) |
| 3447 | + if (cp_len >= 2) then |
| 3448 | + if ((clean_prefix(1:1) == "'" .and. clean_prefix(cp_len:cp_len) == "'") .or. & |
| 3449 | + (clean_prefix(1:1) == '"' .and. clean_prefix(cp_len:cp_len) == '"')) then |
| 3450 | + clean_prefix = clean_prefix(2:cp_len-1) |
| 3451 | + else if (clean_prefix(1:1) == "'" .or. clean_prefix(1:1) == '"') then |
| 3452 | + ! Unclosed quote (user still typing) — strip leading quote only |
| 3453 | + clean_prefix = clean_prefix(2:cp_len) |
| 3454 | + end if |
| 3455 | + end if |
| 3456 | + |
| 3436 | 3457 | ! Extract directory path and filename pattern |
| 3437 | 3458 | last_slash_pos = 0 |
| 3438 | | - do i = len_trim(prefix), 1, -1 |
| 3439 | | - if (prefix(i:i) == '/') then |
| 3459 | + last_slash_pos = 0 |
| 3460 | + do i = len_trim(clean_prefix), 1, -1 |
| 3461 | + if (clean_prefix(i:i) == '/') then |
| 3440 | 3462 | last_slash_pos = i |
| 3441 | 3463 | exit |
| 3442 | 3464 | end if |
| 3443 | 3465 | end do |
| 3444 | 3466 | |
| 3445 | 3467 | if (last_slash_pos > 0) then |
| 3446 | | - dir_path = prefix(:last_slash_pos-1) |
| 3447 | | - file_pattern = prefix(last_slash_pos+1:) |
| 3468 | + dir_path = clean_prefix(:last_slash_pos-1) |
| 3469 | + file_pattern = clean_prefix(last_slash_pos+1:) |
| 3448 | 3470 | if (len_trim(dir_path) == 0) dir_path = '/' |
| 3449 | 3471 | else |
| 3450 | 3472 | dir_path = '.' |
| 3451 | | - file_pattern = trim(prefix) |
| 3473 | + file_pattern = trim(clean_prefix) |
| 3452 | 3474 | end if |
| 3453 | 3475 | |
| 3454 | 3476 | ! Preserve explicit "./" prefix: when user typed "./something", dir_path |
| 3455 | 3477 | ! is "." but completions should include "./" to match what was typed. |
| 3456 | 3478 | ! Pass "./" as dir_path so scan_directory builds paths with "./" prefix. |
| 3457 | | - if (len_trim(prefix) >= 2 .and. prefix(1:2) == './') then |
| 3479 | + if (len_trim(clean_prefix) >= 2 .and. clean_prefix(1:2) == './') then |
| 3458 | 3480 | if (trim(dir_path) == '.') dir_path = './' |
| 3459 | 3481 | end if |
| 3460 | 3482 | |
@@ -3510,11 +3532,12 @@ contains |
| 3510 | 3532 | ! Trailing / on directory forces ls to list contents (not the symlink itself on macOS) |
| 3511 | 3533 | ! When pattern is non-empty, filter with grep to avoid buffer overflow on large directories |
| 3512 | 3534 | ! Use tr to convert newlines to spaces for easier parsing |
| 3535 | + ! Keep newlines as delimiters to preserve spaces in filenames |
| 3513 | 3536 | if (pattern_len > 0) then |
| 3514 | 3537 | ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | grep -i "^' // & |
| 3515 | | - trim(pattern) // '" | tr ' // "'" // char(92) // 'n' // "' ' '" |
| 3538 | + trim(pattern) // '"' |
| 3516 | 3539 | else |
| 3517 | | - ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null | tr ' // "'" // char(92) // 'n' // "' ' '" |
| 3540 | + ls_command = 'ls -1aF "' // trim(expanded_dir) // '/" 2>/dev/null' |
| 3518 | 3541 | end if |
| 3519 | 3542 | |
| 3520 | 3543 | ! Debug output |
@@ -3647,8 +3670,9 @@ contains |
| 3647 | 3670 | num_entries = 0 |
| 3648 | 3671 | pos = 1 |
| 3649 | 3672 | do while (pos <= output_len) |
| 3650 | | - ! Skip whitespace |
| 3651 | | - do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9))) |
| 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))) |
| 3652 | 3676 | pos = pos + 1 |
| 3653 | 3677 | end do |
| 3654 | 3678 | |
@@ -3656,8 +3680,9 @@ contains |
| 3656 | 3680 | |
| 3657 | 3681 | start = pos |
| 3658 | 3682 | |
| 3659 | | - ! Find end of entry (space only, since execute_and_capture converts newlines to spaces) |
| 3660 | | - do while (pos <= output_len .and. output(pos:pos) /= ' ') |
| 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)) |
| 3661 | 3686 | pos = pos + 1 |
| 3662 | 3687 | end do |
| 3663 | 3688 | |
@@ -3676,8 +3701,9 @@ contains |
| 3676 | 3701 | count_pass = 0 |
| 3677 | 3702 | pos = 1 |
| 3678 | 3703 | do while (pos <= output_len .and. count_pass < num_entries) |
| 3679 | | - ! Skip whitespace |
| 3680 | | - do while (pos <= output_len .and. (output(pos:pos) == ' ' .or. output(pos:pos) == char(9))) |
| 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))) |
| 3681 | 3707 | pos = pos + 1 |
| 3682 | 3708 | end do |
| 3683 | 3709 | |
@@ -3685,8 +3711,9 @@ contains |
| 3685 | 3711 | |
| 3686 | 3712 | start = pos |
| 3687 | 3713 | |
| 3688 | | - ! Find end of entry |
| 3689 | | - do while (pos <= output_len .and. output(pos:pos) /= ' ') |
| 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)) |
| 3690 | 3717 | pos = pos + 1 |
| 3691 | 3718 | end do |
| 3692 | 3719 | |
@@ -3832,13 +3859,22 @@ contains |
| 3832 | 3859 | completed_line = partial_input |
| 3833 | 3860 | |
| 3834 | 3861 | ! Find the prefix (command and any earlier arguments) |
| 3862 | + ! Respect quotes: spaces inside quotes don't count as word boundaries |
| 3835 | 3863 | last_space_pos = 0 |
| 3836 | | - do i = actual_len, 1, -1 |
| 3837 | | - if (partial_input(i:i) == ' ') then |
| 3838 | | - last_space_pos = i |
| 3839 | | - exit |
| 3840 | | - end if |
| 3841 | | - end do |
| 3864 | + block |
| 3865 | + logical :: in_single_quote, in_double_quote |
| 3866 | + in_single_quote = .false. |
| 3867 | + in_double_quote = .false. |
| 3868 | + do i = 1, actual_len |
| 3869 | + if (partial_input(i:i) == "'" .and. .not. in_double_quote) then |
| 3870 | + in_single_quote = .not. in_single_quote |
| 3871 | + else if (partial_input(i:i) == '"' .and. .not. in_single_quote) then |
| 3872 | + in_double_quote = .not. in_double_quote |
| 3873 | + else if (partial_input(i:i) == ' ' .and. .not. in_single_quote .and. .not. in_double_quote) then |
| 3874 | + last_space_pos = i |
| 3875 | + end if |
| 3876 | + end do |
| 3877 | + end block |
| 3842 | 3878 | |
| 3843 | 3879 | if (last_space_pos > 0) then |
| 3844 | 3880 | prefix_part = partial_input(:last_space_pos) |