Skip to content

Commit 87a6cc3

Browse files
authored
Merge pull request #407 from Fischstaebchenn/AutoIndenter
change /format-code to add a third option to enable automatic indentation
2 parents 0cd1b8e + 0e307f9 commit 87a6cc3

File tree

6 files changed

+1494
-7
lines changed

6 files changed

+1494
-7
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package net.javadiscord.javabot.systems.user_commands.format_code;
2+
3+
4+
import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
5+
import net.dv8tion.jda.api.interactions.commands.build.Commands;
6+
import net.javadiscord.javabot.util.IndentationHelper;
7+
import net.javadiscord.javabot.util.StringUtils;
8+
import org.jetbrains.annotations.NotNull;
9+
import xyz.dynxsty.dih4jda.interactions.commands.application.ContextCommand;
10+
11+
import java.util.List;
12+
13+
/**
14+
* <h3>This class represents the "Format and Indent Code" Message Context command.</h3>
15+
*/
16+
public class FormatAndIndentCodeMessageContext extends ContextCommand.Message {
17+
/**
18+
* The constructor of this class, which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.CommandData}.
19+
*/
20+
public FormatAndIndentCodeMessageContext() {
21+
setCommandData(Commands.message("Format and Indent Code")
22+
.setGuildOnly(true)
23+
);
24+
}
25+
26+
@Override
27+
public void execute(@NotNull MessageContextInteractionEvent event) {
28+
event.replyFormat("```java\n%s\n```", IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(event.getTarget().getContentRaw()), IndentationHelper.IndentationType.TABS))
29+
.setAllowedMentions(List.of())
30+
.setComponents(FormatCodeCommand.buildActionRow(event.getTarget()))
31+
.queue();
32+
}
33+
}

src/main/java/net/javadiscord/javabot/systems/user_commands/format_code/FormatCodeCommand.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package net.javadiscord.javabot.systems.user_commands.format_code;
22

3+
import net.javadiscord.javabot.util.*;
34
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
45
import net.dv8tion.jda.api.entities.Message;
56
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
@@ -9,10 +10,6 @@
910
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
1011
import net.dv8tion.jda.api.interactions.components.ActionRow;
1112
import net.dv8tion.jda.api.interactions.components.buttons.Button;
12-
import net.javadiscord.javabot.util.Checks;
13-
import net.javadiscord.javabot.util.InteractionUtils;
14-
import net.javadiscord.javabot.util.Responses;
15-
import net.javadiscord.javabot.util.StringUtils;
1613
import org.jetbrains.annotations.Contract;
1714
import org.jetbrains.annotations.NotNull;
1815

