@@ -62,7 +62,7 @@ function textDocument_completion_request(params::CompletionParams, server::Langu
6262 CSTParser. Tokenize. Tokens. CMD,
6363 CSTParser. Tokenize. Tokens. TRIPLE_CMD))
6464 string_completion (t, state)
65- elseif state. x isa EXPR && is_in_import_statement (state. x)
65+ elseif state. x isa EXPR && is_in_import_statement (state. x) || _relative_dot_depth_at (state . doc, state . offset) > 0
6666 import_completions (ppt, pt, t, is_at_end, state. x, state)
6767 elseif t isa CSTParser. Tokens. Token && t. kind == CSTParser. Tokens. DOT && pt isa CSTParser. Tokens. Token && pt. kind == CSTParser. Tokens. IDENTIFIER
6868 # getfield completion, no partial
@@ -173,6 +173,118 @@ function string_macro_altname(s)
173173 end
174174end
175175
176+ # Find innermost module EXPR containing x (or nothing)
177+ function _current_module_expr (x):: Union{EXPR,Nothing}
178+ y = x
179+ while y isa EXPR
180+ if CSTParser. defines_module (y)
181+ return y
182+ end
183+ y = parentof (y)
184+ end
185+ return nothing
186+ end
187+
188+ # Ascend n module EXPR ancestors (0 => same)
189+ function _module_ancestor_expr (modexpr:: Union{EXPR,Nothing} , n:: Int )
190+ n <= 0 && return modexpr
191+ y = modexpr
192+ while n > 0 && y isa EXPR
193+ z = parentof (y)
194+ while z isa EXPR && ! CSTParser. defines_module (z)
195+ z = parentof (z)
196+ end
197+ y = z
198+ n -= 1
199+ end
200+ return y
201+ end
202+
203+ # Count contiguous '.' for relative import at the current cursor/line end.
204+ # Handles: "import .", "import ..", "import ...", "import .Foo", "import ..Foo", etc.
205+ function _relative_dot_depth_at (doc:: Document , offset:: Int )
206+ s = get_text (doc)
207+ k = offset + 1 # 1-based
208+
209+ # Skip trailing whitespace (space, tab, CR, LF)
210+ while k > firstindex (s)
211+ p = prevind (s, k)
212+ c = s[p]
213+ if c == ' ' || c == ' \t ' || c == ' \r ' || c == ' \n '
214+ k = p
215+ else
216+ break
217+ end
218+ end
219+ k == firstindex (s) && return 0
220+ p = prevind (s, k)
221+ c = s[p]
222+
223+ # Case A: cursor directly after dots (e.g. "import ..")
224+ if c == ' .'
225+ cnt = 0
226+ q = p
227+ while q >= firstindex (s) && s[q] == ' .'
228+ cnt += 1
229+ q = prevind (s, q)
230+ end
231+ # q is now the char before the first dot (or before start)
232+ if q >= firstindex (s) && Base. is_id_char (s[q])
233+ return 0 # dots follow an identifier => not relative (e.g. Base.M)
234+ end
235+ return cnt
236+ end
237+
238+ # Case B: cursor after identifier (e.g. "import ..Foo")
239+ if Base. is_id_char (c)
240+ j = p
241+ while j > firstindex (s) && Base. is_id_char (s[j])
242+ j = prevind (s, j)
243+ end
244+ j > firstindex (s) || return 0
245+ if s[j] == ' .'
246+ cnt = 0
247+ q = j
248+ while q >= firstindex (s) && s[q] == ' .'
249+ cnt += 1
250+ q = prevind (s, q)
251+ end
252+ # q is char before the first dot
253+ if q >= firstindex (s) && Base. is_id_char (s[q])
254+ return 0 # dots follow an identifier => qualified, not relative
255+ end
256+ return cnt
257+ end
258+ end
259+
260+ return 0
261+ end
262+
263+ # Collect immediate child module names by scanning CST (works for :module and :file)
264+ function _child_module_names (x:: EXPR )
265+ names = String[]
266+ # For module: body in args[3]; for file: args is the body
267+ if CSTParser. defines_module (x)
268+ b = length (x. args) >= 3 ? x. args[3 ] : nothing
269+ if b isa EXPR && headof (b) === :block && b. args != = nothing
270+ for a in b. args
271+ if a isa EXPR && CSTParser. defines_module (a)
272+ n = CSTParser. isidentifier (a. args[2 ]) ? valof (a. args[2 ]) : String (to_codeobject (a. args[2 ]))
273+ push! (names, String (n))
274+ end
275+ end
276+ end
277+ elseif headof (x) === :file && x. args != = nothing
278+ for a in x. args
279+ if a isa EXPR && CSTParser. defines_module (a)
280+ n = CSTParser. isidentifier (a. args[2 ]) ? valof (a. args[2 ]) : String (to_codeobject (a. args[2 ]))
281+ push! (names, String (n))
282+ end
283+ end
284+ end
285+ return names
286+ end
287+
176288function collect_completions (m:: SymbolServer.ModuleStore , spartial, state:: CompletionState , inclexported= false , dotcomps= false )
177289 possible_names = String[]
178290 for val in m. vals
442554is_in_import_statement (x:: EXPR ) = is_in_fexpr (x, x -> headof (x) in (:using , :import ))
443555
444556function import_completions (ppt, pt, t, is_at_end, x, state:: CompletionState )
445- import_statement = StaticLint. get_parent_fexpr (x, x -> headof (x) === :using || headof (x) === :import )
557+ # 1) Relative import completions: . .. ... and partials
558+ depth = _relative_dot_depth_at (state. doc, state. offset)
559+ if depth > 0
560+ # Find a nearby EXPR so we can locate the current module reliably even at EOL
561+ x0 = x isa EXPR ? x : get_expr (getcst (state. doc), state. offset, 0 , true )
562+ # Current and ancestor module EXPR by CST
563+ cur_modexpr = x0 isa EXPR ? _current_module_expr (x0) : nothing
564+ target_modexpr = cur_modexpr === nothing ? nothing : _module_ancestor_expr (cur_modexpr, depth - 1 )
565+ # Child module names by scanning CST
566+ names = if target_modexpr isa EXPR
567+ _child_module_names (target_modexpr)
568+ elseif cur_modexpr === nothing && depth == 1
569+ _child_module_names (getcst (state. doc)) # file-level '.'
570+ else
571+ String[]
572+ end
573+ if ! isempty (names)
574+ partial = (t. kind == CSTParser. Tokenize. Tokens. IDENTIFIER && is_at_end) ? t. val : " "
575+ for n in names
576+ if isempty (partial) || startswith (n, partial)
577+ add_completion_item (state, CompletionItem (
578+ n,
579+ CompletionItemKinds. Module,
580+ missing ,
581+ MarkupContent (n),
582+ texteditfor (state, partial, n)
583+ ))
584+ end
585+ end
586+ end
587+ return
588+ end
446589
447- import_root = get_import_root (import_statement)
590+ # 2) Non-relative path: proceed with original logic, but guard x
591+ import_statement = x isa EXPR ? StaticLint. get_parent_fexpr (x, y -> headof (y) === :using || headof (y) === :import ) : nothing
592+ import_root = import_statement isa EXPR ? get_import_root (import_statement) : nothing
448593
449594 if (t. kind == CSTParser. Tokens. WHITESPACE && pt. kind ∈ (CSTParser. Tokens. USING, CSTParser. Tokens. IMPORT, CSTParser. Tokens. IMPORTALL, CSTParser. Tokens. COMMA, CSTParser. Tokens. COLON)) ||
450595 (t. kind in (CSTParser. Tokens. COMMA, CSTParser. Tokens. COLON))
0 commit comments