Files
processing4/java/src/processing/mode/java/lsp/PdeAdapter.java
2023-05-11 13:38:15 -07:00

431 lines
13 KiB
Java

package processing.mode.java.lsp;
import java.io.File;
import java.net.URI;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionItemKind;
import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DiagnosticSeverity;
import org.eclipse.lsp4j.InsertTextFormat;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.jsonrpc.CompletableFutures;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.services.LanguageClient;
import org.eclipse.lsp4j.TextEdit;
import org.jsoup.Jsoup;
import processing.app.Base;
import processing.app.contrib.ModeContribution;
import processing.app.Platform;
import processing.app.Preferences;
import processing.app.Problem;
import processing.app.Sketch;
import processing.app.SketchCode;
import processing.mode.java.AutoFormat;
import processing.mode.java.CompletionCandidate;
import processing.mode.java.CompletionGenerator;
import processing.mode.java.ErrorChecker;
import processing.mode.java.JavaMode;
import processing.mode.java.JavaTextArea;
import processing.mode.java.PreprocService;
import processing.mode.java.PreprocSketch;
import static java.util.Arrays.copyOfRange;
class PdeAdapter {
File rootPath;
LanguageClient client;
JavaMode javaMode;
File pdeFile;
Sketch sketch;
CompletionGenerator completionGenerator;
PreprocService preprocService;
ErrorChecker errorChecker;
CompletableFuture<PreprocSketch> cps;
CompletionGenerator suggestionGenerator;
Set<URI> prevDiagnosticReportUris = new HashSet<>();
PreprocSketch ps;
PdeAdapter(File rootPath, LanguageClient client) {
this.rootPath = rootPath;
this.client = client;
File location = Platform.getContentFile("modes/java");
ModeContribution mc =
ModeContribution.load(null, location, JavaMode.class.getName());
if (mc == null) {
// Shouldn't be possible but IntelliJ is complaining about it,
// and we may run into path issues when running externally [fry 221126]
throw new RuntimeException("Could not load Java Mode from " + location);
}
javaMode = (JavaMode) mc.getMode();
pdeFile = new File(rootPath, rootPath.getName() + ".pde");
sketch = new Sketch(pdeFile.toString(), javaMode);
completionGenerator = new CompletionGenerator(javaMode);
preprocService = new PreprocService(javaMode, sketch);
errorChecker = new ErrorChecker(this::updateProblems, preprocService);
cps = CompletableFutures.computeAsync(_x -> {
throw new RuntimeException("unreachable");
});
suggestionGenerator = new CompletionGenerator(javaMode);
notifySketchChanged();
}
static Optional<File> uriToPath(URI uri) {
try {
return Optional.of(new File(uri));
} catch (Exception e) {
return Optional.empty();
}
}
static URI pathToUri(File path) {
return path.toURI();
}
static Offset toLineCol(String s, int offset) {
int line = (int)s.substring(0, offset).chars().filter(c -> c == '\n').count();
int col = offset - s.substring(0, offset).lastIndexOf('\n');
return new Offset(line, col);
}
/**
* Converts a tabOffset to a position within a tab
* @param program current code(text) from a tab
* @param tabOffset character offset inside a tab
* @return Position(line and col) within the tab
*/
static Position toPosition(String program, int tabOffset){
Offset offset = toLineCol(program, tabOffset);
return new Position(offset.line, offset.col-1);
}
/**
* Converts a range (start to end offset) to a location.
* @param program current code(text) from a tab
* @param startTabOffset starting character offset inside a tab
* @param stopTabOffset ending character offset inside a tab
* @param uri uri from a tab
* @return Range inside a file
*/
static Location toLocation(
String program,
int startTabOffset,
int stopTabOffset,
URI uri
){
Position startPos = toPosition(program, startTabOffset);
Position stopPos = toPosition(program, stopTabOffset);
Range range = new Range(startPos, stopPos);
return new Location(uri.toString(), range);
}
static void init() {
Base.setCommandLine();
Platform.init();
Preferences.init();
}
void notifySketchChanged() {
CompletableFuture<PreprocSketch> cps = new CompletableFuture<>();
this.cps = cps;
preprocService.notifySketchChanged();
errorChecker.notifySketchChanged();
preprocService.whenDone(cps::complete);
try { ps = cps.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
Optional<SketchCode> findCodeByUri(URI uri) {
return PdeAdapter.uriToPath(uri)
.flatMap(path -> Arrays.stream(sketch.getCode())
.filter(code -> code.getFile().equals(path))
.findFirst()
);
}
/**
* Looks for the tab number for a given text
* @param code text(code) from a tab
* @return tabIndex where the code belongs to, or empty
*/
public Optional<Integer> findTabIndex(SketchCode code){
int tabsCount = sketch.getCodeCount();
java.util.OptionalInt optionalTabIndex;
optionalTabIndex = IntStream.range(0, tabsCount)
.filter(i -> sketch.getCode(i).equals(code))
.findFirst();
if(optionalTabIndex.isEmpty()){
return Optional.empty();
}
return Optional.of(optionalTabIndex.getAsInt());
}
/**
* Looks for the javaOffset, this offset is the character position inside the
* full java file. The position can be used by the AST to find a node.
* @param uri uri of the file(tab) where to look
* @param line line number
* @param col column number
* @return character offset within the full AST
*/
public Optional<Integer> findJavaOffset(URI uri, int line, int col){
Optional<SketchCode> optionalCode = findCodeByUri(uri);
if(optionalCode.isEmpty()){
System.out.println("couldn't find sketch code");
return Optional.empty();
}
SketchCode code = optionalCode.get();
Optional<Integer> optionalTabIndex = findTabIndex(code);
if (optionalTabIndex.isEmpty()){
System.out.println("couldn't find tab index");
return Optional.empty();
}
int tabIndex = optionalTabIndex.get();
String[] codeLines = copyOfRange(code.getProgram().split("\n"), 0,line);
String codeString = String.join("\n", codeLines);
int tabOffset = codeString.length() + col;
return Optional.of(ps.tabOffsetToJavaOffset(tabIndex, tabOffset));
}
void updateProblems(List<Problem> problems) {
Map<URI, List<Diagnostic>> dias = problems.stream()
.map(prob -> {
SketchCode code = sketch.getCode(prob.getTabIndex());
Optional<Integer> startOffset = prob.getTabStartOffset();
Optional<Integer> endOffset = prob.getTabStopOffset();
assert startOffset.isPresent();
assert endOffset.isPresent();
Diagnostic dia = new Diagnostic(
new Range(
new Position(
prob.getLineNumber(),
PdeAdapter
.toLineCol(code.getProgram(), startOffset.get())
.col - 1
),
new Position(
prob.getLineNumber(),
PdeAdapter
.toLineCol(code.getProgram(), endOffset.get())
.col - 1
)
),
prob.getMessage()
);
dia.setSeverity(
prob.isError()
? DiagnosticSeverity.Error
: DiagnosticSeverity.Warning
);
return new AbstractMap.SimpleEntry<>(
PdeAdapter.pathToUri(code.getFile()),
dia
);
})
.collect(Collectors.groupingBy(
AbstractMap.SimpleEntry::getKey,
Collectors.mapping(
AbstractMap.SimpleEntry::getValue,
Collectors.toList()
)
));
for (Map.Entry<URI, List<Diagnostic>> entry : dias.entrySet()) {
PublishDiagnosticsParams params = new PublishDiagnosticsParams();
params.setUri(entry.getKey().toString());
params.setDiagnostics(entry.getValue());
client.publishDiagnostics(params);
}
for (URI uri : prevDiagnosticReportUris) {
if (!dias.containsKey(uri)) {
PublishDiagnosticsParams params = new PublishDiagnosticsParams();
params.setUri(uri.toString());
params.setDiagnostics(Collections.emptyList());
client.publishDiagnostics(params);
}
}
prevDiagnosticReportUris = dias.keySet();
}
CompletionItem convertCompletionCandidate(CompletionCandidate c) {
CompletionItem item = new CompletionItem();
item.setLabel(c.getElementName());
item.setInsertTextFormat(InsertTextFormat.Snippet);
String insert = c.getCompletionString();
if (insert.contains("( )")) {
insert = insert.replace("( )", "($1)");
} else if (insert.contains(",")) {
int n = 1;
char[] chs = insert.replace("(,", "($1,").toCharArray();
//insert = "";
StringBuilder newInsert = new StringBuilder();
for (char ch : chs) {
if (ch == ',') {
n += 1;
//insert += ",$" + n;
newInsert.append(",$").append(n);
}
//insert += ch;
newInsert.append(ch);
}
insert = newInsert.toString();
}
item.setInsertText(insert);
CompletionItemKind kind = switch (c.getType()) {
case 0 -> // PREDEF_CLASS
CompletionItemKind.Class;
case 1 -> // PREDEF_FIELD
CompletionItemKind.Constant;
case 2 -> // PREDEF_METHOD
CompletionItemKind.Function;
case 3 -> // LOCAL_CLASS
CompletionItemKind.Class;
case 4 -> // LOCAL_METHOD
CompletionItemKind.Method;
case 5 -> // LOCAL_FIELD
CompletionItemKind.Field;
case 6 -> // LOCAL_VARIABLE
CompletionItemKind.Variable;
default -> throw new IllegalArgumentException("Unknown completion type: " + c.getType());
};
item.setKind(kind);
item.setDetail(Jsoup.parse(c.getLabel()).text());
return item;
}
Optional<String> parsePhrase(String text) {
return Optional.ofNullable(JavaTextArea.parsePhrase(text));
}
List<CompletionCandidate> filterPredictions(
List<CompletionCandidate> candidates
) {
return Collections.list(CompletionGenerator.filterPredictions(candidates).elements());
}
CompletableFuture<List<CompletionItem>> generateCompletion(
URI uri,
int line,
int col
) {
return cps.thenApply(ps -> {
Optional<List<CompletionItem>> result =
findCodeByUri(uri)
.flatMap(code -> {
int codeIndex = IntStream.range(0, sketch.getCodeCount())
.filter(i -> sketch.getCode(i).equals(code))
.findFirst()
.getAsInt();
int lineStartOffset = String.join(
"\n",
Arrays.copyOfRange(code.getProgram().split("\n"), 0, line + 1)
)
.length();
int lineNumber = ps.tabOffsetToJavaLine(codeIndex, lineStartOffset);
String text = code.getProgram()
.split("\n")[line] // TODO: 範囲外のエラー処理
.substring(0, col);
return parsePhrase(text)
.map(phrase -> {
System.out.println("phrase: " + phrase);
System.out.println("lineNumber: " + lineNumber);
return Optional.ofNullable(
suggestionGenerator
.preparePredictions(ps, phrase, lineNumber)
)
.filter(x -> !x.isEmpty())
.map(candidates -> {
Collections.sort(candidates);
System.out.println("candidates: " + candidates);
List<CompletionCandidate> filtered = filterPredictions(candidates);
System.out.println("filtered: " + filtered);
return filtered.stream()
.map(this::convertCompletionCandidate)
.collect(Collectors.toList());
});
})
.orElse(Optional.empty());
});
return result.orElse(Collections.emptyList());
});
}
void onChange(URI uri, String text) {
findCodeByUri(uri)
.ifPresent(code -> {
code.setProgram(text);
notifySketchChanged();
});
}
Optional<TextEdit> format(URI uri) {
return findCodeByUri(uri)
.map(SketchCode::getProgram)
.map(code -> {
String newCode = new AutoFormat().format(code);
Offset end = PdeAdapter.toLineCol(code, code.length());
return new TextEdit(
new Range(
new Position(0, 0),
new Position(end.line, end.col)
),
newCode
);
});
}
static class Offset {
int line;
int col;
Offset(int line, int col) {
this.line = line;
this.col = col;
}
}
}