Skip to content

Commit 4acd83c

Browse files
committed
format files in batches with idea
1 parent f3c82b3 commit 4acd83c

File tree

8 files changed

+305
-26
lines changed

8 files changed

+305
-26
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
* Add batching to IDEA formatter ([#2662](https://github.com/diffplug/spotless/pull/2662))
1314

1415
## [4.0.0] - 2025-09-24
1516
### Changes

lib/src/main/java/com/diffplug/spotless/generic/IdeaStep.java

Lines changed: 188 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@
2222
import java.nio.charset.StandardCharsets;
2323
import java.nio.file.Files;
2424
import java.nio.file.Path;
25+
import java.util.ArrayList;
2526
import java.util.HashMap;
27+
import java.util.LinkedHashMap;
2628
import java.util.List;
2729
import java.util.Locale;
2830
import java.util.Map;
2931
import java.util.Objects;
3032
import java.util.Properties;
3133
import java.util.TreeMap;
3234
import java.util.UUID;
35+
import java.util.concurrent.locks.Lock;
36+
import java.util.concurrent.locks.ReentrantLock;
3337
import java.util.regex.Pattern;
34-
import java.util.stream.Collectors;
3538
import java.util.stream.Stream;
3639

3740
import javax.annotation.CheckForNull;
@@ -57,6 +60,10 @@ public final class IdeaStep {
5760

5861
public static final String IDEA_CONFIG_PATH_PROPERTY = "idea.config.path";
5962
public static final String IDEA_SYSTEM_PATH_PROPERTY = "idea.system.path";
63+
64+
/** Default batch size for formatting multiple files in a single IDEA invocation */
65+
public static final int DEFAULT_BATCH_SIZE = 100;
66+
6067
@Nonnull
6168
private final IdeaStepBuilder builder;
6269

@@ -87,6 +94,7 @@ public static final class IdeaStepBuilder {
8794
private String binaryPath = IDEA_EXECUTABLE_DEFAULT;
8895
@Nullable private String codeStyleSettingsPath;
8996
private final Map<String, String> ideaProperties = new HashMap<>();
97+
private int batchSize = DEFAULT_BATCH_SIZE;
9098

9199
@Nonnull
92100
private final File buildDir;
@@ -118,18 +126,34 @@ public IdeaStepBuilder setIdeaProperties(@Nonnull Map<String, String> ideaProper
118126
return this;
119127
}
120128

129+
/**
130+
* Sets the batch size for formatting multiple files in a single IDEA invocation.
131+
* Default is {@link #DEFAULT_BATCH_SIZE}.
132+
*
133+
* @param batchSize the maximum number of files to format in a single batch (must be >= 1)
134+
* @return this builder
135+
*/
136+
public IdeaStepBuilder setBatchSize(int batchSize) {
137+
if (batchSize < 1) {
138+
throw new IllegalArgumentException("Batch size must be at least 1, got: " + batchSize);
139+
}
140+
this.batchSize = batchSize;
141+
return this;
142+
}
143+
121144
public FormatterStep build() {
122145
return create(this);
123146
}
124147

125148
@Override
126149
public String toString() {
127-
return "IdeaStepBuilder[useDefaults=%s, binaryPath=%s, codeStyleSettingsPath=%s, ideaProperties=%s, buildDir=%s]".formatted(
150+
return "IdeaStepBuilder[useDefaults=%s, binaryPath=%s, codeStyleSettingsPath=%s, ideaProperties=%s, buildDir=%s, batchSize=%d]".formatted(
128151
this.useDefaults,
129152
this.binaryPath,
130153
this.codeStyleSettingsPath,
131154
this.ideaProperties,
132-
this.buildDir);
155+
this.buildDir,
156+
this.batchSize);
133157
}
134158
}
135159

@@ -142,6 +166,7 @@ private static final class State implements Serializable {
142166
@Nullable private final String codeStyleSettingsPath;
143167
private final boolean withDefaults;
144168
private final TreeMap<String, String> ideaProperties;
169+
private final int batchSize;
145170

146171
private State(@Nonnull IdeaStepBuilder builder) {
147172
LOGGER.debug("Creating {} state with configuration {}", NAME, builder);
@@ -150,6 +175,7 @@ private State(@Nonnull IdeaStepBuilder builder) {
150175
this.codeStyleSettingsPath = builder.codeStyleSettingsPath;
151176
this.ideaProperties = new TreeMap<>(builder.ideaProperties);
152177
this.binaryPath = resolveFullBinaryPathAndCheckVersion(builder.binaryPath);
178+
this.batchSize = builder.batchSize;
153179
}
154180

155181
private static String resolveFullBinaryPathAndCheckVersion(String binaryPath) {
@@ -211,22 +237,50 @@ private static boolean isMacOs() {
211237
return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac");
212238
}
213239

240+
/**
241+
* Represents a file to be formatted, tracking both the temporary file and the original file reference.
242+
*/
243+
private static class FileToFormat {
244+
final File tempFile;
245+
final File originalFile;
246+
247+
FileToFormat(File tempFile, File originalFile) {
248+
this.tempFile = tempFile;
249+
this.originalFile = originalFile;
250+
}
251+
}
252+
214253
private String format(IdeaStepFormatterCleanupResources ideaStepFormatterCleanupResources, String unix, File file) throws Exception {
215-
// since we cannot directly work with the file, we need to write the unix string to a temporary file
216-
File tempFile = Files.createTempFile("spotless", file.getName()).toFile();
217-
try {
218-
Files.write(tempFile.toPath(), unix.getBytes(StandardCharsets.UTF_8));
219-
List<String> params = getParams(tempFile);
220-
221-
Map<String, String> env = createEnv();
222-
LOGGER.info("Launching IDEA formatter for orig file {} with params: {} and env: {}", file, params, env);
223-
var result = ideaStepFormatterCleanupResources.runner.exec(null, env, null, params);
224-
LOGGER.debug("command finished with exit code: {}", result.exitCode());
225-
LOGGER.debug("command finished with stdout: {}",
226-
result.assertExitZero(StandardCharsets.UTF_8));
227-
return Files.readString(tempFile.toPath());
228-
} finally {
229-
Files.delete(tempFile.toPath());
254+
// Delegate to the batch formatter in the cleanup resources
255+
return ideaStepFormatterCleanupResources.formatFile(this, unix, file);
256+
}
257+
258+
/**
259+
* Formats multiple files in a single IDEA invocation.
260+
*
261+
* @param ideaStepFormatterCleanupResources the cleanup resources containing the process runner
262+
* @param filesToFormat the list of files to format
263+
* @throws Exception if formatting fails
264+
*/
265+
private void formatBatch(IdeaStepFormatterCleanupResources ideaStepFormatterCleanupResources, List<FileToFormat> filesToFormat) throws Exception {
266+
if (filesToFormat.isEmpty()) {
267+
return;
268+
}
269+
270+
LOGGER.info("Formatting batch of {} files with IDEA", filesToFormat.size());
271+
272+
List<String> params = getParamsForBatch(filesToFormat);
273+
Map<String, String> env = createEnv();
274+
275+
LOGGER.debug("Launching IDEA formatter with params: {} and env: {}", params, env);
276+
var result = ideaStepFormatterCleanupResources.runner.exec(null, env, null, params);
277+
LOGGER.debug("Batch command finished with exit code: {}", result.exitCode());
278+
LOGGER.debug("Batch command finished with stdout: {}", result.assertExitZero(StandardCharsets.UTF_8));
279+
280+
// Read back the formatted content for each file
281+
for (FileToFormat fileToFormat : filesToFormat) {
282+
String formatted = Files.readString(fileToFormat.tempFile.toPath());
283+
ideaStepFormatterCleanupResources.cacheFormattedResult(fileToFormat.originalFile, formatted);
230284
}
231285
}
232286

@@ -266,7 +320,10 @@ private File createIdeaPropertiesFile() {
266320
return ideaProps.toFile();
267321
}
268322

269-
private List<String> getParams(File file) {
323+
/**
324+
* Builds command-line parameters for formatting multiple files in a single invocation.
325+
*/
326+
private List<String> getParamsForBatch(List<FileToFormat> filesToFormat) {
270327
/* https://www.jetbrains.com/help/idea/command-line-formatter.html */
271328
var builder = Stream.<String> builder();
272329
builder.add(binaryPath);
@@ -278,13 +335,18 @@ private List<String> getParams(File file) {
278335
builder.add("-s");
279336
builder.add(codeStyleSettingsPath);
280337
}
281-
builder.add("-charset").add("UTF-8");
282-
builder.add(ThrowingEx.get(file::getCanonicalPath));
283-
return builder.build().collect(Collectors.toList());
338+
builder.add("-charset");
339+
builder.add("UTF-8");
340+
341+
// Add all file paths
342+
for (FileToFormat fileToFormat : filesToFormat) {
343+
builder.add(ThrowingEx.get(fileToFormat.tempFile::getCanonicalPath));
344+
}
345+
return builder.build().toList();
284346
}
285347

286348
private FormatterFunc.Closeable toFunc() {
287-
IdeaStepFormatterCleanupResources ideaStepFormatterCleanupResources = new IdeaStepFormatterCleanupResources(uniqueBuildFolder, new ProcessRunner());
349+
IdeaStepFormatterCleanupResources ideaStepFormatterCleanupResources = new IdeaStepFormatterCleanupResources(uniqueBuildFolder, new ProcessRunner(), batchSize);
288350
return FormatterFunc.Closeable.of(ideaStepFormatterCleanupResources, this::format);
289351
}
290352
}
@@ -294,14 +356,116 @@ private static class IdeaStepFormatterCleanupResources implements AutoCloseable
294356
private final File uniqueBuildFolder;
295357
@Nonnull
296358
private final ProcessRunner runner;
359+
private final int batchSize;
360+
361+
// Batch processing state (transient - not serialized)
362+
private final Map<File, String> formattedCache = new LinkedHashMap<>();
363+
private final List<State.FileToFormat> pendingBatch = new ArrayList<>();
364+
private final Lock batchLock = new ReentrantLock();
365+
private State currentState;
297366

298-
public IdeaStepFormatterCleanupResources(@Nonnull File uniqueBuildFolder, @Nonnull ProcessRunner runner) {
367+
public IdeaStepFormatterCleanupResources(@Nonnull File uniqueBuildFolder, @Nonnull ProcessRunner runner, int batchSize) {
299368
this.uniqueBuildFolder = uniqueBuildFolder;
300369
this.runner = runner;
370+
this.batchSize = batchSize;
371+
}
372+
373+
/**
374+
* Formats a single file, using batch processing for efficiency.
375+
* Files are accumulated and formatted in batches to minimize IDEA process startups.
376+
*/
377+
String formatFile(State state, String unix, File file) throws Exception {
378+
batchLock.lock();
379+
try {
380+
// Store the state reference for batch processing
381+
if (currentState == null) {
382+
currentState = state;
383+
}
384+
385+
// Check if we already have the formatted result cached
386+
if (formattedCache.containsKey(file)) {
387+
String result = formattedCache.remove(file);
388+
LOGGER.debug("Returning cached formatted result for file: {}", file);
389+
return result;
390+
}
391+
392+
// Create a temporary file for this content
393+
File tempFile = Files.createTempFile(uniqueBuildFolder.toPath(), "spotless", file.getName()).toFile();
394+
Files.write(tempFile.toPath(), unix.getBytes(StandardCharsets.UTF_8));
395+
396+
// Add to pending batch
397+
pendingBatch.add(new State.FileToFormat(tempFile, file));
398+
LOGGER.debug("Added file {} to pending batch (size: {})", file, pendingBatch.size());
399+
400+
// If batch is full, process it
401+
if (pendingBatch.size() >= batchSize) {
402+
LOGGER.info("Batch size reached ({}/{}), processing batch", pendingBatch.size(), batchSize);
403+
processPendingBatch();
404+
}
405+
406+
// Check cache again after potential batch processing
407+
if (formattedCache.containsKey(file)) {
408+
return formattedCache.remove(file);
409+
}
410+
411+
// If still not in cache, we need to process immediately (shouldn't happen normally)
412+
// This is a safety fallback
413+
LOGGER.warn("File {} not found in cache after batch processing, forcing immediate format", file);
414+
List<State.FileToFormat> singleFileBatch = new ArrayList<>();
415+
singleFileBatch.add(new State.FileToFormat(tempFile, file));
416+
currentState.formatBatch(this, singleFileBatch);
417+
return formattedCache.remove(file);
418+
419+
} finally {
420+
batchLock.unlock();
421+
}
422+
}
423+
424+
/**
425+
* Caches a formatted result for a file.
426+
*/
427+
void cacheFormattedResult(File originalFile, String formatted) {
428+
formattedCache.put(originalFile, formatted);
429+
}
430+
431+
/**
432+
* Processes all pending files in the current batch.
433+
*/
434+
private void processPendingBatch() throws Exception {
435+
if (pendingBatch.isEmpty() || currentState == null) {
436+
return;
437+
}
438+
439+
List<State.FileToFormat> batchToProcess = new ArrayList<>(pendingBatch);
440+
pendingBatch.clear();
441+
442+
try {
443+
currentState.formatBatch(this, batchToProcess);
444+
} finally {
445+
// Clean up temp files
446+
for (State.FileToFormat fileToFormat : batchToProcess) {
447+
try {
448+
Files.deleteIfExists(fileToFormat.tempFile.toPath());
449+
} catch (IOException e) {
450+
LOGGER.warn("Failed to delete temporary file: {}", fileToFormat.tempFile, e);
451+
}
452+
}
453+
}
301454
}
302455

303456
@Override
304457
public void close() throws Exception {
458+
batchLock.lock();
459+
try {
460+
// Process any remaining files in the batch
461+
if (!pendingBatch.isEmpty()) {
462+
LOGGER.info("Processing remaining {} files in batch on close", pendingBatch.size());
463+
processPendingBatch();
464+
}
465+
} finally {
466+
batchLock.unlock();
467+
}
468+
305469
// close the runner
306470
runner.close();
307471
// delete the unique build folder

plugin-gradle/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* Add `batchSize` to `<idea>` formatter which determines the number of files to format in a single IDEA invocation (default=100) ([#2662](https://github.com/diffplug/spotless/pull/2662))
68

79
## [8.0.0] - 2025-09-24
810
### Changed

plugin-gradle/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1657,6 +1657,9 @@ spotless {
16571657
16581658
// if idea is not on your path, you must specify the path to the executable
16591659
idea().binaryPath('/path/to/idea')
1660+
1661+
// to set the number of files per IDEA involcation (default: 100)
1662+
idea().batchSize(100)
16601663
}
16611664
}
16621665
```
@@ -1666,7 +1669,6 @@ See [here](../INTELLIJ_IDEA_SCREENSHOTS.md) for an explanation on how to extract
16661669
16671670
### Limitations
16681671
- Currently, only IntelliJ IDEA is supported - none of the other jetbrains IDE. Consider opening a PR if you want to change this.
1669-
- Launching IntelliJ IDEA from the command line is pretty expensive and as of now, we do this for each file. If you want to change this, consider opening a PR.
16701672
16711673
## Generic steps
16721674

plugin-maven/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* Add `batchSize` to `<idea>` formatter which determines the number of files to format in a single IDEA invocation (default=100) ([#2662](https://github.com/diffplug/spotless/pull/2662))
68

79
## [3.0.0] - 2025-09-24
810
### Changes

plugin-maven/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1733,6 +1733,8 @@ Spotless provides access to IntelliJ IDEA's command line formatter.
17331733
<withDefaults>false</withDefaults>
17341734
<!-- if idea is not on your path, you must specify the path to the executable -->
17351735
<binaryPath>/path/to/idea</binaryPath>
1736+
<!-- the number of files to format in one idea invocation (default: 100) -->
1737+
<batchSize>100</batchSize>
17361738
</idea>
17371739
</format>
17381740
</formats>
@@ -1744,7 +1746,6 @@ See [here](../INTELLIJ_IDEA_SCREENSHOTS.md) for an explanation on how to extract
17441746

17451747
### Limitations
17461748
- Currently, only IntelliJ IDEA is supported - none of the other jetbrains IDE. Consider opening a PR if you want to change this.
1747-
- Launching IntelliJ IDEA from the command line is pretty expensive and as of now, we do this for each file. If you want to change this, consider opening a PR.
17481749

17491750
## Generic steps
17501751

plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Idea.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,16 @@ public class Idea implements FormatterStepFactory {
3333
@Parameter
3434
private Boolean withDefaults = true;
3535

36+
@Parameter
37+
private Integer batchSize = IdeaStep.DEFAULT_BATCH_SIZE;
38+
3639
@Override
3740
public FormatterStep newFormatterStep(FormatterStepConfig config) {
3841
return IdeaStep.newBuilder(config.getFileLocator().getBuildDir())
3942
.setUseDefaults(withDefaults)
4043
.setCodeStyleSettingsPath(codeStyleSettingsPath)
4144
.setBinaryPath(binaryPath)
45+
.setBatchSize(batchSize)
4246
.build();
4347
}
4448
}

0 commit comments

Comments
 (0)