@@ -214,6 +214,224 @@ function msg_qty(descr::Any, qty::Any; kwargs...)
214214 return msg_qty (string (descr), string (qty); kwargs... )
215215end
216216
217+ function msg_path (descr_raw:: AbstractString , path_raw:: AbstractString ;
218+ linewidth:: Union{Int,Nothing} = nothing ,
219+ leftwidth:: Union{Int,Nothing} = nothing , indentation:: Int = 2 ,
220+ filler:: Char = ' .' , separator:: AbstractString = " " ,
221+ delimiter:: AbstractString = " " ,
222+ continuation_label:: AbstractString = " (continued)" )
223+ descr, path = strip (descr_raw), strip (path_raw)
224+
225+ # Determine effective linewidth
226+ effective_linewidth = something (linewidth, leftwidth != = nothing ? default_linewidth () : nothing , default_linewidth ())
227+
228+ # Try to fit on one line
229+ len_filling = get_len_filling (linewidth, leftwidth, indentation, descr, separator, path)
230+ required_length = indentation + length (descr) + length (separator) + max (len_filling, 1 ) + length (path)
231+
232+ if required_length <= effective_linewidth
233+ # Fits on one line - use msg_qty
234+ return msg_qty (descr, path; linewidth= linewidth, leftwidth= leftwidth,
235+ indentation= indentation, filler= filler, separator= separator,
236+ delimiter= delimiter, newline= true )
237+ end
238+
239+ # Path is too long - split into multiple lines
240+ return _msg_path_multiline (descr, path, effective_linewidth, indentation, filler,
241+ separator, delimiter, continuation_label)
242+ end
243+
244+ function _msg_path_multiline (descr, path, linewidth, indentation, filler, separator, delimiter, continuation_label)
245+ min_filler = 2 * length (delimiter)
246+
247+ # First line: calculate budget for path
248+ first_line_budget = linewidth - indentation - length (descr) - length (separator) - min_filler
249+ break_idx = _find_path_break (path, first_line_budget)
250+
251+ path_first = break_idx > 0 ? path[1 : break_idx] : " "
252+ path_rest = break_idx > 0 ? path[break_idx+ 1 : end ] : path
253+
254+ # Build first line
255+ first_line = if ! isempty (path_first)
256+ len_fill = linewidth - indentation - length (descr) - length (separator) - length (path_first)
257+ _msg_qty (descr, path_first, len_fill, indentation, filler, separator, delimiter, true )
258+ else
259+ len_fill = linewidth - indentation - length (descr) - length (separator)
260+ _msg_qty (descr, " " , len_fill, indentation, filler, separator, delimiter, true )
261+ end
262+
263+ # Build continuation lines
264+ continuation_lines = _build_continuation_lines (path_rest, continuation_label, linewidth,
265+ indentation, filler, delimiter)
266+
267+ return first_line * continuation_lines
268+ end
269+
270+ function _build_continuation_lines (remaining_path, label, linewidth, indentation, filler, delimiter)
271+ isempty (remaining_path) && return " "
272+
273+ result = " "
274+ min_filler = 2 * length (delimiter)
275+ prefix_len = indentation + length (label)
276+
277+ while ! isempty (remaining_path)
278+ budget = linewidth - prefix_len - min_filler
279+
280+ if length (remaining_path) <= budget
281+ # Last piece fits
282+ len_fill = linewidth - prefix_len - length (remaining_path)
283+ result *= _msg_qty (label, remaining_path, len_fill, indentation, filler, " " , delimiter, true )
284+ break
285+ else
286+ # Need another split
287+ break_idx = _find_path_break (remaining_path, budget)
288+ piece = break_idx > 0 ? remaining_path[1 : break_idx] : remaining_path[1 : min (budget, end )]
289+ remaining_path = length (piece) < length (remaining_path) ? remaining_path[length (piece)+ 1 : end ] : " "
290+
291+ len_fill = linewidth - prefix_len - length (piece)
292+ result *= _msg_qty (label, piece, len_fill, indentation, filler, " " , delimiter, true )
293+ end
294+ end
295+
296+ return result
297+ end
298+
299+ function _find_path_break (path:: AbstractString , max_length:: Int )
300+ length (path) <= max_length && return length (path)
301+ max_length <= 0 && return 0
302+
303+ # Find last directory separator within budget
304+ last_sep = findlast (c -> c == ' /' || c == ' \\ ' , @view path[1 : min (max_length, end )])
305+
306+ return something (last_sep, min (max_length, length (path)))
307+ end
308+
309+ function msg_vec (descr_raw:: AbstractString , vec:: AbstractVector ;
310+ linewidth:: Union{Int,Nothing} = nothing ,
311+ leftwidth:: Union{Int,Nothing} = nothing , indentation:: Int = 2 ,
312+ filler:: Char = ' .' , separator:: AbstractString = " " ,
313+ delimiter:: AbstractString = " " ,
314+ continuation_label:: AbstractString = " (continued)" ,
315+ vec_delimiter:: AbstractString = " , " ,
316+ vec_brackets:: Tuple{AbstractString,AbstractString} = (" [" , " ]" ))
317+ descr = strip (descr_raw)
318+
319+ # Format vector as string
320+ vec_str = _format_vector (vec, vec_delimiter, vec_brackets)
321+
322+ # Determine effective linewidth
323+ effective_linewidth = something (linewidth, leftwidth != = nothing ? default_linewidth () : nothing , default_linewidth ())
324+
325+ # Try to fit on one line
326+ len_filling = get_len_filling (linewidth, leftwidth, indentation, descr, separator, vec_str)
327+ required_length = indentation + length (descr) + length (separator) + max (len_filling, 1 ) + length (vec_str)
328+
329+ if required_length <= effective_linewidth
330+ # Fits on one line - use msg_qty
331+ return msg_qty (descr, vec_str; linewidth= linewidth, leftwidth= leftwidth,
332+ indentation= indentation, filler= filler, separator= separator,
333+ delimiter= delimiter, newline= true )
334+ end
335+
336+ # Vector string is too long - split into multiple lines
337+ return _msg_vec_multiline (descr, vec_str, effective_linewidth, indentation, filler,
338+ separator, delimiter, continuation_label)
339+ end
340+
341+ function _format_vector (vec:: AbstractVector , vec_delimiter:: AbstractString ,
342+ vec_brackets:: Tuple{AbstractString,AbstractString} )
343+ isempty (vec) && return vec_brackets[1 ] * vec_brackets[2 ]
344+ elements = _format_elements (vec)
345+ vec_str = vec_brackets[1 ] * join (elements, vec_delimiter) * vec_brackets[2 ]
346+ return vec_str
347+ end
348+
349+ function _format_elements (vec:: AbstractVector{<:Real} )
350+ return [@sprintf (" %.7g" , x) for x in vec]
351+ end
352+ function _format_elements (vec:: AbstractVector{T} ) where {T}
353+ return [string (x) for x in vec]
354+ end
355+
356+ function _msg_vec_multiline (descr, vec_str, linewidth, indentation, filler, separator,
357+ delimiter, continuation_label)
358+ min_filler = 2 * length (delimiter)
359+
360+ # First line: calculate budget for vector string
361+ first_line_budget = linewidth - indentation - length (descr) - length (separator) - min_filler
362+ break_idx = _find_vec_break (vec_str, first_line_budget)
363+
364+ vec_first = break_idx > 0 ? vec_str[1 : break_idx] : " "
365+ vec_rest = break_idx > 0 ? vec_str[break_idx+ 1 : end ] : vec_str
366+
367+ # Build first line
368+ first_line = if ! isempty (vec_first)
369+ len_fill = linewidth - indentation - length (descr) - length (separator) - length (vec_first)
370+ _msg_qty (descr, vec_first, len_fill, indentation, filler, separator, delimiter, true )
371+ else
372+ len_fill = linewidth - indentation - length (descr) - length (separator)
373+ _msg_qty (descr, " " , len_fill, indentation, filler, separator, delimiter, true )
374+ end
375+
376+ # Build continuation lines
377+ continuation_lines = _build_vec_continuation_lines (vec_rest, continuation_label, linewidth,
378+ indentation, filler, delimiter)
379+
380+ return first_line * continuation_lines
381+ end
382+
383+ function _build_vec_continuation_lines (remaining_vec, label, linewidth, indentation, filler, delimiter)
384+ isempty (remaining_vec) && return " "
385+
386+ result = " "
387+ min_filler = 2 * length (delimiter)
388+ prefix_len = indentation + length (label)
389+
390+ while ! isempty (remaining_vec)
391+ budget = linewidth - prefix_len - min_filler
392+
393+ if length (remaining_vec) <= budget
394+ # Last piece fits
395+ len_fill = linewidth - prefix_len - length (remaining_vec)
396+ result *= _msg_qty (label, remaining_vec, len_fill, indentation, filler, " " , delimiter, true )
397+ break
398+ else
399+ # Need another split
400+ break_idx = _find_vec_break (remaining_vec, budget)
401+ piece = break_idx > 0 ? remaining_vec[1 : break_idx] : remaining_vec[1 : min (budget, end )]
402+ remaining_vec = length (piece) < length (remaining_vec) ? remaining_vec[length (piece)+ 1 : end ] : " "
403+
404+ len_fill = linewidth - prefix_len - length (piece)
405+ result *= _msg_qty (label, piece, len_fill, indentation, filler, " " , delimiter, true )
406+ end
407+ end
408+
409+ return result
410+ end
411+
412+ function _find_vec_break (vec_str:: AbstractString , max_length:: Int )
413+ length (vec_str) <= max_length && return length (vec_str)
414+ max_length <= 0 && return 0
415+
416+ # Find last comma or space within budget (prefer comma)
417+ last_comma = findlast (== (' ,' ), @view vec_str[1 : min (max_length, end )])
418+
419+ if last_comma != = nothing
420+ # Include the comma and any following spaces
421+ break_point = last_comma
422+ while break_point < min (max_length, length (vec_str)) &&
423+ vec_str[break_point + 1 ] == ' '
424+ break_point += 1
425+ end
426+ return break_point
427+ end
428+
429+ # If no comma, look for space
430+ last_space = findlast (== (' ' ), @view vec_str[1 : min (max_length, end )])
431+
432+ return something (last_space, min (max_length, length (vec_str)))
433+ end
434+
217435function msg_fields_inline (obj:: T ) where {T}
218436 fields = fieldnames (T)
219437 values = Tuple (getfield (obj, field) for field in fields)
0 commit comments