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
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.0.5] - Unreleased

### Added
- New path predicates: `hasParentMatching`, `hasAncestorMatching`, `hasDirectChildMatching`, `hasDescendantMatching`, `hasSiblingMatching`, `hasFullPathMatchingGlob`, `hasFullPathMatching`, `hasNameMatchingGlob`, `hasNameStartingWith`
- New path matchers:
`hasParentMatching`, `hasAncestorMatching`, `hasDirectChildMatching`, `hasDescendantMatching`, `hasSiblingMatching`,
`hasAbsolutePathMatchingGlob`, `hasAbsolutePathMatching`,
`hasRelativePathMatchingGlob`, `hasRelativePathMatching`,
`hasNameMatchingGlob`, `hasNameStartingWith`

### Changed
- `PathUtils` removed, `PathPredicates` rework
- Line extension: empty string is permitted
- Filtering: split into distinct directories and files filters
- Filtering: now using `PathMatcher` instead of `Predicate<Path>`
- Filtering: split into distinct directories and files filters for better control
- `PathUtils` and `PathPredicates` removed, use `PathMatchers` instead
- Line extension: empty string is permitted to force line break in compact paths

### Fixed
- The folder name is properly displayed at root when calling `prettyPrint(".")` (instead of "./")
Expand Down
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,14 @@ child_limit_static/
Or you can also set a limitation function, to dynamically choose the number of children displayed in each directory.
It avoids cluttering the whole console with known large folders (e.g. `node_modules`) but continue to pretty print normally other folders.

Use the `ChildLimitBuilder` and `PathPredicates` classes to help you build the limit function that fits your needs.
Use the `ChildLimitBuilder` and `PathMatchers` classes to help you build the limit function that fits your needs.

```java
// Example: ChildLimitDynamic.java
var isNodeModulePredicate = PathPredicates.builder().hasName("node_modules").build();
var isNodeModuleMatcher = PathMatchers.hasName("node_modules");
var childLimit = ChildLimitBuilder.builder()
.defaultLimit(ChildLimitBuilder.UNLIMITED)
.limit(isNodeModulePredicate, 0)
.limit(isNodeModuleMatcher, 0)
.build();
var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(options -> options.withChildLimit(childLimit))
Expand Down Expand Up @@ -274,23 +274,23 @@ sorting/
```

## Filtering
Files and directories can be selectively included or excluded using a custom `Predicate<Path>`.
Files and directories can be selectively included or excluded using a custom `PathMatcher`.

Filtering is independant for files & directories. Files are filtered only if their parent directory pass the directory filter.
If none of some directory's children match, the directory is still displayed.

The `PathPredicates` class provides several ready-to-use methods for creating common predicates, as well as a builder for creating more advanced predicates.
The `PathMatchers` class provides several ready-to-use methods for creating and combining common matchers.

```java
// Example: Filtering.java
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
var isJavaFilePredicate = PathPredicates.builder().hasExtension("java").build();
var excludeDirWithNoJavaFiles = PathMatchers.not(PathMatchers.hasNameEndingWith("no_java_file"));
var hasJavaExtension = PathMatchers.hasExtension("java");

var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(
options -> options
.filterDirectories(excludeDirWithNoJavaFiles)
.filterFiles(hasJavaExtensionPredicate)
.filterFiles(hasJavaExtension)
)
.build();
```
Expand All @@ -315,17 +315,19 @@ If the function returns `null`, nothing is added.

