Skip to content

Commit

Permalink
Create service to map file types to supported languages (stylesheets/…
Browse files Browse the repository at this point in the history
…regex-mapping)

Consumers can add their own extensions as well. Users can change file type associations to alternative languages if they want, or create entries themselves to point to a supported language.
  • Loading branch information
Col-E committed Aug 12, 2023
1 parent 5154bda commit 78cfa97
Show file tree
Hide file tree
Showing 13 changed files with 419 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.*;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Random;
Expand Down Expand Up @@ -60,6 +63,20 @@ public static String cutOffAtFirst(@Nonnull String text, @Nonnull String cutoff)
return text.substring(0, i);
}

/**
* @param text
* Input text.
* @param after
* Sequence to find and use as a cut-off point.
*
* @return Input text, after the occurrence of the given pattern.
*/
public static String getAfter(@Nonnull String text, @Nonnull String after) {
int i = text.lastIndexOf(after);
if (i < 0) return text;
return text.substring(i + after.length());
}

/**
* @param text
* Input text.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package software.coley.recaf.services.text;

import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import javafx.collections.ObservableList;
import software.coley.recaf.info.BinaryXmlFileInfo;
import software.coley.recaf.info.FileInfo;
import software.coley.recaf.info.Info;
import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.services.Service;
import software.coley.recaf.ui.LanguageStylesheets;
import software.coley.recaf.ui.control.richtext.Editor;
import software.coley.recaf.ui.control.richtext.syntax.RegexLanguages;
import software.coley.recaf.ui.control.richtext.syntax.RegexRule;
import software.coley.recaf.ui.control.richtext.syntax.RegexSyntaxHighlighter;
import software.coley.recaf.util.StringUtil;

import static software.coley.recaf.util.StringUtil.*;

/**
* Provides mapping of {@link FileInfo#getName() file extensions} to {@link RegexRule language patterns}
* for use by {@link RegexSyntaxHighlighter}.
* <p>
* Users can change which highlighter is used for different file extensions by updating the
* {@link FileTypeAssociationServiceConfig#getExtensionsToLangKeys() extensions map config}.
*
* @author Matt Coley
*/
@ApplicationScoped
public class FileTypeAssociationService implements Service {
public static final String SERVICE_ID = "file-type-association";
private final FileTypeAssociationServiceConfig config;

@Inject
public FileTypeAssociationService(@Nonnull FileTypeAssociationServiceConfig config) {
this.config = config;
}

/**
* Updates the syntax highlighter and stylesheet of the given editor to match the file contents of the given info object.
* <br>
* {@link JvmClassInfo} will use {@code java} syntax highlighting. {@link FileInfo} will use the file extension in their names
* to determine which syntax to use.
*
* @param info
* Info to target.
* @param editor
* Editor to update.
*/
public void configureEditorSyntax(@Nonnull Info info, @Nonnull Editor editor) {
String fileExtension;
if (info.isClass()) fileExtension = "java";
else if (info instanceof BinaryXmlFileInfo) fileExtension = "xml";
else fileExtension = getAfter(info.getName(), ".");
configureEditorSyntax(fileExtension, editor);
}

/**
* Updates the syntax highlighter and stylesheet of the given editor to match the file contents of the given file type.
*
* @param fileExtension
* File extension to target.
* @param editor
* Editor to update.
*/
public void configureEditorSyntax(@Nonnull String fileExtension, @Nonnull Editor editor) {
String lowerExtension = fileExtension.toLowerCase();
String mappedExtension = config.getExtensionsToLangKeys().getOrDefault(lowerExtension, lowerExtension);

String sheet = LanguageStylesheets.getLanguageStylesheet(mappedExtension);
RegexRule language = RegexLanguages.getLanguage(mappedExtension);

ObservableList<String> stylesheets = editor.getStylesheets();
if (sheet != null && !stylesheets.contains(sheet))
stylesheets.add(sheet);
if (language != null)
editor.setSyntaxHighlighter(new RegexSyntaxHighlighter(language));
}

@Nonnull
@Override
public String getServiceId() {
return SERVICE_ID;
}

@Nonnull
@Override
public FileTypeAssociationServiceConfig getServiceConfig() {
return config;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package software.coley.recaf.services.text;

import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import software.coley.collections.tuple.Pair;
import software.coley.observables.ObservableMap;
import software.coley.recaf.config.BasicConfigContainer;
import software.coley.recaf.config.BasicMapConfigValue;
import software.coley.recaf.config.ConfigGroups;
import software.coley.recaf.services.ServiceConfig;
import software.coley.recaf.ui.LanguageStylesheets;
import software.coley.recaf.ui.control.richtext.syntax.RegexLanguages;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Config for {@link FileTypeAssociationService}.
* <p>
* Retains mapping of file extensions to language keys used by:
* <ul>
* <li>{@link RegexLanguages#getLanguage(String)}</li>
* <li>{@link LanguageStylesheets#getLanguageStylesheet(String)}</li>
* </ul>
*
* @author Matt Coley
*/
@ApplicationScoped
public class FileTypeAssociationServiceConfig extends BasicConfigContainer implements ServiceConfig {
private final ExtensionMapping extensionsToLangKeys;

@Inject
public FileTypeAssociationServiceConfig() {
super(ConfigGroups.SERVICE_UI, FileTypeAssociationService.SERVICE_ID + CONFIG_SUFFIX);

extensionsToLangKeys = new ExtensionMapping(List.of(
new Pair<>("java", "java"),
new Pair<>("xml", "xml"),
new Pair<>("html", "xml"),
new Pair<>("enigma", "enigma")
));
addValue(new BasicMapConfigValue<>("extensions-to-langs", Map.class, String.class, String.class, extensionsToLangKeys));
}

/**
* @return Map of file extensions to {@link RegexLanguages} name keys.
*
* @see RegexLanguages#getLanguage(String)
*/
@Nonnull
public ExtensionMapping getExtensionsToLangKeys() {
return extensionsToLangKeys;
}

/**
* Maps file extensions to {@link RegexLanguages} entries.
*/
public static class ExtensionMapping extends ObservableMap<String, String, Map<String, String>> {
public ExtensionMapping(@Nonnull List<Pair<String, String>> extensions) {
this(extensions.stream().collect(Collectors.toMap(Pair::getLeft, Pair::getRight)));
}

public ExtensionMapping(@Nonnull Map<String, String> extensions) {
super(extensions, HashMap::new);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package software.coley.recaf.ui;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import software.coley.recaf.ui.control.richtext.syntax.RegexLanguages;

import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import static software.coley.recaf.util.StringUtil.cutOffAtFirst;
import static software.coley.recaf.util.StringUtil.shortenPath;

/**
* Languages stylesheets for syntax highlighting.
*
* @author Matt Coley
* @see RegexLanguages For retrieving matchers for the supported languages.
*/
public class LanguageStylesheets {
private static final Map<String, String> NAME_TO_PATH = new HashMap<>();
private static final String SHEET_JAVA;
private static final String SHEET_XML;
private static final String SHEET_ENIGMA;

// Prevent construction
private LanguageStylesheets() {
}

static {
SHEET_JAVA = addLanguage("/syntax/java.css");
SHEET_XML = addLanguage("/syntax/xml.css");
SHEET_ENIGMA = addLanguage("/syntax/enigma.css");
}

/**
* Adds support for the language outlined by the contents of the given file.
*
* @param path
* Full path to stylesheet file. The language name is the file name, without the extension.
*
* @return Full path to stylesheet file.
*/
@Nonnull
public static String addLanguage(@Nonnull String path) {
String name = cutOffAtFirst(shortenPath(path), ".");
return addLanguage(name, path);
}

/**
* @param name
* Language name to use as a key. Will be used in {@link #getLanguages()} and {@link #getLanguageStylesheet(String)}.
* @param path
* Full path to stylesheet file.
*
* @return Full path to stylesheet file.
*
* @see RegexLanguages#addLanguage(String, InputStream) You should assign a regex language model by the same language name.
*/
@Nonnull
public static String addLanguage(@Nonnull String name, @Nonnull String path) {
NAME_TO_PATH.put(name, path);
return path;
}

/**
* @param name
* Name of the language, matching the file path inside Recaf's {@code /syntax/} directory,
* without the {@code .css} extension. To get {@link #getJavaStylesheet()} use {@code java}.
*
* @return Language stylesheet path, or {@code null} if unknown language.
*
* @see RegexLanguages#getLanguage(String) Language to match with.
*/
@Nullable
public static String getLanguageStylesheet(String name) {
return NAME_TO_PATH.get(name);
}

/**
* @return Map of language names to stylesheet paths.
*/
@Nonnull
public static Map<String, String> getLanguages() {
return NAME_TO_PATH;
}

/**
* @return Stylesheet for Java.
*
* @see RegexLanguages#getJavaLanguage()
*/
@Nonnull
public static String getJavaStylesheet() {
return SHEET_JAVA;
}

/**
* @return Stylesheet for XML.
*
* @see RegexLanguages#getXmlLanguage()
*/
@Nonnull
public static String getXmlStylesheet() {
return SHEET_XML;
}

/**
* @return Stylesheet for Engima.
*
* @see RegexLanguages#getLangEngimaMap()
*/
@Nonnull
public static String getEnigmaStylesheet() {
return SHEET_ENIGMA;
}
}
Loading

0 comments on commit 78cfa97

Please sign in to comment.