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
101 changes: 101 additions & 0 deletions builtins/src/main/java/org/jline/builtins/ClasspathResourceUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) 2002-2025, the original author(s).
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package org.jline.builtins;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.*;
import java.util.HashMap;

/**
* Utility class for working with classpath resources.
* <p>
* This utility provides methods to convert classpath resources to Path objects,
* which can be used with JLine's configuration classes like ConfigurationPath.
* </p>
*/
public class ClasspathResourceUtil {

/**
* Converts a classpath resource to a Path.
*
* @param name The resource name (e.g., "/nano/jnanorc")
* @return The Path to the resource
* @throws IOException If an I/O error occurs
* @throws URISyntaxException If the resource URI is invalid
*/
public static Path getResourcePath(String name) throws IOException, URISyntaxException {
return getResourcePath(name, ClasspathResourceUtil.class.getClassLoader());
}

/**
* Converts a classpath resource to a Path.
*
* @param name The resource name (e.g., "/nano/jnanorc")
* @param clazz The class to use for resource loading
* @return The Path to the resource
* @throws IOException If an I/O error occurs
* @throws URISyntaxException If the resource URI is invalid
*/
public static Path getResourcePath(String name, Class<?> clazz) throws IOException, URISyntaxException {
URL resource = clazz.getResource(name);
if (resource == null) {
throw new IOException("Resource not found: " + name);
}
return getResourcePath(resource);
}

/**
* Converts a classpath resource to a Path.
*
* @param name The resource name (e.g., "/nano/jnanorc")
* @param classLoader The ClassLoader to use for resource loading
* @return The Path to the resource
* @throws IOException If an I/O error occurs
* @throws URISyntaxException If the resource URI is invalid
*/
public static Path getResourcePath(String name, ClassLoader classLoader) throws IOException, URISyntaxException {
URL resource = classLoader.getResource(name);
if (resource == null) {
throw new IOException("Resource not found: " + name);
}
return getResourcePath(resource);
}

/**
* Converts a URL to a Path.
*
* @param resource The URL to convert
* @return The Path to the resource
* @throws IOException If an I/O error occurs
* @throws URISyntaxException If the resource URI is invalid
*/
public static Path getResourcePath(URL resource) throws IOException, URISyntaxException {
URI uri = resource.toURI();
String scheme = uri.getScheme();

if (scheme.equals("file")) {
return Paths.get(uri);
}

if (!scheme.equals("jar")) {
throw new IllegalArgumentException("Cannot convert to Path: " + uri);
}

String s = uri.toString();
int separator = s.indexOf("!/");
String entryName = s.substring(separator + 2);
URI fileURI = URI.create(s.substring(0, separator));

FileSystem fs = FileSystems.newFileSystem(fileURI, new HashMap<>());
return fs.getPath(entryName);
}
}
35 changes: 32 additions & 3 deletions builtins/src/main/java/org/jline/builtins/ConfigurationPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
* for configuration files first in the user's configuration directory, then falling back
* to the application's configuration directory.
* </p>
* <p>
* This class also supports loading configuration files from the classpath. The application
* configuration directory can be a classpath resource path, which will be resolved using
* the ClasspathResourceUtil class.
* </p>
*/
public class ConfigurationPath {
private final Path appConfig;
Expand All @@ -36,16 +41,30 @@ public ConfigurationPath(Path appConfig, Path userConfig) {
}

/**
* Search configuration file first from userConfig and then appConfig directory. Returns null if file is not found.
* Configuration class constructor with classpath resource support.
* @param classpathResource Classpath resource path (e.g., "/nano")
* @param userConfig User private configuration directory
*/
public ConfigurationPath(String classpathResource, Path userConfig) {
this.appConfig = null;
this.userConfig = userConfig;
}

/**
* Search configuration file first from userConfig, then appConfig directory, and finally from classpath.
* Returns null if file is not found.
*
* @param name Configuration file name.
* @return Configuration file.
*
*/
public Path getConfig(String name) {
Path out = null;
// First check user config
if (userConfig != null && Files.exists(userConfig.resolve(name))) {
out = userConfig.resolve(name);
} else if (appConfig != null && Files.exists(appConfig.resolve(name))) {
}
// Then check app config directory
else if (appConfig != null && Files.exists(appConfig.resolve(name))) {
out = appConfig.resolve(name);
}
return out;
Expand Down Expand Up @@ -81,4 +100,14 @@ public Path getUserConfig(String name, boolean create) throws IOException {
}
return out;
}

/**
* Creates a ConfigurationPath from a classpath resource.
*
* @param classpathResource The classpath resource path (e.g., "/nano")
* @return A ConfigurationPath that will look for resources in the specified classpath location
*/
public static ConfigurationPath fromClasspath(String classpathResource) {
return new ConfigurationPath(classpathResource, null);
}
}
4 changes: 2 additions & 2 deletions builtins/src/main/java/org/jline/builtins/Less.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@ private void parseConfig(Path file) throws IOException {
if (!line.isEmpty() && !line.startsWith("#")) {
List<String> parts = SyntaxHighlighter.RuleSplitter.split(line);
if (parts.get(0).equals(COMMAND_INCLUDE)) {
SyntaxHighlighter.nanorcInclude(parts.get(1), syntaxFiles);
SyntaxHighlighter.nanorcInclude(file, parts.get(1), syntaxFiles);
} else if (parts.get(0).equals(COMMAND_THEME)) {
SyntaxHighlighter.nanorcTheme(parts.get(1), syntaxFiles);
SyntaxHighlighter.nanorcTheme(file, parts.get(1), syntaxFiles);
} else if (parts.size() == 2
&& (parts.get(0).equals("set") || parts.get(0).equals("unset"))) {
String option = parts.get(1);
Expand Down
6 changes: 3 additions & 3 deletions builtins/src/main/java/org/jline/builtins/Nano.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ protected class Buffer {

protected Buffer(String file) {
this.file = file;
this.syntaxHighlighter = SyntaxHighlighter.build(file != null ? root.resolve(file) : null, syntaxName);
this.syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, file, syntaxName, nanorcIgnoreErrors);
}

public void setDirty(boolean dirty) {
Expand Down Expand Up @@ -1845,9 +1845,9 @@ private void parseConfig(Path file) throws IOException {
if (!line.isEmpty() && !line.startsWith("#")) {
List<String> parts = SyntaxHighlighter.RuleSplitter.split(line);
if (parts.get(0).equals(COMMAND_INCLUDE)) {
SyntaxHighlighter.nanorcInclude(parts.get(1), syntaxFiles);
SyntaxHighlighter.nanorcInclude(file, parts.get(1), syntaxFiles);
} else if (parts.get(0).equals(COMMAND_THEME)) {
SyntaxHighlighter.nanorcTheme(parts.get(1), syntaxFiles);
SyntaxHighlighter.nanorcTheme(file, parts.get(1), syntaxFiles);
} else if (parts.size() == 2
&& (parts.get(0).equals("set") || parts.get(0).equals("unset"))) {
String option = parts.get(1);
Expand Down
41 changes: 29 additions & 12 deletions builtins/src/main/java/org/jline/builtins/SyntaxHighlighter.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,9 @@ public static SyntaxHighlighter build(Path nanorc, String syntaxName) {
if (!line.isEmpty() && !line.startsWith("#")) {
List<String> parts = RuleSplitter.split(line);
if (parts.get(0).equals(COMMAND_INCLUDE)) {
nanorcInclude(parts.get(1), syntaxFiles);
nanorcInclude(nanorc, parts.get(1), syntaxFiles);
} else if (parts.get(0).equals(COMMAND_THEME)) {
nanorcTheme(parts.get(1), syntaxFiles);
nanorcTheme(nanorc, parts.get(1), syntaxFiles);
}
}
}
Expand All @@ -165,37 +165,54 @@ public static SyntaxHighlighter build(Path nanorc, String syntaxName) {
return out;
}

protected static void nanorcInclude(String parameter, List<Path> syntaxFiles) throws IOException {
addFiles(parameter, s -> s.forEach(syntaxFiles::add));
protected static void nanorcInclude(Path nanorc, String parameter, List<Path> syntaxFiles) throws IOException {
addFiles(nanorc, parameter, s -> s.forEach(syntaxFiles::add));
}

protected static void nanorcTheme(String parameter, List<Path> syntaxFiles) throws IOException {
addFiles(parameter, s -> s.findFirst().ifPresent(p -> syntaxFiles.add(0, p)));
protected static void nanorcTheme(Path nanorc, String parameter, List<Path> syntaxFiles) throws IOException {
addFiles(nanorc, parameter, s -> s.findFirst().ifPresent(p -> syntaxFiles.add(0, p)));
}

protected static void addFiles(String parameter, Consumer<Stream<Path>> consumer) throws IOException {
protected static void addFiles(Path nanorc, String parameter, Consumer<Stream<Path>> consumer) throws IOException {
if (parameter.contains("*") || parameter.contains("?")) {
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parameter);
try (Stream<Path> pathStream = Files.walk(Paths.get(new File(parameter).getParent()))) {
PathMatcher pathMatcher = nanorc.getFileSystem().getPathMatcher("glob:" + parameter);
try (Stream<Path> pathStream =
Files.walk(nanorc.resolveSibling(parameter).getParent())) {
consumer.accept(pathStream.filter(pathMatcher::matches));
}
} else {
consumer.accept(Stream.of(Paths.get(parameter)));
consumer.accept(Stream.of(nanorc.resolveSibling(parameter)));
}
}

/**
* Build SyntaxHighlighter
* <p>
* This method builds a SyntaxHighlighter from a URL or classpath resource.
* The URL can be a file URL, an HTTP URL, or a classpath resource URL.
* </p>
* <p>
* For classpath resources, use the "classpath:" prefix followed by the resource path.
* For example: "classpath:/nano/jnanorc"
* </p>
*
* @param nanorcUrl Url of nanorc file
* @param nanorcUrl URL or classpath resource path of nanorc file
* @return SyntaxHighlighter
*/
public static SyntaxHighlighter build(String nanorcUrl) {
SyntaxHighlighter out = new SyntaxHighlighter(nanorcUrl);
InputStream inputStream;
try {
if (nanorcUrl.startsWith("classpath:")) {
inputStream = new Source.ResourceSource(nanorcUrl.substring(10), null).read();
String resourcePath = nanorcUrl.substring(10);
try {
// Try to get the resource as a Path first
Path resourceAsPath = ClasspathResourceUtil.getResourcePath(resourcePath);
inputStream = Files.newInputStream(resourceAsPath);
} catch (Exception e) {
// Fall back to direct resource loading if Path conversion fails
inputStream = new Source.ResourceSource(resourcePath, null).read();
}
} else {
inputStream = new Source.URLSource(new URI(nanorcUrl).toURL(), null).read();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2002-2025, the original author(s).
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* https://opensource.org/licenses/BSD-3-Clause
*/
package org.jline.builtins;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;

import org.jline.utils.AttributedString;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

/**
* Test for using ConfigurationPath with classpath resources.
*/
public class ClasspathConfigurationPathTest {

@Test
public void testClasspathConfigurationPath() throws Exception {
// Get the resource path for the nanorc file
Path appConfig = getResourcePath("/nano/jnanorc").getParent();

// Create a ConfigurationPath with the classpath resource
ConfigurationPath configPath = new ConfigurationPath(appConfig, null);

// Verify we can get the config file
Path nanorcPath = configPath.getConfig("jnanorc");
assertNotNull(nanorcPath, "Should find jnanorc file");
assertTrue(Files.exists(nanorcPath), "jnanorc file should exist");

// Test that we can use the config file with SyntaxHighlighter
SyntaxHighlighter highlighter = SyntaxHighlighter.build(nanorcPath, "java");
assertNotNull(highlighter, "Highlighter should not be null");

// Test highlighting some Java code with keywords that should be highlighted
String javaCode = "public class Test { private static final int x = 42; }";
AttributedString highlighted = highlighter.highlight(javaCode);
assertNotNull(highlighted, "Highlighted text should not be null");

// The length of the text remains the same
assertEquals(
javaCode.length(),
highlighted.length(),
"Highlighted text should have the same length as original text");

// Just verify the highlighter was created successfully
// We can't reliably test the actual highlighting in a unit test
// since it depends on the terminal capabilities
}

/**
* Helper method to get a Path from a classpath resource.
*/
static Path getResourcePath(String name) throws IOException, URISyntaxException {
return ClasspathResourceUtil.getResourcePath(name, ClasspathConfigurationPathTest.class);
}
}
Loading
Loading