```java
// Example: LineExtension.java
var printedPath = Path.of("src/example/resources/line_extension");

Function<Path, String> lineExtension = path -> {
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/api")) {
if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/api", printedPath).matches(path)) {
return "\t\t\t// All API code: controllers, etc.";
}
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/domain")) {
if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/domain", printedPath).matches(path)) {
return "\t\t\t// All domain code: value objects, etc.";
}
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/infra")) {
if (PathMatchers.hasRelativePathMatchingGlob("src/main/java/infra", printedPath).matches(path)) {
return "\t\t\t// All infra code: database, email service, etc.";
}
if (PathPredicates.hasNameMatchingGlob(path, "*.properties")) {
if (PathMatchers.hasNameMatchingGlob("*.properties").matches(path)) {
return "\t// Config file";
}
return null;
Expand Down
44 changes: 25 additions & 19 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
# Roadmap

## Done
- [x] Option: tree format Unicode box drawing / classic ASCII
- [x] Option: use emojis
- [x] Option: children limit (static & dynamic)
- [x] Option: compact directories display
- [x] Option: max directory depth
- [x] Add examples & README
- [x] Use JSpecify annotations
- [x] Unit tests, using @TempDir
- [x] Mutation testing
- [x] Pre-defined Path predicates
- [x] Publish on Maven Central!
- [x] Child limitation function helper
- [x] More default emojis
- [x] Option: Filtering
- [x] Option: Ordering
- [x] Use Github wiki to document options instead of readme
- [x] Jacoco coverage report
- [x] Option: Line extension (=additional text after the file name)
- [x] **Features**
- [x] Option: filtering
- [x] Option: ordering
- [x] Option: emojis
- [x] Option: compact directories display
- [x] Option: line extension (=additional text after the file name)
- [x] Option: children limit (static & dynamic)
- [x] Option: tree format Unicode box drawing / classic ASCII
- [x] Option: max directory depth
- [x] **Documentation**
- [x] Add examples & README
- [x] Use Github wiki to document options instead of readme
- [x] **Code style**
- [x] Use JSpecify annotations
- [x] **Testing**
- [x] Unit tests, using @TempDir
- [x] Jacoco coverage report
- [x] Mutation testing
- [x] SonarCloud integration
- [x] **Workflows**
- [x] Github actions
- [x] Publish on Maven Central!

## To do
- [x] More `PathPredicates` functions!
- [x] More `PathMatchers` functions!
- [ ] Helper class for line extension
- [ ] Option: custom emojis
- [ ] Rework/fix Github wiki to be up to date

## Backlog / To analyze / To implement if requested
- [ ] Option: custom tree format
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder;
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;

public class ChildLimitDynamic {

public static void main(String[] args) {
var isNodeModulePredicate = PathPredicates.builder().hasName("node_modules").build();
var isNodeModuleMatcher = PathMatchers.hasName("node_modules");
var childLimit = ChildLimitBuilder.builder()
.defaultLimit(ChildLimitBuilder.UNLIMITED)
.limit(isNodeModulePredicate, 0)
.limit(isNodeModuleMatcher, 0)
.build();
var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(options -> options.withChildLimit(childLimit))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import io.github.computerdaddyguy.jfiletreeprettyprinter.ChildLimitBuilder;
import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PrettyPrintOptions.Sorts;
import java.nio.file.Path;
import java.util.function.Function;
Expand All @@ -11,39 +11,40 @@ public class CompleteExample {

public static void main(String[] args) {

var filterDir = PathPredicates.builder()
.pathTest(path -> !PathPredicates.hasName(path, ".git"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.git"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.github"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/.settings"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/example"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/src/test"))
.pathTest(path -> !PathPredicates.hasFullPathMatchingGlob(path, "**/target"))
.build();
var jFileTreePrettyPrintFolder = Path.of(".");

var filterFiles = PathPredicates.builder()
.pathTest(path -> !PathPredicates.hasNameStartingWith(path, "."))
.pathTest(path -> {
if (PathPredicates.hasParentMatching(path, parent -> PathPredicates.hasName(parent, "jfiletreeprettyprinter"))) {
return PathPredicates.hasName(path, "FileTreePrettyPrinter.java");
}
return true;
})
.build();
var filterDir = PathMatchers.noneOf(
PathMatchers.hasName(".git"),
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, ".git"),
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, ".github"),
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, ".settings"),
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, "src/example"),
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, "src/test"),
PathMatchers.hasRelativePathMatchingGlob(jFileTreePrettyPrintFolder, "target")
);

var filterFiles = PathMatchers.allOf(
PathMatchers.not(PathMatchers.hasNameStartingWith(".")),
PathMatchers.ifMatchesThenElse(
PathMatchers.hasDirectParentMatching(PathMatchers.hasName("jfiletreeprettyprinter")), // if
PathMatchers.hasName("FileTreePrettyPrinter.java"), // then
p -> true // else
)
);

var childLimitFunction = ChildLimitBuilder.builder()
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0)
.limit(path -> PathPredicates.hasFullPathMatchingGlob(path, "**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0)
.limit(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/renderer"), 0)
.limit(PathMatchers.hasAbsolutePathMatchingGlob("**/io/github/computerdaddyguy/jfiletreeprettyprinter/scanner"), 0)
.build();

Function<Path, String> lineExtension = path -> {
if (PathPredicates.hasName(path, "JfileTreePrettyPrinter-structure.png")) {
if (PathMatchers.hasName("JfileTreePrettyPrinter-structure.png").matches(path)) {
return "\t// This image";
} else if (PathPredicates.hasName(path, "FileTreePrettyPrinter.java")) {
} else if (PathMatchers.hasName("FileTreePrettyPrinter.java").matches(path)) {
return "\t// Main entry point";
} else if (PathPredicates.hasName(path, "README.md")) {
} else if (PathMatchers.hasName("README.md").matches(path)) {
return "\t\t// You're reading at this!";
} else if (PathPredicates.hasName(path, "java")) {
} else if (PathMatchers.hasName("java").matches(path)) {
return "";
}
return null;
Expand All @@ -61,7 +62,8 @@ public static void main(String[] args) {
.sort(Sorts.DIRECTORY_FIRST)
)
.build();
var tree = prettyPrinter.prettyPrint(".");

var tree = prettyPrinter.prettyPrint(jFileTreePrettyPrintFolder);
System.out.println(tree);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
package io.github.computerdaddyguy.jfiletreeprettyprinter.example;

import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
import java.nio.file.Path;
import java.util.function.Predicate;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;

public class Filtering {

public static void main(String[] args) {
Predicate<Path> excludeDirWithNoJavaFiles = dir -> !PathPredicates.hasNameEndingWith(dir, "no_java_file");
var hasJavaExtensionPredicate = PathPredicates.builder().hasExtension("java").build();
var excludeDirWithNoJavaFiles = PathMatchers.not(PathMatchers.hasNameEndingWith("no_java_file"));
var hasJavaExtension = PathMatchers.hasExtension("java");

var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(
options -> options
.filterDirectories(excludeDirWithNoJavaFiles)
.filterFiles(hasJavaExtensionPredicate)
.filterFiles(hasJavaExtension)
)
.build();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
package io.github.computerdaddyguy.jfiletreeprettyprinter.example;

import io.github.computerdaddyguy.jfiletreeprettyprinter.FileTreePrettyPrinter;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathPredicates;
import io.github.computerdaddyguy.jfiletreeprettyprinter.PathMatchers;
import java.nio.file.Path;
import java.util.function.Function;

public class LineExtension {

public static void main(String[] args) {
var printedPath = Path.of("src/example/resources/line_extension");

Function<Path, String> lineExtension = path -> {
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/api")) {
if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/api").matches(path)) {
return "\t\t\t// All API code: controllers, etc.";
}
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/domain")) {
if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/domain").matches(path)) {
return "\t\t\t// All domain code: value objects, etc.";
}
if (PathPredicates.hasFullPathMatchingGlob(path, "**/src/main/java/infra")) {
if (PathMatchers.hasRelativePathMatchingGlob(printedPath, "src/main/java/infra").matches(path)) {
return "\t\t\t// All infra code: database, email service, etc.";
}
if (PathPredicates.hasNameMatchingGlob(path, "*.properties")) {
if (PathMatchers.hasNameMatchingGlob("*.properties").matches(path)) {
return "\t// Config file";
}
return null;
};
var prettyPrinter = FileTreePrettyPrinter.builder()
.customizeOptions(options -> options.withLineExtension(lineExtension))
.build();
var tree = prettyPrinter.prettyPrint("src/example/resources/line_extension");
var tree = prettyPrinter.prettyPrint(printedPath);
System.out.println(tree);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.github.computerdaddyguy.jfiletreeprettyprinter;

import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import org.jspecify.annotations.NullMarked;

Expand All @@ -26,8 +26,8 @@
* <pre>{@code
* var childLimit = ChildLimitBuilder.builder()
* .defaultLimit(ChildLimit.UNLIMITED) // unlimited unless specified
* .limit(path -> PathPredicates.hasName(path, "bigDir"), 10) // max 10 children in "bigDir"
* .limit(path -> PathPredicates.hasName(path, "emptyDir"), 0) // disallow children in "emptyDir"
* .limit(PathMatchers.hasName("bigDir"), 10) // max 10 children in "bigDir"
* .limit(PathMatchers.hasName("emptyDir"), 0) // disallow children in "emptyDir"
* .build();
*
* }</pre>
Expand All @@ -53,7 +53,7 @@ private ChildLimitBuilder() {
this.defaultControl = UNLIMITED_CONTROL;
}

private record ChildControl(Predicate<Path> pathPredicate, int limit) {
private record ChildControl(PathMatcher pathMatcher, int limit) {

}

Expand All @@ -74,7 +74,7 @@ public ToIntFunction<Path> build() {
var immutControls = List.copyOf(controls);
var immutDefaultControl = this.defaultControl;
return p -> immutControls.stream()
.filter(control -> control.pathPredicate().test(p))
.filter(control -> control.pathMatcher().matches(p))
.findFirst()
.orElse(immutDefaultControl)
.limit();
Expand All @@ -93,21 +93,21 @@ public ChildLimitBuilder defaultLimit(int limit) {
}

/**
* Adds a child limit rule for paths matching the given predicate.
* Adds a child limit rule for paths matching the given matcher.
* <p>
* Rules are evaluated in the order they are added. The first matching rule wins.
* </p>
*
* @param pathPredicate the condition for paths
* @param pathMatcher the condition for paths
* @param limit the maximum number of children (use {@link #UNLIMITED} for no restriction)
*
* @return this builder for chaining
*
* @throws NullPointerException if {@code pathPredicate} is null
* @throws NullPointerException if {@code pathMatcher} is null
*/
public ChildLimitBuilder limit(Predicate<Path> pathPredicate, int limit) {
Objects.requireNonNull(pathPredicate, "pathPredicate is null");
this.controls.add(new ChildControl(pathPredicate, limit));
public ChildLimitBuilder limit(PathMatcher pathMatcher, int limit) {
Objects.requireNonNull(pathMatcher, "pathMatcher is null");
this.controls.add(new ChildControl(pathMatcher, limit));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public DefaultFileTreePrettyPrinter(PathToTreeScanner scanner, TreeEntryRenderer
@Override
public String prettyPrint(Path path) {
Objects.requireNonNull(path, "path cannot be null");
var cleanedPath = path.normalize().toAbsolutePath(); // required to avoid "./" at root when calling prettyPrint(".")
var tree = scanner.scan(cleanedPath);
var tree = scanner.scan(path);
return renderer.renderTree(tree);
}

Expand Down
Loading
Loading