Skip to content

Commit 8a6804c

Browse files
authored
Make installable as an app (#2)
1 parent 7841400 commit 8a6804c

6 files changed

Lines changed: 276 additions & 83 deletions

File tree

Project.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ITensorFormatter"
22
uuid = "b6bf39f1-c9d3-4bad-aad8-593d802f65fd"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
authors = ["ITensor developers <support@itensor.org> and contributors"]
55

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

14+
[apps]
15+
itfmt = {}
16+
1417
[compat]
1518
JuliaFormatter = "2.3.0"
1619
JuliaSyntax = "0.4.10"

README.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
[![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)
88
[![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)
99

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

1219
<picture>
@@ -33,19 +40,29 @@ julia> Pkg.Registry.add(url = "git@github.com:ITensor/ITensorRegistry.git")
3340
```
3441
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.
3542

36-
Then, the package can be added as usual through the package manager:
37-
38-
```julia
39-
julia> Pkg.add("ITensorFormatter")
43+
In Julia v1.12 and later, ITensorFormatter should be installed as a
44+
[Pkg app](https://pkgdocs.julialang.org/dev/apps/):
45+
```sh
46+
julia -e 'using Pkg; Pkg.Apps.add("ITensorFormatter")'
47+
```
48+
Assuming `~/.julia/bin` is in your `PATH` you can now invoke `itfmt`, e.g.:
49+
```sh
50+
# Format all files in-place in the current directory (recursively)
51+
# !! DON'T DO THIS FROM YOUR HOME DIRECTORY !!
52+
itfmt .
4053
```
4154

42-
## Examples
43-
44-
````julia
45-
using ITensorFormatter: ITensorFormatter
46-
````
55+
### Legacy installation
4756

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

5067
---
5168

bin/itfmt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
3+
# A simple driver script for invoking ITensorFormatter's main function. Put this script
4+
# somewhere in PATH and make sure it is executable. The script expects ITensorFormatter to
5+
# be installed in the `@itfmt` shared environment and julia to be available in
6+
# PATH. See installation instructions in the repository README for more
7+
# details.
8+
#
9+
# Repository: https://github.com/ITensor/ITensorFormatter.jl
10+
# SPDX-License-Identifier: Apache-2.0
11+
12+
export JULIA_LOAD_PATH="@itfmt"
13+
exec julia --startup-file=no -e 'using ITensorFormatter; exit(ITensorFormatter.main(ARGS))' -- "$@"

examples/README.jl

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
# [![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)
88
# [![Aqua](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)
99

10+
# ITensorFormatter.jl is a code formatting tool for Julia source files. It primarily
11+
# uses the [Runic.jl](https://github.com/fredrikekre/Runic.jl) code formatter, but also
12+
# organizes using/import statements by merging adjacent blocks, sorting modules and
13+
# symbols, and line-wrapping (similar to, and based off of, the using/import statement
14+
# organization functionality in
15+
# [LanguageServer.jl](https://github.com/julia-vscode/LanguageServer.jl)).
16+
1017
# ## Support
1118
#
1219
# {CCQ_LOGO}
@@ -33,15 +40,30 @@ julia> Pkg.Registry.add(url = "git@github.com:ITensor/ITensorRegistry.git")
3340
=#
3441
# 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.
3542

36-
# Then, the package can be added as usual through the package manager:
37-
43+
# In Julia v1.12 and later, ITensorFormatter should be installed as a
44+
# [Pkg app](https://pkgdocs.julialang.org/dev/apps/):
3845
#=
39-
```julia
40-
julia> Pkg.add("ITensorFormatter")
46+
```sh
47+
julia -e 'using Pkg; Pkg.Apps.add("ITensorFormatter")'
48+
```
49+
=#
50+
# Assuming `~/.julia/bin` is in your `PATH` you can now invoke `itfmt`, e.g.:
51+
#=
52+
```sh
53+
# Format all files in-place in the current directory (recursively)
54+
# !! DON'T DO THIS FROM YOUR HOME DIRECTORY !!
55+
itfmt .
4156
```
4257
=#
4358

44-
# ## Examples
59+
# ### Legacy installation
4560

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

src/ITensorFormatter.jl

Lines changed: 132 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -21,50 +21,38 @@ function find_using_or_import(x)
2121
return nothing
2222
else
2323
for child in children(x)
24-
x = find_using_or_import(child)
25-
isnothing(x) || return x
24+
result = find_using_or_import(child)
25+
isnothing(result) || return result
2626
end
2727
return nothing
2828
end
2929
end
3030

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

33-
function organize_import_file(f)
34-
jst = parseall(SyntaxNode, read(f, String))
35-
return organize_import_block(jst)
33+
function organize_import_blocks_string(s)
34+
jst = parseall(SyntaxNode, s)
35+
return organize_import_blocks(jst)
3636
end
37-
38-
function organize_import_block(input)
39-
# Collect all sibling blocks that are also using/import expressions
40-
41-
x = find_using_or_import(input)
42-
isnothing(x) && return JuliaSyntax.sourcetext(input)
43-
44-
siblings = []
45-
46-
child_nodes = children(x)
47-
first_ind = findfirst(is_using_or_import, child_nodes)
48-
49-
for ind in first_ind:length(child_nodes)
50-
if is_using_or_import(child_nodes[ind])
51-
push!(siblings, child_nodes[ind])
52-
else
53-
break
54-
end
37+
organize_import_blocks_file(f) = organize_import_blocks_string(read(f, String))
38+
39+
# Sort symbols, but keep the module self-reference first if present
40+
function sort_with_self_first(syms, self)
41+
self′ = pop!(syms, self, nothing)
42+
sorted = sort!(collect(syms))
43+
if self′ !== nothing
44+
pushfirst!(sorted, self)
5545
end
46+
return sorted
47+
end
5648

57-
src = JuliaSyntax.sourcetext(input)
58-
59-
# Collect all modules and symbols
49+
# Organize a single block of adjacent import/using statements
50+
function organize_import_block(siblings, node_text)
6051
using_mods = Set{String}()
6152
using_syms = Dict{String, Set{String}}()
6253
import_mods = Set{String}()
6354
import_syms = Dict{String, Set{String}}()
6455

65-
# Extract the source text of a node, trimming whitespace
66-
node_text(x) = strip(src[char_range(x)])
67-
6856
for s in siblings
6957
isusing = kind(s) === K"using"
7058
for a in children(s)
@@ -86,21 +74,6 @@ function organize_import_block(input)
8674
end
8775
end
8876

89-
# Rejoin and sort
90-
# TODO: Currently regular string sorting is used, which roughly will correspond to
91-
# BlueStyle (modules, types, ..., functions) since usually CamelCase is used for
92-
# modules, types, etc, but possibly this can be improved by using information
93-
# available from SymbolServer
94-
# Sort symbols, but keep the module self-reference first if present
95-
function sort_with_self_first(syms, self)
96-
self′ = pop!(syms, self, nothing)
97-
sorted = sort!(collect(syms))
98-
if self′ !== nothing
99-
pushfirst!(sorted, self)
100-
end
101-
return sorted
102-
end
103-
10477
import_lines = String[]
10578
for m in import_mods
10679
push!(import_lines, "import " * m)
@@ -121,43 +94,142 @@ function organize_import_block(input)
12194
join(io, sort!(using_lines), "\n")
12295
str_to_fmt = String(take!(io))
12396

124-
# Line wrap the using/import statements only
125-
formatted = JuliaFormatter.format_text(str_to_fmt; join_lines_based_on_source = true)
97+
return JuliaFormatter.format_text(str_to_fmt; join_lines_based_on_source = true)
98+
end
12699

127-
first_pos = first(char_range(siblings[1]))
128-
last_pos = last(char_range(siblings[end]))
100+
function organize_import_blocks(input)
101+
src = JuliaSyntax.sourcetext(input)
102+
x = find_using_or_import(input)
103+
isnothing(x) && return src
129104

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

132-
return content
107+
# Find all groups of adjacent import/using statements
108+
groups = Vector{Any}[]
109+
i = 1
110+
while i <= length(child_nodes)
111+
if is_using_or_import(child_nodes[i])
112+
group_start = i
113+
while i <= length(child_nodes) && is_using_or_import(child_nodes[i])
114+
i += 1
115+
end
116+
push!(groups, child_nodes[group_start:(i - 1)])
117+
else
118+
i += 1
119+
end
120+
end
121+
122+
# Extract the source text of a node, trimming whitespace
123+
node_text(n) = strip(src[char_range(n)])
124+
125+
# Process each group from right to left to preserve positions
126+
for siblings in reverse(groups)
127+
formatted = organize_import_block(siblings, node_text)
128+
first_pos = first(char_range(siblings[1]))
129+
last_pos = last(char_range(siblings[end]))
130+
src = src[1:(first_pos - 1)] * chomp(formatted) * src[(last_pos + 1):end]
131+
end
132+
133+
return src
134+
end
135+
136+
const ITENSORFORMATTER_VERSION = pkgversion(@__MODULE__)
137+
138+
# Print a typical cli program help message
139+
function print_help()
140+
io = stdout
141+
printstyled(io, "NAME", bold = true)
142+
println(io)
143+
println(io, " ITensorFormatter.main - format Julia source code")
144+
println(io)
145+
printstyled(io, "SYNOPSIS", bold = true)
146+
println(io)
147+
println(io, " julia -m ITensorFormatter [<options>] <path>...")
148+
println(io)
149+
printstyled(io, "DESCRIPTION", bold = true)
150+
println(io)
151+
println(
152+
io, """
153+
`ITensorFormatter.main` (typically invoked as `julia -m ITensorFormatter`)
154+
formats Julia source code using the ITensorFormatter.jl formatter.
155+
"""
156+
)
157+
printstyled(io, "OPTIONS", bold = true)
158+
println(io)
159+
println(
160+
io, """
161+
<path>...
162+
Input path(s) (files and/or directories) to process. For directories,
163+
all files (recursively) with the '*.jl' suffix are used as input files.
164+
165+
--help
166+
Print this message.
167+
168+
--version
169+
Print ITensorFormatter and julia version information.
170+
"""
171+
)
172+
return
173+
end
174+
175+
function print_version()
176+
print(stdout, "itfmt version ")
177+
print(stdout, ITENSORFORMATTER_VERSION)
178+
print(stdout, ", julia version ")
179+
print(stdout, VERSION)
180+
println(stdout)
181+
return
133182
end
134183

135184
"""
136185
ITensorFormatter.main(argv)
137186
138187
Format Julia source files. Primarily formats using Runic formatting, but additionally
139188
organizes using/import statements by merging adjacent blocks, sorting modules and symbols,
140-
and line-wrapping. Accepts file paths and directories as arguments. Options starting with
141-
`--` are forwarded to Runic, see the
142-
[Runic documentation](https://github.com/fredrikekre/Runic.jl) for more details.
189+
and line-wrapping. Accepts file paths and directories as arguments.
190+
191+
# Examples
192+
```julia-repl
193+
julia> using ITensorFormatter: ITensorFormatter
194+
195+
julia> ITensorFormatter.main(["."]);
196+
197+
julia> ITensorFormatter.main(["file1.jl", "file2.jl"]);
198+
199+
```
143200
"""
144201
function main(argv)
202+
argv_options = filter(startswith("--"), argv)
203+
if !isempty(argv_options)
204+
if "--help" in argv_options
205+
print_help()
206+
return 0
207+
elseif "--version" in argv_options
208+
print_version()
209+
return 0
210+
else
211+
return error("Options not supported: `$argv_options`.")
212+
end
213+
end
214+
# `argv` doesn't have any options, so treat all arguments as file/directory paths.
215+
isempty(argv) && return error("No input paths provided.")
145216
inputfiles = String[]
146-
x = filter(!startswith("--"), argv)
147217
for x in argv
148-
if startswith(x, "--")
149-
# Ignore options for now, they are assumed to be for Runic.
150-
elseif isdir(x)
218+
if isdir(x)
151219
Runic.scandir!(inputfiles, x)
152-
else # isfile(x)
220+
elseif isfile(x)
153221
push!(inputfiles, x) # Assume it is a file for now
222+
else
223+
error("Input path is not a file or directory: `$x`.")
154224
end
155225
end
226+
isempty(inputfiles) && return 0
156227
for inputfile in inputfiles
157-
content = organize_import_file(inputfile)
228+
content = organize_import_blocks_file(inputfile)
158229
write(inputfile, content)
159230
end
160-
Runic.main(argv)
231+
pushfirst!(inputfiles, "--inplace")
232+
Runic.main(inputfiles)
161233
return 0
162234
end
163235

0 commit comments

Comments
 (0)