@@ -47,9 +47,9 @@ const JULIAFORMATTER_OPTIONS = (
4747 trailing_zero = true ,
4848)
4949
50- is_using_or_import (x) = kind (x) === K " using" || kind (x) === K " import"
50+ is_using_or_import (x:: SyntaxNode ) = kind (x) === K " using" || kind (x) === K " import"
5151
52- function find_using_or_import (x)
52+ function find_using_or_import (x:: SyntaxNode )
5353 if is_using_or_import (x)
5454 return x. parent
5555 elseif iszero (length (children (x)))
@@ -63,46 +63,62 @@ function find_using_or_import(x)
6363 end
6464end
6565
66- char_range (x) = x. position: (x. position + span (x) - 1 )
66+ # JuliaSyntax nodes report (position, span) in *bytes*.
67+ # Julia strings must be sliced using *valid string indices* (start bytes of UTF-8 chars).
68+ #
69+ # Convert a node's byte range into a safe `UnitRange` of valid string indices.
70+ # (O(1): fix endpoints with `thisind`.)
71+ function node_char_range (n:: SyntaxNode , src:: AbstractString )
72+ startb = n. position
73+ nspan = span (n)
74+ nspan <= 0 && return startb: (startb - 1 ) # empty range
75+ stopb = min (startb + nspan - 1 , ncodeunits (src))
76+ return thisind (src, startb): thisind (src, stopb)
77+ end
6778
68- function organize_import_blocks_string (s)
69- jst = parseall (SyntaxNode, s )
79+ function organize_import_blocks_string (s:: AbstractString )
80+ jst = parseall (SyntaxNode, String (s) )
7081 return organize_import_blocks (jst)
7182end
72- organize_import_blocks_file (f) = organize_import_blocks_string (read (f, String))
7383
74- # Sort symbols, but keep the module self-reference first if present
75- function sort_with_self_first (syms, self)
76- self′ = pop! (syms, self, nothing )
77- sorted = sort! (collect (syms))
78- if self′ != = nothing
79- pushfirst! (sorted, self)
80- end
81- return sorted
84+ function organize_import_blocks_file (f:: AbstractString )
85+ return organize_import_blocks_string (read (f, String))
86+ end
87+
88+ # Sort symbols, but keep the module self-reference (bare and all aliases) first if present
89+ function sort_with_self_first (syms:: Vector{String} , self:: String )
90+ syms = unique (syms)
91+ selfs = self in syms ? [self] : String[]
92+ self_aliases = sort! (filter (s -> startswith (s, self * " as " ), syms))
93+ rest = sort! (setdiff (syms, [selfs; self_aliases]))
94+ return [selfs; self_aliases; rest]
8295end
8396
8497# Organize a single block of adjacent import/using statements
85- function organize_import_block (siblings, node_text)
98+ function organize_import_block (siblings:: AbstractVector{<:SyntaxNode} , node_text)
8699 using_mods = Set {String} ()
87- using_syms = Dict {String, Set {String}} ()
100+ using_syms = Dict {String, Vector {String}} ()
88101 import_mods = Set {String} ()
89- import_syms = Dict {String, Set {String}} ()
102+ import_syms = Dict {String, Vector {String}} ()
90103
91104 for s in siblings
92105 isusing = kind (s) === K " using"
93106 for a in children (s)
94107 if kind (a) === K " :"
95108 a_args = children (a)
96- mod = node_text (a_args[1 ])
97- set = get! (Set , isusing ? using_syms : import_syms, mod)
109+ mod = String ( node_text (a_args[1 ]) )
110+ set = get! (() -> String[] , isusing ? using_syms : import_syms, mod)
98111 for i in 2 : length (a_args)
99112 push! (set, String (node_text (a_args[i])))
100113 end
101114 elseif kind (a) === K " ." || kind (a) === K " importpath"
102115 push! (isusing ? using_mods : import_mods, String (node_text (a)))
103116 elseif ! isusing && kind (a) === K " as"
104117 a_args = children (a)
105- push! (import_mods, node_text (a_args[1 ]) * " as " * node_text (a_args[end ]))
118+ push! (
119+ import_mods,
120+ String (node_text (a_args[1 ])) * " as " * String (node_text (a_args[end ]))
121+ )
106122 else
107123 error (" Unexpected syntax in using/import statement." )
108124 end
@@ -124,21 +140,26 @@ function organize_import_block(siblings, node_text)
124140 push! (using_lines, " using " * m * " : " * join (sort_with_self_first (s, m), " , " ))
125141 end
126142 io = IOBuffer ()
127- join (io, sort! (import_lines), " \n " )
128- length (import_lines) > 0 && length (using_lines) > 0 && print (io, " \n " )
129- join (io, sort! (using_lines), " \n " )
143+ if ! isempty (import_lines)
144+ print (io, join (sort! (import_lines), " \n " ))
145+ end
146+ if ! isempty (import_lines) && ! isempty (using_lines)
147+ print (io, " \n " )
148+ end
149+ if ! isempty (using_lines)
150+ print (io, join (sort! (using_lines), " \n " ))
151+ end
130152 return String (take! (io))
131153end
132154
133- function organize_import_blocks (input)
134- src = JuliaSyntax. sourcetext (input)
155+ function organize_import_blocks (input:: SyntaxNode )
156+ # Keep a stable copy for slicing: node positions/spans refer to this text.
157+ src0 = JuliaSyntax. sourcetext (input)
135158 x = find_using_or_import (input)
136- isnothing (x) && return src
137-
159+ isnothing (x) && return src0
138160 child_nodes = children (x)
139-
140161 # Find all groups of adjacent import/using statements
141- groups = Vector{Any }[]
162+ groups = Vector{SyntaxNode }[]
142163 i = 1
143164 while i <= length (child_nodes)
144165 if is_using_or_import (child_nodes[i])
@@ -151,18 +172,25 @@ function organize_import_blocks(input)
151172 i += 1
152173 end
153174 end
154-
155- # Extract the source text of a node, trimming whitespace
156- node_text (n) = strip (src[char_range (n)])
157-
175+ # Extract the source text of a node, trimming whitespace (Unicode-safe).
176+ # Always slice from src0 (stable offsets), not the rewritten `src`.
177+ node_text (n:: SyntaxNode ) = strip (src0[node_char_range (n, src0)])
178+ # Rewritten output source.
179+ src = src0
158180 # Process each group from right to left to preserve positions
159181 for siblings in reverse (groups)
160182 formatted = organize_import_block (siblings, node_text)
161- first_pos = first (char_range (siblings[1 ]))
162- last_pos = last (char_range (siblings[end ]))
163- src = src[1 : (first_pos - 1 )] * chomp (formatted) * src[(last_pos + 1 ): end ]
183+ # Compute splice bounds using src0 (node offsets), then splice into src.
184+ first_pos = first (node_char_range (first (siblings), src0))
185+ last_pos = last (node_char_range (last (siblings), src0))
186+ # Unicode-safe splice boundaries (never do ±1 on raw integer indices)
187+ before =
188+ first_pos == firstindex (src) ? " " :
189+ src[firstindex (src): prevind (src, first_pos)]
190+ after_start = nextind (src, last_pos)
191+ after = after_start > lastindex (src) ? " " : src[after_start: end ]
192+ src = before * chomp (formatted) * after
164193 end
165-
166194 return src
167195end
168196
0 commit comments