Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "ITensorFormatter"
uuid = "b6bf39f1-c9d3-4bad-aad8-593d802f65fd"
version = "0.2.0"
version = "0.2.1"
authors = ["ITensor developers <support@itensor.org> and contributors"]

[workspace]
Expand All @@ -11,6 +11,9 @@ JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4"
Runic = "62bfec6d-59d7-401d-8490-b29ee721c001"

[apps]
itfmt = {}

[compat]
JuliaFormatter = "2.3.0"
JuliaSyntax = "0.4.10"
Expand Down
37 changes: 27 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
[![code style: runic](https://img.shields.io/badge/code_style-%E1%9A%B1%E1%9A%A2%E1%9A%BE%E1%9B%81%E1%9A%B2-black)](https://github.com/fredrikekre/Runic.jl)
[![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)

ITensorFormatter.jl is a code formatting tool for Julia source files. It primarily
uses the [Runic.jl](https://github.com/fredrikekre/Runic.jl) code formatter, but also
organizes using/import statements by merging adjacent blocks, sorting modules and
symbols, and line-wrapping (similar to, and based off of, the using/import statement
organization functionality in
[LanguageServer.jl](https://github.com/julia-vscode/LanguageServer.jl)).

## Support

<picture>
Expand All @@ -33,19 +40,29 @@ julia> Pkg.Registry.add(url = "git@github.com:ITensor/ITensorRegistry.git")
```
if you want to use SSH credentials, which can make it so you don't have to enter your Github ursername and password when registering packages.

Then, the package can be added as usual through the package manager:

```julia
julia> Pkg.add("ITensorFormatter")
In Julia v1.12 and later, ITensorFormatter should be installed as a
[Pkg app](https://pkgdocs.julialang.org/dev/apps/):
```sh
julia -e 'using Pkg; Pkg.Apps.add("ITensorFormatter")'
```
Assuming `~/.julia/bin` is in your `PATH` you can now invoke `itfmt`, e.g.:
```sh
# Format all files in-place in the current directory (recursively)
# !! DON'T DO THIS FROM YOUR HOME DIRECTORY !!
itfmt .
```

## Examples

````julia
using ITensorFormatter: ITensorFormatter
````
### Legacy installation

Examples go here.
In Julia v1.11 and earlier (or if you don't want to use a Pkg app), ITensorFormatter can
also be installed manually with Julia's package manager:
```sh
# Install ITensorFormatter
julia --project=@itfmt --startup-file=no -e 'using Pkg; Pkg.add("ITensorFormatter")'
# Install the itfmt shell script
curl -fsSL -o ~/.local/bin/itfmt https://raw.githubusercontent.com/ITensor/ITensorFormatter.jl/refs/heads/main/bin/itfmt
chmod +x ~/.local/bin/itfmt
```

---

Expand Down
13 changes: 13 additions & 0 deletions bin/itfmt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh

# A simple driver script for invoking ITensorFormatter's main function. Put this script
# somewhere in PATH and make sure it is executable. The script expects ITensorFormatter to
# be installed in the `@itfmt` shared environment and julia to be available in
# PATH. See installation instructions in the repository README for more
# details.
#
# Repository: https://github.com/ITensor/ITensorFormatter.jl
# SPDX-License-Identifier: Apache-2.0

export JULIA_LOAD_PATH="@itfmt"
exec julia --startup-file=no -e 'using ITensorFormatter; exit(ITensorFormatter.main(ARGS))' -- "$@"
36 changes: 29 additions & 7 deletions examples/README.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
# [![code style: runic](https://img.shields.io/badge/code_style-%E1%9A%B1%E1%9A%A2%E1%9A%BE%E1%9B%81%E1%9A%B2-black)](https://github.com/fredrikekre/Runic.jl)
# [![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)

# ITensorFormatter.jl is a code formatting tool for Julia source files. It primarily
# uses the [Runic.jl](https://github.com/fredrikekre/Runic.jl) code formatter, but also
# organizes using/import statements by merging adjacent blocks, sorting modules and
# symbols, and line-wrapping (similar to, and based off of, the using/import statement
# organization functionality in
# [LanguageServer.jl](https://github.com/julia-vscode/LanguageServer.jl)).

# ## Support
#
# {CCQ_LOGO}
Expand All @@ -33,15 +40,30 @@ julia> Pkg.Registry.add(url = "git@github.com:ITensor/ITensorRegistry.git")
=#
# if you want to use SSH credentials, which can make it so you don't have to enter your Github ursername and password when registering packages.

# Then, the package can be added as usual through the package manager:

# In Julia v1.12 and later, ITensorFormatter should be installed as a
# [Pkg app](https://pkgdocs.julialang.org/dev/apps/):
#=
```julia
julia> Pkg.add("ITensorFormatter")
```sh
julia -e 'using Pkg; Pkg.Apps.add("ITensorFormatter")'
```
=#
# Assuming `~/.julia/bin` is in your `PATH` you can now invoke `itfmt`, e.g.:
#=
```sh
# Format all files in-place in the current directory (recursively)
# !! DON'T DO THIS FROM YOUR HOME DIRECTORY !!
itfmt .
```
=#

# ## Examples
# ### Legacy installation

using ITensorFormatter: ITensorFormatter
# Examples go here.
# In Julia v1.11 and earlier (or if you don't want to use a Pkg app), ITensorFormatter can
# also be installed manually with Julia's package manager:
# ```sh
# # Install ITensorFormatter
# julia --project=@itfmt --startup-file=no -e 'using Pkg; Pkg.add("ITensorFormatter")'
# # Install the itfmt shell script
# curl -fsSL -o ~/.local/bin/itfmt https://raw.githubusercontent.com/ITensor/ITensorFormatter.jl/refs/heads/main/bin/itfmt
# chmod +x ~/.local/bin/itfmt
# ```
192 changes: 132 additions & 60 deletions src/ITensorFormatter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,38 @@ function find_using_or_import(x)
return nothing
else
for child in children(x)
x = find_using_or_import(child)
isnothing(x) || return x
result = find_using_or_import(child)
isnothing(result) || return result
end
return nothing
end
end

char_range(x) = x.position:(x.position + span(x) - 1)

function organize_import_file(f)
jst = parseall(SyntaxNode, read(f, String))
return organize_import_block(jst)
function organize_import_blocks_string(s)
jst = parseall(SyntaxNode, s)
return organize_import_blocks(jst)
end

function organize_import_block(input)
# Collect all sibling blocks that are also using/import expressions

x = find_using_or_import(input)
isnothing(x) && return JuliaSyntax.sourcetext(input)

siblings = []

child_nodes = children(x)
first_ind = findfirst(is_using_or_import, child_nodes)

for ind in first_ind:length(child_nodes)
if is_using_or_import(child_nodes[ind])
push!(siblings, child_nodes[ind])
else
break
end
organize_import_blocks_file(f) = organize_import_blocks_string(read(f, String))

# Sort symbols, but keep the module self-reference first if present
function sort_with_self_first(syms, self)
self′ = pop!(syms, self, nothing)
sorted = sort!(collect(syms))
if self′ !== nothing
pushfirst!(sorted, self)
end
return sorted
end

src = JuliaSyntax.sourcetext(input)

# Collect all modules and symbols
# Organize a single block of adjacent import/using statements
function organize_import_block(siblings, node_text)
using_mods = Set{String}()
using_syms = Dict{String, Set{String}}()
import_mods = Set{String}()
import_syms = Dict{String, Set{String}}()

# Extract the source text of a node, trimming whitespace
node_text(x) = strip(src[char_range(x)])

for s in siblings
isusing = kind(s) === K"using"
for a in children(s)
Expand All @@ -86,21 +74,6 @@ function organize_import_block(input)
end
end

# Rejoin and sort
# TODO: Currently regular string sorting is used, which roughly will correspond to
# BlueStyle (modules, types, ..., functions) since usually CamelCase is used for
# modules, types, etc, but possibly this can be improved by using information
# available from SymbolServer
# Sort symbols, but keep the module self-reference first if present
function sort_with_self_first(syms, self)
self′ = pop!(syms, self, nothing)
sorted = sort!(collect(syms))
if self′ !== nothing
pushfirst!(sorted, self)
end
return sorted
end

import_lines = String[]
for m in import_mods
push!(import_lines, "import " * m)
Expand All @@ -121,43 +94,142 @@ function organize_import_block(input)
join(io, sort!(using_lines), "\n")
str_to_fmt = String(take!(io))

# Line wrap the using/import statements only
formatted = JuliaFormatter.format_text(str_to_fmt; join_lines_based_on_source = true)
return JuliaFormatter.format_text(str_to_fmt; join_lines_based_on_source = true)
end

first_pos = first(char_range(siblings[1]))
last_pos = last(char_range(siblings[end]))
function organize_import_blocks(input)
src = JuliaSyntax.sourcetext(input)
x = find_using_or_import(input)
isnothing(x) && return src

content = src[1:(first_pos - 1)] * chomp(formatted) * src[(last_pos + 1):end]
child_nodes = children(x)

return content
# Find all groups of adjacent import/using statements
groups = Vector{Any}[]
i = 1
while i <= length(child_nodes)
if is_using_or_import(child_nodes[i])
group_start = i
while i <= length(child_nodes) && is_using_or_import(child_nodes[i])
i += 1
end
push!(groups, child_nodes[group_start:(i - 1)])
else
i += 1
end
end

# Extract the source text of a node, trimming whitespace
node_text(n) = strip(src[char_range(n)])

# Process each group from right to left to preserve positions
for siblings in reverse(groups)
formatted = organize_import_block(siblings, node_text)
first_pos = first(char_range(siblings[1]))
last_pos = last(char_range(siblings[end]))
src = src[1:(first_pos - 1)] * chomp(formatted) * src[(last_pos + 1):end]
end

return src
end

const ITENSORFORMATTER_VERSION = pkgversion(@__MODULE__)

# Print a typical cli program help message
function print_help()
io = stdout
printstyled(io, "NAME", bold = true)
println(io)
println(io, " ITensorFormatter.main - format Julia source code")
println(io)
printstyled(io, "SYNOPSIS", bold = true)
println(io)
println(io, " julia -m ITensorFormatter [<options>] <path>...")
println(io)
printstyled(io, "DESCRIPTION", bold = true)
println(io)
println(
io, """
`ITensorFormatter.main` (typically invoked as `julia -m ITensorFormatter`)
formats Julia source code using the ITensorFormatter.jl formatter.
"""
)
printstyled(io, "OPTIONS", bold = true)
println(io)
println(
io, """
<path>...
Input path(s) (files and/or directories) to process. For directories,
all files (recursively) with the '*.jl' suffix are used as input files.

--help
Print this message.

--version
Print ITensorFormatter and julia version information.
"""
)
return
end

function print_version()
print(stdout, "itfmt version ")
print(stdout, ITENSORFORMATTER_VERSION)
print(stdout, ", julia version ")
print(stdout, VERSION)
println(stdout)
return
end

"""
ITensorFormatter.main(argv)

Format Julia source files. Primarily formats using Runic formatting, but additionally
organizes using/import statements by merging adjacent blocks, sorting modules and symbols,
and line-wrapping. Accepts file paths and directories as arguments. Options starting with
`--` are forwarded to Runic, see the
[Runic documentation](https://github.com/fredrikekre/Runic.jl) for more details.
and line-wrapping. Accepts file paths and directories as arguments.

# Examples
```julia-repl
julia> using ITensorFormatter: ITensorFormatter

julia> ITensorFormatter.main(["."]);

julia> ITensorFormatter.main(["file1.jl", "file2.jl"]);

```
"""
function main(argv)
argv_options = filter(startswith("--"), argv)
if !isempty(argv_options)
if "--help" in argv_options
print_help()
return 0
elseif "--version" in argv_options
print_version()
return 0
else
return error("Options not supported: `$argv_options`.")
end
end
# `argv` doesn't have any options, so treat all arguments as file/directory paths.
isempty(argv) && return error("No input paths provided.")
inputfiles = String[]
x = filter(!startswith("--"), argv)
for x in argv
if startswith(x, "--")
# Ignore options for now, they are assumed to be for Runic.
elseif isdir(x)
if isdir(x)
Runic.scandir!(inputfiles, x)
else # isfile(x)
elseif isfile(x)
push!(inputfiles, x) # Assume it is a file for now
else
error("Input path is not a file or directory: `$x`.")
end
end
isempty(inputfiles) && return 0
for inputfile in inputfiles
content = organize_import_file(inputfile)
content = organize_import_blocks_file(inputfile)
write(inputfile, content)
end
Runic.main(argv)
pushfirst!(inputfiles, "--inplace")
Runic.main(inputfiles)
return 0
end

Expand Down
Loading
Loading