@@ -49,7 +46,11 @@ public FormatCodeCommand() {
4946
.addChoice("SQL", "sql")
5047
.addChoice("Swift", "swift")
5148
.addChoice("TypeScript", "typescript")
52-
.addChoice("XML", "xml")
49+
.addChoice("XML", "xml"),
50+
new OptionData(OptionType.STRING,"auto-indent","The type of indentation applied to the message, does not automatically indent if left blank.",false)
51+
.addChoice("Four Spaces","FOUR_SPACES")
52+
.addChoice("Two Spaces","TWO_SPACES")
53+
.addChoice("Tabs","TABS")
5354
)
5455
);
5556
}
@@ -64,6 +65,7 @@ public FormatCodeCommand() {
6465
public void execute(@NotNull SlashCommandInteractionEvent event) {
6566
OptionMapping idOption = event.getOption("message-id");
6667
String format = event.getOption("format", "java", OptionMapping::getAsString);
68+
String indentation = event.getOption("auto-indent","null",OptionMapping::getAsString);
6769
event.deferReply().queue();
6870
if (idOption == null) {
6971
event.getChannel().getHistory()
@@ -74,7 +76,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
7476
.filter(m -> !m.getAuthor().isBot()).findFirst()
7577
.orElse(null);
7678
if (target != null) {
77-
event.getHook().sendMessageFormat("```%s\n%s\n```", format, StringUtils.standardSanitizer().compute(target.getContentRaw()))
79+
event.getHook().sendMessageFormat("```%s\n%s\n```", format, IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(target.getContentRaw()),IndentationHelper.IndentationType.valueOf(indentation)))
7880
.setAllowedMentions(List.of())
7981
.setComponents(buildActionRow(target))
8082
.queue();
@@ -89,7 +91,7 @@ public void execute(@NotNull SlashCommandInteractionEvent event) {
8991
}
9092
long messageId = idOption.getAsLong();
9193
event.getChannel().retrieveMessageById(messageId).queue(
92-
target -> event.getHook().sendMessageFormat("```%s\n%s\n```", format, StringUtils.standardSanitizer().compute(target.getContentRaw()))
94+
target -> event.getHook().sendMessageFormat("```%s\n%s\n```", format, IndentationHelper.formatIndentation(StringUtils.standardSanitizer().compute(target.getContentRaw()), IndentationHelper.IndentationType.valueOf(indentation)))
9395
.setAllowedMentions(List.of())
9496
.setComponents(buildActionRow(target))
9597
.queue(),
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package net.javadiscord.javabot.util;
2+
3+
/**
4+
* Utility class to help indent strings that contain code.
5+
*/
6+
public class IndentationHelper {
7+
/**
8+
* Enum which denotes the different types of indentation possible.
9+
* When {@link #NULL} is passed to any method in this class, the output of said method should match the input.
10+
*/
11+
public enum IndentationType {
12+
/**
13+
* A tab character (\t) should be used for indentation.
14+
*/
15+
TABS("\t"),
16+
/**
17+
* Four spaces should be used for indentation.
18+
*/
19+
FOUR_SPACES(" "),
20+
/**
21+
* Two spaces should be used for indentation.
22+
*/
23+
TWO_SPACES(" "),
24+
/**
25+
* Doesn't define an indentation type but
26+
* Is used as a substitute to null to indicate that a String given to any method in {@link IndentationHelper} should not be changed.
27+
*/
28+
NULL("");
29+
30+
/**
31+
* Holds the characters used for indentation of the type.
32+
*/
33+
private final String pattern;
34+
35+
/**
36+
* Constructs the indentation type.
37+
*
38+
* @param pattern The pattern to be used as indentation
39+
*/
40+
IndentationType(String pattern) {
41+
this.pattern = pattern;
42+
}
43+
44+
/**
45+
* Get the pattern for a given Indentation type.
46+
*
47+
* @return the pattern to be used for indenting.
48+
*/
49+
public String getPattern() {
50+
return pattern;
51+
}
52+
53+
/**
54+
* Returns the number of characters a pattern is using.
55+
*
56+
* @return the number of characters the pattern of this type consists of.
57+
*/
58+
private int getNumberOfChars() {
59+
return pattern.length();
60+
}
61+
}
62+
63+
/**
64+
* Private enum to denote the current State of the Indentation Process.
65+
*/
66+
private enum IndentationState {
67+
/**
68+
* Denotes that the Process is currently in a codeblock and should indent the code accordingly.
69+
*/
70+
CODE,
71+
/**
72+
* Denotes that the Process is currently in a String literal.
73+
*/
74+
STRING,
75+
/**
76+
* Denotes that the Indentation Process is currently in a character literal.
77+
*/
78+
CHARACTER,
79+
/**
80+
* Denotes that the Process is currently in a single line comment.
81+
*/
82+
SINGLE_LINE_COMMENT,
83+
/**
84+
* Denotes that the process is inside a multi line comment or a javadoc.
85+
*/
86+
MULTI_LINE_COMMENT
87+
}
88+
89+
private final String text;
90+
private final IndentationType type;
91+
private boolean startOfLine = true;
92+
private int numberOfBrackets = 0;
93+
private StringBuilder builder;
94+
95+
private IndentationHelper(String text, IndentationType type) {
96+
this.text = text;
97+
this.type = type;
98+
this.builder = new StringBuilder((int) (text.length() * 1.25f));
99+
}
100+
101+
/**
102+
* Aims to indent the given String using the pattern provided. Will return the String unchanged if {@link IndentationHelper.IndentationType#NULL} is passed as the IndentationType parameter.
103+
*
104+
* @param text The text that should be indented.
105+
* @param type The type of indentation to be used.
106+
* @return The indented String with the format specified.
107+
*/
108+
public static String formatIndentation(String text, IndentationType type) {
109+
return new IndentationHelper(text, type).formatIndentation();
110+
}
111+
112+
private String formatIndentation() {
113+
if (type == IndentationType.NULL) {
114+
return text;
115+
}
116+
IndentationState currentState = IndentationState.CODE;
117+
for (int i = 0; i < text.length(); i++) {
118+
char current = text.charAt(i);
119+
if (startOfLine && Character.isWhitespace(current) && current != '\n') {
120+
continue;
121+
}
122+
builder.append(current);
123+
if (current == '\n') {
124+
builder.append(type.getPattern().repeat(Math.max(numberOfBrackets, 0)));
125+
startOfLine = true;
126+
}
127+
currentState = switch (currentState) {
128+
case CODE -> {
129+
IndentationState state = processTokenInCode(i,current);
130+
i += state == IndentationState.MULTI_LINE_COMMENT ? 1:0;
131+
yield state;
132+
}
133+
case STRING -> {
134+
if (current == '\"' && !isEscaped(builder, builder.length() - 1)) {
135+
yield IndentationState.CODE;
136+
}
137+
yield IndentationState.STRING;
138+
}
139+
case CHARACTER -> {
140+
if (current == '\'' && !isEscaped(builder, builder.length() - 1)) {
141+
yield IndentationState.CODE;
142+
}
143+
yield IndentationState.CHARACTER;
144+
}
145+
case SINGLE_LINE_COMMENT ->
146+
current == '\n' ? IndentationState.CODE : IndentationState.SINGLE_LINE_COMMENT;
147+
case MULTI_LINE_COMMENT -> {
148+
if (current == '*' && i + 1 < text.length() && text.charAt(i + 1) == '/') {
149+
yield IndentationState.CODE;
150+
}
151+
yield IndentationState.MULTI_LINE_COMMENT;
152+
}
153+
};
154+
if (!Character.isWhitespace(current) && startOfLine) {
155+
startOfLine = false;
156+
}
157+
}
158+
return builder.toString();
159+
}
160+
161+
private IndentationState processTokenInCode(int i, char current) {
162+
switch (current) {
163+
case '{' -> numberOfBrackets++;
164+
case '}' -> {
165+
numberOfBrackets--;
166+
if (startOfLine && builder.length() - type.getNumberOfChars() - 1 >= 0) {
167+
builder.replace(builder.length() - type.getNumberOfChars() - 1, builder.length(), "}");
168+
}
169+
}
170+
case '\'' -> {
171+
return IndentationState.CHARACTER;
172+
}
173+
case '\"' -> {
174+
return IndentationState.STRING;
175+
}
176+
case '/' -> {
177+
if (i + 1 < text.length()) {
178+
if (text.charAt(i + 1) == '/') {
179+
return IndentationState.SINGLE_LINE_COMMENT;
180+
} else if (text.charAt(i + 1) == '*') {
181+
builder.append(text.charAt(++i));
182+
return IndentationState.MULTI_LINE_COMMENT;
183+
}
184+
}
185+
}
186+
}
187+
return IndentationState.CODE;
188+
}
189+
190+
/**
191+
* Determines if the character in the StringBuilder at the specified position is escaped.
192+
*
193+
* @param builder the StringBuilder which holds the current String
194+
* @param index The index at which the character to be checked is located.
195+
* @return True, if the escape character is referring to the character at the index, false otherwise.
196+
*/
197+
private static boolean isEscaped(StringBuilder builder, int index) {
198+
int numberOfCharacters = 0;
199+
index--;
200+
if (index >= builder.length()) {
201+
return false;
202+
}
203+
while (index > 0 && builder.charAt(index) == '\\') {
204+
numberOfCharacters++;
205+
index--;
206+
}
207+
return numberOfCharacters % 2 == 1;
208+
}
209+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package net.javadiscord.javabot.util;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.IOException;
6+
import java.net.URISyntaxException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.fail;
12+
13+
/**
14+
* Test for the {@link IndentationHelper} class.
15+
*/
16+
public class IndentationHelperTest {
17+
/**
18+
* Tests the {@link IndentationHelper#formatIndentation(String, IndentationHelper.IndentationType)} method.
19+
*/
20+
@Test
21+
public void testFormatIndentation() {
22+
String[] unformatted = null;
23+
String[] formatted = null;
24+
try {
25+
unformatted = Files.readString(Path.of(IndentationHelper.class.getResource("/Unformatted Strings.txt").toURI())).split("----");
26+
formatted = Files.readString(Path.of(IndentationHelper.class.getResource("/Formatted Strings.txt").toURI())).split("----");
27+
} catch (NullPointerException | URISyntaxException e) {
28+
fail("Files to run the test not present", e);
29+
} catch (IOException e) {
30+
e.printStackTrace();
31+
}
32+
33+
for (int i = 0, k = 0; i < unformatted.length; i++) {
34+
assertEquals(formatted[k++], IndentationHelper.formatIndentation(unformatted[i], IndentationHelper.IndentationType.FOUR_SPACES), "Method failed to format a text with four spaces correctly");
35+
assertEquals(formatted[k++], IndentationHelper.formatIndentation(unformatted[i], IndentationHelper.IndentationType.TWO_SPACES), "Method failed to format a text with two spaces correctly");
36+
assertEquals(formatted[k++], IndentationHelper.formatIndentation(unformatted[i], IndentationHelper.IndentationType.TABS), "Method failed to format a text with tabs correctly.");
37+
assertEquals(formatted[k++], IndentationHelper.formatIndentation(unformatted[i], IndentationHelper.IndentationType.NULL), "Method returned a String not matching the input");
38+
}
39+
40+
}
41+
}

0 commit comments

Comments
 (0)