mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Implemented partial reparse in JavaCompilerService
This commit is contained in:
Binary file not shown.
@@ -35,6 +35,7 @@ import com.itsaky.androidide.managers.PreferenceManager;
|
||||
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
|
||||
import com.itsaky.androidide.utils.ILogger;
|
||||
import com.itsaky.lsp.api.ILanguageServer;
|
||||
import com.itsaky.lsp.models.ChangeType;
|
||||
import com.itsaky.lsp.models.Command;
|
||||
import com.itsaky.lsp.models.DefinitionResult;
|
||||
import com.itsaky.lsp.models.DiagnosticItem;
|
||||
@@ -195,8 +196,20 @@ public class IDEEditor extends CodeEditor {
|
||||
final var documentHandler = mLanguageServer.getDocumentHandler();
|
||||
final var file = getFile().toPath();
|
||||
if (documentHandler.accepts(file)) {
|
||||
var type = ChangeType.INSERT;
|
||||
if (event.getAction() == ContentChangeEvent.ACTION_DELETE) {
|
||||
type = ChangeType.DELETE;
|
||||
} else if (event.getAction() == ContentChangeEvent.ACTION_SET_NEW_TEXT) {
|
||||
type = ChangeType.NEW_TEXT;
|
||||
}
|
||||
|
||||
var changeDelta = type == ChangeType.NEW_TEXT ? 0 : event.getChangedText().length();
|
||||
if (type == ChangeType.DELETE) {
|
||||
changeDelta = -changeDelta;
|
||||
}
|
||||
|
||||
documentHandler.onContentChange(
|
||||
new DocumentChangeEvent(file, getText(), mFileVersion + 1));
|
||||
new DocumentChangeEvent(file, getText(), mFileVersion + 1, type, changeDelta));
|
||||
}
|
||||
|
||||
checkForSignatureHelp(event);
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ class StopWatch(
|
||||
}
|
||||
|
||||
fun lapFromLast(message: String) {
|
||||
log.debug("$message in ${System.currentTimeMillis() - start}ms")
|
||||
log.debug("$message in ${System.currentTimeMillis() - lastLap}ms")
|
||||
lastLap = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -17,6 +17,8 @@
|
||||
|
||||
package com.itsaky.androidide.utils
|
||||
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
|
||||
/**
|
||||
* A stop watch helps to log duration between the time when the instance of the stopwatch instance
|
||||
* was created and the time when [StopWatch.log] or [StopWatch.lap] method is called.
|
||||
@@ -24,17 +26,26 @@ package com.itsaky.androidide.utils
|
||||
* @param label The label for the log message.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class StopWatch(val label: String, val start: Long) {
|
||||
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
|
||||
class StopWatch(
|
||||
val label: String,
|
||||
val start: Long = System.currentTimeMillis(),
|
||||
var lastLap: Long = start
|
||||
) {
|
||||
constructor(label: String) : this(label, System.currentTimeMillis())
|
||||
|
||||
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
|
||||
fun log() {
|
||||
log.debug("$label completed in ${System.currentTimeMillis() - start}ms")
|
||||
}
|
||||
|
||||
|
||||
fun lap(message: String) {
|
||||
log.debug("$message in ${System.currentTimeMillis() - start}ms")
|
||||
lastLap = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
fun lapFromLast(message: String) {
|
||||
log.debug("$message in ${System.currentTimeMillis() - lastLap}ms")
|
||||
lastLap = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ data class DocumentOpenEvent(var openedFile: Path, var text: String, var version
|
||||
|
||||
data class DocumentCloseEvent(var closedFile: Path) : DocumentEvent(closedFile)
|
||||
|
||||
data class DocumentChangeEvent(var changedFile: Path, var newText: CharSequence, var version: Int) :
|
||||
data class DocumentChangeEvent(var changedFile: Path, var newText: CharSequence, var version: Int, var changeType: ChangeType, var changeDelta: Int) :
|
||||
DocumentEvent(changedFile)
|
||||
|
||||
data class DocumentSaveEvent(var savedFile: Path) : DocumentEvent(savedFile)
|
||||
@@ -40,3 +40,9 @@ data class DocumentSaveEvent(var savedFile: Path) : DocumentEvent(savedFile)
|
||||
data class ShowDocumentParams(var file: Path, var selection: Range)
|
||||
|
||||
data class ShowDocumentResult(var success: Boolean)
|
||||
|
||||
enum class ChangeType {
|
||||
INSERT,
|
||||
DELETE,
|
||||
NEW_TEXT
|
||||
}
|
||||
|
||||
@@ -307,6 +307,9 @@ public class JavaLanguageServer implements ILanguageServer, IDocumentHandler {
|
||||
// If a file's content is changed, it is definitely visible to user.
|
||||
onFileSelected(event.getChangedFile());
|
||||
FileStore.change(event);
|
||||
if (compiler != null) {
|
||||
compiler.onContentChanged(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,7 +29,7 @@ import com.itsaky.lsp.java.partial.DiagnosticListenerImpl;
|
||||
import com.itsaky.lsp.java.visitors.MethodRangeScanner;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.tools.javac.api.JavacTaskImpl;
|
||||
|
||||
import java.io.File;
|
||||
@@ -63,7 +63,7 @@ public class CompileBatch implements AutoCloseable {
|
||||
/** Indicates the task that requested the compilation is finished with it. */
|
||||
boolean closed;
|
||||
|
||||
final Map<String, List<Pair<Range, MethodTree>>> methodPositions = new HashMap<>();
|
||||
final Map<String, List<Pair<Range, TreePath>>> methodPositions = new HashMap<>();
|
||||
|
||||
CompileBatch(JavaCompilerService parent, Collection<? extends JavaFileObject> files) {
|
||||
this.parent = parent;
|
||||
@@ -72,26 +72,13 @@ public class CompileBatch implements AutoCloseable {
|
||||
this.roots = new ArrayList<>();
|
||||
|
||||
final StopWatch watch = new StopWatch("Create CompileBatch");
|
||||
boolean first = true;
|
||||
for (CompilationUnitTree t : borrow.task.parse()) {
|
||||
if (first) {
|
||||
watch.lap("Parsed");
|
||||
first = false;
|
||||
}
|
||||
|
||||
final Iterable<? extends CompilationUnitTree> trees = borrow.task.parse();
|
||||
watch.lap("CompilationUnitTree(s) parsed");
|
||||
|
||||
for (CompilationUnitTree t : trees) {
|
||||
roots.add(t);
|
||||
|
||||
final StopWatch positionWatch = new StopWatch("Scan method positions");
|
||||
final List<Pair<Range, MethodTree>> positions = new ArrayList<>();
|
||||
new MethodRangeScanner(this.task).scan(t, positions);
|
||||
final String path = new File(t.getSourceFile().toUri()).getAbsolutePath();
|
||||
final List<Pair<Range, MethodTree>> old = this.methodPositions.put(path, positions);
|
||||
if (old != null) {
|
||||
throw new IllegalStateException(
|
||||
"Duplicate CompilationUnitTree for file:" + t.getSourceFile().toUri());
|
||||
}
|
||||
|
||||
positionWatch.log();
|
||||
updatePositions(t, false);
|
||||
}
|
||||
|
||||
// The results of borrow.task.analyze() are unreliable when errors are present
|
||||
@@ -100,6 +87,20 @@ public class CompileBatch implements AutoCloseable {
|
||||
watch.log();
|
||||
}
|
||||
|
||||
void updatePositions(CompilationUnitTree tree, boolean allowDuplicate) {
|
||||
final StopWatch positionWatch = new StopWatch("Scan method positions");
|
||||
final List<Pair<Range, TreePath>> positions = new ArrayList<>();
|
||||
new MethodRangeScanner(this.task).scan(tree, positions);
|
||||
final String path = new File(tree.getSourceFile().toUri()).getAbsolutePath();
|
||||
final List<Pair<Range, TreePath>> old = this.methodPositions.put(path, positions);
|
||||
if (old != null && !allowDuplicate) {
|
||||
throw new IllegalStateException(
|
||||
"Duplicate CompilationUnitTree for file:" + tree.getSourceFile().toUri());
|
||||
}
|
||||
|
||||
positionWatch.log();
|
||||
}
|
||||
|
||||
private ReusableCompiler.Borrow batchTask(
|
||||
@NonNull JavaCompilerService parent, @NonNull Collection<? extends JavaFileObject> sources) {
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import com.itsaky.androidide.utils.ILogger;
|
||||
import com.itsaky.androidide.utils.StopWatch;
|
||||
import com.itsaky.lsp.java.FileStore;
|
||||
import com.itsaky.lsp.java.models.CompilationRequest;
|
||||
import com.itsaky.lsp.java.parser.ParseTask;
|
||||
@@ -34,9 +35,13 @@ import com.itsaky.lsp.java.utils.Extractors;
|
||||
import com.itsaky.lsp.java.utils.ScanClassPath;
|
||||
import com.itsaky.lsp.java.utils.StringSearch;
|
||||
import com.itsaky.lsp.java.visitors.FindTypeDeclarations;
|
||||
import com.itsaky.lsp.models.DocumentChangeEvent;
|
||||
import com.itsaky.lsp.models.Range;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.util.SourcePositions;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.source.util.Trees;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
@@ -74,8 +79,9 @@ public class JavaCompilerService implements CompilerProvider {
|
||||
protected final Map<JavaFileObject, Long> cachedModified = new HashMap<>();
|
||||
protected final SourceFileManager fileManager;
|
||||
private final Cache<Void, List<String>> cacheFileImports = new Cache<>();
|
||||
protected ReusableCompiler compiler = new ReusableCompiler();
|
||||
public ReusableCompiler compiler = new ReusableCompiler();
|
||||
private CompileBatch cachedCompile;
|
||||
private int changeDelta = 0;
|
||||
|
||||
public JavaCompilerService(Set<Path> classPath, Set<Path> docPath) {
|
||||
this.classPath = Collections.unmodifiableSet(classPath);
|
||||
@@ -300,53 +306,82 @@ public class JavaCompilerService implements CompilerProvider {
|
||||
|| request.sources.size() != 1; // Cannot perform a reparse if there are multiple files
|
||||
}
|
||||
|
||||
private void tryReparse(final CompilationRequest request) {
|
||||
private void tryReparse(@NonNull final CompilationRequest request) {
|
||||
|
||||
// Satisfy lint
|
||||
Objects.requireNonNull(request.partialRequest);
|
||||
|
||||
final StopWatch watch = new StopWatch("Method reparse");
|
||||
final File file = new File(request.sources.iterator().next().toUri());
|
||||
final String path = file.getAbsolutePath();
|
||||
final List<Pair<Range, MethodTree>> positions = this.cachedCompile.methodPositions.get(path);
|
||||
final List<Pair<Range, TreePath>> positions = this.cachedCompile.methodPositions.get(path);
|
||||
if (positions == null) {
|
||||
LOG.warn("Cannot perform reparse. No method positions found.");
|
||||
recompile(request);
|
||||
return;
|
||||
}
|
||||
|
||||
final Pair<Range, MethodTree> currentMethod =
|
||||
final Pair<Range, TreePath> currentMethod =
|
||||
binarySearchCurrentMethod(positions, request.partialRequest.cursor);
|
||||
if (currentMethod == null) {
|
||||
LOG.warn("Cannot perform reparse. Unable to find current method");
|
||||
recompile(request);
|
||||
return;
|
||||
} else {
|
||||
watch.lapFromLast("Found method at cursor position");
|
||||
}
|
||||
|
||||
LOG.debug("Trying to reparse method:", currentMethod.second.getName());
|
||||
final MethodTree methodTree = (MethodTree) currentMethod.second.getLeaf();
|
||||
LOG.debug("Trying to reparse method:", methodTree.getName());
|
||||
|
||||
final CompilationInfo info =
|
||||
new CompilationInfo(
|
||||
cachedCompile.task, cachedCompile.diagnosticListener, cachedCompile.roots.get(0));
|
||||
watch.setLastLap(System.currentTimeMillis());
|
||||
final SourcePositions sourcePositions = Trees.instance(cachedCompile.task).getSourcePositions();
|
||||
final int start = (int) sourcePositions.getStartPosition(info.cu, methodTree.getBody());
|
||||
final int end =
|
||||
(int) sourcePositions.getEndPosition(info.cu, methodTree.getBody()) + this.changeDelta;
|
||||
watch.lapFromLast("Found start and end positions of current method");
|
||||
final PartialReparser reparser = new PartialReparserImpl();
|
||||
|
||||
if (end < 0 || end > request.partialRequest.contents.length()) {
|
||||
LOG.warn(
|
||||
"Cannot reparse. Invalid change delta. end:",
|
||||
end,
|
||||
"changeDelta:",
|
||||
this.changeDelta,
|
||||
"content.length:",
|
||||
request.partialRequest.contents.length());
|
||||
recompile(request);
|
||||
return;
|
||||
}
|
||||
|
||||
final String newBody = request.partialRequest.contents.substring(start, end);
|
||||
final boolean reparsed =
|
||||
reparser.reparseMethod(info, currentMethod.second, request.partialRequest.contents);
|
||||
reparser.reparseMethod(
|
||||
info, currentMethod.second, newBody, request.partialRequest.contents);
|
||||
if (!reparsed) {
|
||||
LOG.error("Failed to reparse");
|
||||
recompile(request);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Successfully reparsed method", currentMethod.second.getName());
|
||||
watch.log();
|
||||
LOG.info("Successfully reparsed method", methodTree.getName());
|
||||
updateModificationCache(request);
|
||||
cachedCompile.updatePositions(info.cu, true);
|
||||
this.changeDelta = 0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Pair<Range, MethodTree> binarySearchCurrentMethod(
|
||||
@NonNull final List<Pair<Range, MethodTree>> positions, final long cursor) {
|
||||
private Pair<Range, TreePath> binarySearchCurrentMethod(
|
||||
@NonNull final List<Pair<Range, TreePath>> positions, final long cursor) {
|
||||
int left = 0;
|
||||
int right = positions.size() - 1;
|
||||
while (left <= right) {
|
||||
int mid = (left + right) / 2;
|
||||
final Pair<Range, MethodTree> method = positions.get(mid);
|
||||
final Pair<Range, TreePath> method = positions.get(mid);
|
||||
final Range range = method.first;
|
||||
final int startIndex = range.getStart().requireIndex();
|
||||
final int endIndex = range.getEnd().requireIndex();
|
||||
@@ -365,6 +400,11 @@ public class JavaCompilerService implements CompilerProvider {
|
||||
private synchronized void recompile(CompilationRequest request) {
|
||||
close();
|
||||
cachedCompile = performCompilation(request.sources);
|
||||
updateModificationCache(request);
|
||||
this.changeDelta = 0;
|
||||
}
|
||||
|
||||
private void updateModificationCache(final CompilationRequest request) {
|
||||
cachedModified.clear();
|
||||
for (JavaFileObject f : request.sources) {
|
||||
cachedModified.put(f, f.getLastModified());
|
||||
@@ -396,6 +436,10 @@ public class JavaCompilerService implements CompilerProvider {
|
||||
return new CompileBatch(this, moreSources);
|
||||
}
|
||||
|
||||
public void onContentChanged(@NonNull DocumentChangeEvent event) {
|
||||
this.changeDelta += event.getChangeDelta();
|
||||
}
|
||||
|
||||
public synchronized void close() {
|
||||
if (cachedCompile != null && !cachedCompile.closed) {
|
||||
cachedCompile.close();
|
||||
|
||||
@@ -103,9 +103,10 @@ public class ReusableCompiler {
|
||||
private static final JavacTool systemProvider = JavacTool.create();
|
||||
private static final CancelService cancelService = new CancelServiceImpl();
|
||||
private final List<String> currentOptions = new ArrayList<>();
|
||||
private ReusableContext currentContext;
|
||||
private boolean checkedOut;
|
||||
|
||||
public ReusableContext currentContext;
|
||||
|
||||
/**
|
||||
* Creates a new task as if by {@link javax.tools.JavaCompiler#getTask} and runs the provided
|
||||
* worker with it. The task is only valid while the worker is running. The internal structures may
|
||||
|
||||
@@ -38,9 +38,9 @@ public class SourceFileObject implements JavaFileObject {
|
||||
/** path is the absolute path to this file on disk */
|
||||
final Path path;
|
||||
/** contents is the text in this file, or null if we should use the text in FileStore */
|
||||
final String contents;
|
||||
String contents;
|
||||
/** if contents is set, the modified time of contents */
|
||||
final Instant modified;
|
||||
Instant modified;
|
||||
|
||||
public SourceFileObject(Path path) {
|
||||
this(path, null, Instant.EPOCH);
|
||||
|
||||
@@ -29,9 +29,9 @@ import javax.tools.JavaFileObject
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
data class CompilationInfo(
|
||||
val task: JavacTaskImpl,
|
||||
val diagnosticListener: DiagnosticListener<JavaFileObject>,
|
||||
val cu: CompilationUnitTree,
|
||||
@JvmField val task: JavacTaskImpl,
|
||||
@JvmField val diagnosticListener: DiagnosticListener<JavaFileObject>,
|
||||
@JvmField val cu: CompilationUnitTree,
|
||||
) {
|
||||
val trees: JavacTrees = JavacTrees.instance(task)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.itsaky.lsp.java.partial;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.sun.tools.javac.api.ClientCodeWrapper;
|
||||
import com.sun.tools.javac.util.JCDiagnostic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -121,7 +122,7 @@ public class DiagnosticListenerImpl implements DiagnosticListener<JavaFileObject
|
||||
this.affectedErrors = new ArrayList<>(tail.size());
|
||||
HashSet<DiagNode> tailNodes = new HashSet<>();
|
||||
for (Iterator<Map.Entry<Integer, Collection<DiagNode>>> it = tail.entrySet().iterator();
|
||||
it.hasNext(); ) {
|
||||
it.hasNext(); ) {
|
||||
Map.Entry<Integer, Collection<DiagNode>> e = it.next();
|
||||
for (DiagNode d : e.getValue()) {
|
||||
tailNodes.add(d);
|
||||
@@ -132,7 +133,12 @@ public class DiagnosticListenerImpl implements DiagnosticListener<JavaFileObject
|
||||
while (node != null) {
|
||||
if (tailNodes.contains(node)) {
|
||||
errors.unlink(node);
|
||||
final JCDiagnostic diagnostic = (JCDiagnostic) node.diag;
|
||||
final JCDiagnostic diagnostic;
|
||||
if (node.diag instanceof ClientCodeWrapper.DiagnosticSourceUnwrapper) {
|
||||
diagnostic = ((ClientCodeWrapper.DiagnosticSourceUnwrapper) node.diag).d;
|
||||
} else {
|
||||
diagnostic = (JCDiagnostic) node.diag;
|
||||
}
|
||||
if (diagnostic == null) {
|
||||
throw new IllegalStateException(
|
||||
"#184910: diagnostic == null "
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package com.itsaky.lsp.java.partial
|
||||
|
||||
import com.sun.source.tree.MethodTree
|
||||
import com.sun.source.util.TreePath
|
||||
|
||||
/**
|
||||
* A parser reparser parses part of code.
|
||||
@@ -25,5 +25,5 @@ import com.sun.source.tree.MethodTree
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface PartialReparser {
|
||||
fun reparseMethod(ci: CompilationInfo, orig: MethodTree, newBody: String): Boolean
|
||||
fun reparseMethod(ci: CompilationInfo, methodPath: TreePath, newBody: String, fileContents: CharSequence): Boolean
|
||||
}
|
||||
|
||||
@@ -18,16 +18,15 @@
|
||||
package com.itsaky.lsp.java.partial
|
||||
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.androidide.utils.StopWatch
|
||||
import com.itsaky.lsp.java.FileStore
|
||||
import com.itsaky.lsp.java.compiler.JavacFlowListener
|
||||
import com.itsaky.lsp.java.visitors.FindAnonymousVisitor
|
||||
import com.itsaky.lsp.java.visitors.FindMethodAt
|
||||
import com.itsaky.lsp.java.visitors.TranslatePositionsVisitor
|
||||
import com.itsaky.lsp.java.visitors.TranslateMethodPositionsVisitor
|
||||
import com.itsaky.lsp.java.visitors.UnEnter
|
||||
import com.sun.source.tree.BlockTree
|
||||
import com.sun.source.tree.ClassTree
|
||||
import com.sun.source.tree.CompilationUnitTree
|
||||
import com.sun.source.tree.MethodTree
|
||||
import com.sun.source.tree.Tree.Kind.BLOCK
|
||||
import com.sun.source.tree.Tree.Kind.METHOD
|
||||
import com.sun.source.util.TreePath
|
||||
@@ -116,7 +115,7 @@ class PartialReparserImpl : PartialReparser {
|
||||
}
|
||||
}
|
||||
|
||||
override fun reparseMethod(ci: CompilationInfo, orig: MethodTree, newBody: String): Boolean {
|
||||
override fun reparseMethod(ci: CompilationInfo, methodPath: TreePath, newBody: String, fileContents: CharSequence): Boolean {
|
||||
|
||||
if (!allowPartialReparse) {
|
||||
log.debug("Partial reparse is disabled")
|
||||
@@ -126,15 +125,19 @@ class PartialReparserImpl : PartialReparser {
|
||||
val cu = ci.cu
|
||||
val fo = cu.sourceFile
|
||||
val task = ci.task
|
||||
val methodPath = FindMethodAt(task).scan(cu, ((orig as JCMethodDecl).pos + 1).toLong())
|
||||
if (methodPath == null || methodPath.leaf.kind != METHOD) {
|
||||
val method = methodPath.leaf as JCMethodDecl
|
||||
if (methodPath.leaf.kind != METHOD) {
|
||||
log.warn("Provided TreePath does not correspond to a MethodTree")
|
||||
return false
|
||||
}
|
||||
|
||||
val methodScope = ci.trees.getScope(TreePath(methodPath, (methodPath.leaf as MethodTree).body))
|
||||
val watch = StopWatch("reparseMethod")
|
||||
val methodScope = ci.trees.getScope(TreePath(methodPath, method.body))
|
||||
watch.lapFromLast("Found method scope")
|
||||
val jt = JavacTrees.instance(task)
|
||||
val origStartPos = jt.sourcePositions.getStartPosition(cu, orig.body).toInt()
|
||||
val origEndPos = jt.sourcePositions.getEndPosition(cu, orig.body).toInt()
|
||||
val origStartPos = jt.sourcePositions.getStartPosition(cu, method.body).toInt()
|
||||
val origEndPos = jt.sourcePositions.getEndPosition(cu, method.body).toInt()
|
||||
watch.lapFromLast("Found start and end positions")
|
||||
if (origStartPos < 0) {
|
||||
log.warn("Javac returned start position $origStartPos < 0")
|
||||
return false
|
||||
@@ -146,8 +149,8 @@ class PartialReparserImpl : PartialReparser {
|
||||
}
|
||||
|
||||
val fav = FindAnonymousVisitor()
|
||||
fav.scan(orig.body, null)
|
||||
|
||||
fav.scan(method.body, null)
|
||||
watch.lapFromLast("FindAnonymousVisitor")
|
||||
if (fav.hasLocalClass) {
|
||||
log.debug("Skipped reparse method. Old local classes.", fo)
|
||||
return false
|
||||
@@ -160,14 +163,15 @@ class PartialReparserImpl : PartialReparser {
|
||||
val l = NBLog.instance(context)
|
||||
l.startPartialReparse(fo)
|
||||
val prevLogged = l.useSource(fo)
|
||||
var block: JCBlock?
|
||||
val block: JCBlock?
|
||||
|
||||
try {
|
||||
val dl = ci.diagnosticListener as DiagnosticListenerImpl
|
||||
dl.startPartialReparse(origStartPos, origEndPos)
|
||||
var start = System.currentTimeMillis()
|
||||
watch.lapFromLast("Started partial reparse")
|
||||
val docComments = HashMap<JCTree, Any>()
|
||||
block = reparseMethodBody(context, cu, orig, newBody, docComments)
|
||||
block = reparseMethodBody(context, cu, method, newBody, docComments)
|
||||
watch.lapFromLast("Partially reparsed method")
|
||||
val endPosTable = (cu as JCCompilationUnit).endPositions
|
||||
log.debug("Reparsed method in $fo")
|
||||
if (block == null) {
|
||||
@@ -178,11 +182,14 @@ class PartialReparserImpl : PartialReparser {
|
||||
val sourcePositions = jt.sourcePositions
|
||||
val newEndPos = sourcePositions.getEndPosition(cu, block).toInt()
|
||||
if (newEndPos != origStartPos + newBody.length) {
|
||||
log.warn("Invalid position in reparsed method")
|
||||
return false
|
||||
}
|
||||
|
||||
fav.reset()
|
||||
fav.scan(block, null)
|
||||
watch.lapFromLast("FindAnonymousVisitor")
|
||||
|
||||
val newNoInner = fav.noInner
|
||||
if (fav.hasLocalClass || noInner != newNoInner) {
|
||||
log.debug("Skipped method reparse (new local class): $fo")
|
||||
@@ -191,44 +198,46 @@ class PartialReparserImpl : PartialReparser {
|
||||
val docCommentsTable = lazyDocCommentsTable!!.get(cu.docComments) as MutableMap<JCTree, Any>
|
||||
docCommentsTable.keys.removeAll(fav.docOwners)
|
||||
docCommentsTable.putAll(docComments)
|
||||
watch.lapFromLast("Updated doc comments")
|
||||
val delta = newEndPos - origEndPos
|
||||
val tpv = TranslatePositionsVisitor(orig, endPosTable, delta)
|
||||
val tpv = TranslateMethodPositionsVisitor(method, endPosTable, delta)
|
||||
tpv.scan(cu, null)
|
||||
doUnenter(context, cu, orig)
|
||||
orig.body = block
|
||||
watch.lapFromLast("Translated positions")
|
||||
doUnenter(context, cu, method)
|
||||
watch.lapFromLast("Unentered")
|
||||
method.body = block
|
||||
|
||||
log.debug("Reparsed method body (", System.currentTimeMillis() - start, "ms ):", block)
|
||||
|
||||
start = System.currentTimeMillis()
|
||||
reAttrMethodBody(context, methodScope, orig, block)
|
||||
log.debug("Resolved method (", System.currentTimeMillis() - start, "ms):", block)
|
||||
reAttrMethodBody(context, methodScope, method, block)
|
||||
watch.lapFromLast("ReAttr method")
|
||||
|
||||
if (!dl.hasPartialReparseErrors()) {
|
||||
val fl = JavacFlowListener.instance(context)
|
||||
if (fl != null && fl.hasFlowCompleted(cu.sourceFile)) {
|
||||
log.debug("Reflow...")
|
||||
start = System.currentTimeMillis()
|
||||
val tp = TreePath.getPath(cu, orig)
|
||||
val start = System.currentTimeMillis()
|
||||
val tp = TreePath.getPath(cu, method)
|
||||
val t = tp.parentPath.leaf as ClassTree
|
||||
reflowMethodBody(context, t, orig)
|
||||
log.debug("Reflowed method (", System.currentTimeMillis() - start, "ms)")
|
||||
reflowMethodBody(context, t, method)
|
||||
log.debug("Reflowed method in ${System.currentTimeMillis() - start}ms)")
|
||||
}
|
||||
watch.lapFromLast("Method reflow")
|
||||
}
|
||||
|
||||
val contents = FileStore.contents(Paths.get(cu.sourceFile.toUri()))
|
||||
val arr = CharArray(contents.length)
|
||||
for (index in contents.indices) {
|
||||
arr[index] = contents[index]
|
||||
|
||||
val arr = CharArray(fileContents.length)
|
||||
for (index in fileContents.indices) {
|
||||
arr[index] = fileContents[index]
|
||||
}
|
||||
|
||||
|
||||
lineMapBuild!!.invoke(cu.getLineMap(), arr, arr.size)
|
||||
|
||||
watch.lapFromLast("Line map built")
|
||||
dl.endPartialReparse(delta)
|
||||
} finally {
|
||||
l.endPartialReparse(cu.sourceFile)
|
||||
l.useSource(prevLogged)
|
||||
watch.log()
|
||||
}
|
||||
} catch (err: Throwable) {
|
||||
log.error("Failed to reparse method", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.itsaky.lsp.api.AbstractServiceProvider;
|
||||
import com.itsaky.lsp.api.ICompletionProvider;
|
||||
import com.itsaky.lsp.api.IServerSettings;
|
||||
import com.itsaky.lsp.internal.model.CachedCompletion;
|
||||
import com.itsaky.lsp.java.compiler.CompileTask;
|
||||
import com.itsaky.lsp.java.compiler.JavaCompilerService;
|
||||
import com.itsaky.lsp.java.compiler.SourceFileObject;
|
||||
import com.itsaky.lsp.java.compiler.SynchronizedTask;
|
||||
@@ -39,11 +40,16 @@ import com.itsaky.lsp.java.providers.completion.MemberReferenceCompletionProvide
|
||||
import com.itsaky.lsp.java.providers.completion.MemberSelectCompletionProvider;
|
||||
import com.itsaky.lsp.java.providers.completion.SwitchConstantCompletionProvider;
|
||||
import com.itsaky.lsp.java.providers.completion.TopLevelSnippetsProvider;
|
||||
import com.itsaky.lsp.java.utils.ASTFixer;
|
||||
import com.itsaky.lsp.java.visitors.FindCompletionsAt;
|
||||
import com.itsaky.lsp.java.visitors.PrintingVisitor;
|
||||
import com.itsaky.lsp.java.visitors.PruneMethodBodies;
|
||||
import com.itsaky.lsp.models.CompletionParams;
|
||||
import com.itsaky.lsp.models.CompletionResult;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.tools.javac.api.JavacTaskImpl;
|
||||
import com.sun.tools.javac.tree.JCTree;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
@@ -121,12 +127,21 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
|
||||
ParseTask task = compiler.parse(file);
|
||||
|
||||
long cursor = task.root.getLineMap().getPosition(line, column);
|
||||
StringBuilder contents = new PruneMethodBodies(task.task).scan(task.root, cursor);
|
||||
int endOfLine = endOfLine(contents, (int) cursor);
|
||||
contents.insert(endOfLine, ';');
|
||||
StringBuilder pruned = new PruneMethodBodies(task.task).scan(task.root, cursor);
|
||||
int endOfLine = endOfLine(pruned, (int) cursor);
|
||||
pruned.insert(endOfLine, ';');
|
||||
|
||||
final CharSequence contents;
|
||||
if (compiler.compiler.currentContext != null) {
|
||||
contents = new ASTFixer(compiler.compiler.currentContext).fix(pruned);
|
||||
} else {
|
||||
contents = pruned.toString();
|
||||
}
|
||||
|
||||
final String contentString = contents.toString();
|
||||
CompletionResult result = compileAndComplete(file, contentString, cursor, new PartialReparseRequest(cursor - params.requirePrefix().length(), contentString));
|
||||
final PartialReparseRequest partialRequest =
|
||||
new PartialReparseRequest(cursor - params.requirePrefix().length(), contentString);
|
||||
CompletionResult result = compileAndComplete(file, contentString, cursor, partialRequest);
|
||||
if (result == null) {
|
||||
result = CompletionResult.EMPTY;
|
||||
}
|
||||
@@ -171,54 +186,78 @@ public class CompletionProvider extends AbstractServiceProvider implements IComp
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private CompletionResult compileAndComplete(Path file, String contents, long cursor, PartialReparseRequest partialRequest) {
|
||||
Instant started = Instant.now();
|
||||
SourceFileObject source = new SourceFileObject(file, contents, Instant.now());
|
||||
String partial = partialIdentifier(contents, (int) cursor);
|
||||
boolean endsWithParen = endsWithParen(contents, (int) cursor);
|
||||
private CompletionResult compileAndComplete(
|
||||
Path file, String contents, final long cursor, PartialReparseRequest partialRequest) {
|
||||
final Instant started = Instant.now();
|
||||
final SourceFileObject source = new SourceFileObject(file, contents, Instant.now());
|
||||
final String partial = partialIdentifier(contents, (int) cursor);
|
||||
final boolean endsWithParen = endsWithParen(contents, (int) cursor);
|
||||
SynchronizedTask synchronizedTask =
|
||||
compiler.compile(
|
||||
new CompilationRequest(
|
||||
Collections.singletonList(source), partialRequest));
|
||||
compiler.compile(new CompilationRequest(Collections.singletonList(source), partialRequest));
|
||||
return synchronizedTask.get(
|
||||
task -> {
|
||||
if (task == null
|
||||
|| task.task == null
|
||||
|| ((JavacTaskImpl) task.task).getContext() == null) {
|
||||
return CompletionResult.EMPTY;
|
||||
}
|
||||
LOG.info("...compiled in " + Duration.between(started, Instant.now()).toMillis() + "ms");
|
||||
TreePath path = new FindCompletionsAt(task.task).scan(task.root(), cursor);
|
||||
|
||||
final Class<? extends IJavaCompletionProvider> klass;
|
||||
switch (path.getLeaf().getKind()) {
|
||||
case IDENTIFIER:
|
||||
klass = IdentifierCompletionProvider.class;
|
||||
break;
|
||||
case MEMBER_SELECT:
|
||||
klass = MemberSelectCompletionProvider.class;
|
||||
break;
|
||||
case MEMBER_REFERENCE:
|
||||
klass = MemberReferenceCompletionProvider.class;
|
||||
break;
|
||||
case SWITCH:
|
||||
klass = SwitchConstantCompletionProvider.class;
|
||||
break;
|
||||
case IMPORT:
|
||||
klass = ImportCompletionProvider.class;
|
||||
break;
|
||||
default:
|
||||
klass = KeywordCompletionProvider.class;
|
||||
break;
|
||||
String newPartial = partial;
|
||||
if (path.getLeaf().getKind() == Tree.Kind.IMPORT) {
|
||||
newPartial = qualifiedPartialIdentifier(contents, (int) cursor);
|
||||
if (newPartial.endsWith(ASTFixer.IDENT)) {
|
||||
newPartial = newPartial.substring(0, newPartial.length() - ASTFixer.IDENT.length());
|
||||
}
|
||||
}
|
||||
|
||||
final IJavaCompletionProvider provider =
|
||||
ReflectUtils.reflect(klass).newInstance(file, cursor, compiler, getSettings()).get();
|
||||
|
||||
if (provider instanceof ImportCompletionProvider) {
|
||||
((ImportCompletionProvider) provider)
|
||||
.setImportPath(qualifiedPartialIdentifier(contents, (int) cursor));
|
||||
}
|
||||
|
||||
return provider.complete(task, path, partial, endsWithParen);
|
||||
return doComplete(file, contents, cursor, newPartial, endsWithParen, task, path);
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CompletionResult doComplete(
|
||||
final Path file,
|
||||
final String contents,
|
||||
final long cursor,
|
||||
final String partial,
|
||||
final boolean endsWithParen,
|
||||
final CompileTask task,
|
||||
final TreePath path) {
|
||||
final Class<? extends IJavaCompletionProvider> klass;
|
||||
switch (path.getLeaf().getKind()) {
|
||||
case IDENTIFIER:
|
||||
klass = IdentifierCompletionProvider.class;
|
||||
break;
|
||||
case MEMBER_SELECT:
|
||||
klass = MemberSelectCompletionProvider.class;
|
||||
break;
|
||||
case MEMBER_REFERENCE:
|
||||
klass = MemberReferenceCompletionProvider.class;
|
||||
break;
|
||||
case SWITCH:
|
||||
klass = SwitchConstantCompletionProvider.class;
|
||||
break;
|
||||
case IMPORT:
|
||||
klass = ImportCompletionProvider.class;
|
||||
break;
|
||||
default:
|
||||
klass = KeywordCompletionProvider.class;
|
||||
break;
|
||||
}
|
||||
|
||||
final IJavaCompletionProvider provider =
|
||||
ReflectUtils.reflect(klass).newInstance(file, cursor, compiler, getSettings()).get();
|
||||
|
||||
if (provider instanceof ImportCompletionProvider) {
|
||||
((ImportCompletionProvider) provider)
|
||||
.setImportPath(qualifiedPartialIdentifier(contents, (int) cursor));
|
||||
}
|
||||
|
||||
return provider.complete(task, path, partial, endsWithParen);
|
||||
}
|
||||
|
||||
private boolean endsWithParen(@NonNull String contents, int cursor) {
|
||||
for (int i = cursor; i < contents.length(); i++) {
|
||||
if (!Character.isJavaIdentifierPart(contents.charAt(i))) {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.lsp.java.utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.sun.source.tree.LineMap;
|
||||
import com.sun.tools.javac.parser.Scanner;
|
||||
import com.sun.tools.javac.parser.ScannerFactory;
|
||||
import com.sun.tools.javac.parser.Tokens;
|
||||
import com.sun.tools.javac.parser.Tokens.TokenKind;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class ASTFixer {
|
||||
public static final String IDENT = "I_N_J_E_C_T_E_D";
|
||||
|
||||
private static final Set<TokenKind> MEMBER_SELECTION_TOKENS =
|
||||
ImmutableSet.of(
|
||||
Tokens.TokenKind.IDENTIFIER,
|
||||
Tokens.TokenKind.LT,
|
||||
TokenKind.NEW,
|
||||
TokenKind.THIS,
|
||||
TokenKind.SUPER,
|
||||
TokenKind.CLASS,
|
||||
TokenKind.STAR);
|
||||
private static final Set<TokenKind> INVALID_SELECTION_SUFFIXES =
|
||||
ImmutableSet.of(TokenKind.RBRACE);
|
||||
|
||||
private final Context context;
|
||||
|
||||
public ASTFixer(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public CharSequence fix(CharSequence content) {
|
||||
Scanner scanner = ScannerFactory.instance(context).newScanner(content, true);
|
||||
List<Edit> edits = new ArrayList<>();
|
||||
for (; ; scanner.nextToken()) {
|
||||
Tokens.Token token = scanner.token();
|
||||
if (token.kind == TokenKind.EOF) {
|
||||
break;
|
||||
} else if (token.kind == TokenKind.DOT || token.kind == TokenKind.COLCOL) {
|
||||
fixMemberSelection(scanner, edits);
|
||||
} else if (token.kind == TokenKind.ERROR) {
|
||||
int errPos = scanner.errPos();
|
||||
if (errPos >= 0 && errPos < content.length()) {
|
||||
fixError(scanner, content, edits);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Edit.applyInsertions(content, edits);
|
||||
}
|
||||
|
||||
private void fixMemberSelection(@NonNull Scanner scanner, List<Edit> edits) {
|
||||
Tokens.Token token = scanner.token();
|
||||
Tokens.Token nextToken = scanner.token(1);
|
||||
|
||||
LineMap lineMap = scanner.getLineMap();
|
||||
int tokenLine = (int) lineMap.getLineNumber(token.pos);
|
||||
int nextLine = (int) lineMap.getLineNumber(nextToken.pos);
|
||||
|
||||
if (nextLine > tokenLine) {
|
||||
edits.add(Edit.create(token.endPos, IDENT + ";"));
|
||||
} else if (!MEMBER_SELECTION_TOKENS.contains(nextToken.kind)) {
|
||||
String toInsert = IDENT;
|
||||
if (INVALID_SELECTION_SUFFIXES.contains(nextToken.kind)) {
|
||||
toInsert = IDENT + ";";
|
||||
}
|
||||
edits.add(Edit.create(token.endPos, toInsert));
|
||||
}
|
||||
}
|
||||
|
||||
private void fixError(
|
||||
@NonNull Scanner scanner, @NonNull CharSequence content, List<Edit> edits) {
|
||||
int errPos = scanner.errPos();
|
||||
if (content.charAt(errPos) == '.' && errPos > 0 && content.charAt(errPos) == '.') {
|
||||
if (errPos < content.length() - 1
|
||||
&& Character.isJavaIdentifierStart(content.charAt(errPos + 1))) {
|
||||
edits.add(Edit.create(errPos, IDENT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Edit {
|
||||
private static final Ordering<Edit> REVERSE_INSERTION =
|
||||
Ordering.natural().onResultOf(Edit::getPos).reverse();
|
||||
|
||||
private final int pos;
|
||||
private final String text;
|
||||
|
||||
public Edit(int pos, String text) {
|
||||
this.pos = pos;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public int getPos() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Contract(value = "_, _ -> new", pure = true)
|
||||
public static Edit create(int pos, String text) {
|
||||
return new Edit(pos, text);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static CharSequence applyInsertions(CharSequence content, List<Edit> edits) {
|
||||
ImmutableList<Edit> reverseEdits =
|
||||
REVERSE_INSERTION.immutableSortedCopy(edits);
|
||||
|
||||
StringBuilder sb = new StringBuilder(content);
|
||||
|
||||
for (Edit edit : reverseEdits) {
|
||||
sb.insert(edit.getPos(), edit.getText());
|
||||
}
|
||||
return sb;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
package com.itsaky.lsp.java.visitors;
|
||||
|
||||
import com.itsaky.androidide.utils.ILogger;
|
||||
import com.sun.source.tree.CaseTree;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.ErroneousTree;
|
||||
@@ -34,13 +33,15 @@ import com.sun.source.util.Trees;
|
||||
|
||||
public class FindCompletionsAt extends TreePathScanner<TreePath, Long> {
|
||||
|
||||
private static final ILogger LOG = ILogger.newInstance("FindCompletionsAt");
|
||||
// private static final ILogger LOG = ILogger.newInstance("FindCompletionsAt");
|
||||
private final JavacTask task;
|
||||
private CompilationUnitTree root;
|
||||
|
||||
public FindCompletionsAt(JavacTask task) {
|
||||
this.task = task;
|
||||
} @Override
|
||||
}
|
||||
|
||||
@Override
|
||||
public TreePath visitCompilationUnit(CompilationUnitTree t, Long find) {
|
||||
root = t;
|
||||
return reduce(super.visitCompilationUnit(t, find), getCurrentPath());
|
||||
@@ -137,6 +138,4 @@ public class FindCompletionsAt extends TreePathScanner<TreePath, Long> {
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@
|
||||
package com.itsaky.lsp.java.visitors
|
||||
|
||||
import androidx.core.util.Pair
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.itsaky.lsp.models.Position
|
||||
import com.itsaky.lsp.models.Range
|
||||
import com.sun.source.tree.CompilationUnitTree
|
||||
import com.sun.source.tree.LineMap
|
||||
import com.sun.source.tree.MethodTree
|
||||
import com.sun.source.tree.Tree
|
||||
import com.sun.source.util.TreeScanner
|
||||
import com.sun.source.util.TreePath
|
||||
import com.sun.source.util.TreePathScanner
|
||||
import com.sun.source.util.Trees
|
||||
import com.sun.tools.javac.api.JavacTaskImpl
|
||||
|
||||
@@ -34,7 +35,9 @@ import com.sun.tools.javac.api.JavacTaskImpl
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class MethodRangeScanner(val task: JavacTaskImpl) :
|
||||
TreeScanner<Unit, MutableList<Pair<Range, MethodTree>>>() {
|
||||
TreePathScanner<Unit, MutableList<Pair<Range, TreePath>>>() {
|
||||
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
|
||||
var root: CompilationUnitTree? = null
|
||||
var lines: LineMap? = null
|
||||
@@ -42,14 +45,14 @@ class MethodRangeScanner(val task: JavacTaskImpl) :
|
||||
|
||||
override fun visitCompilationUnit(
|
||||
node: CompilationUnitTree?,
|
||||
p: MutableList<Pair<Range, MethodTree>>?
|
||||
p: MutableList<Pair<Range, TreePath>>?
|
||||
) {
|
||||
this.root = node
|
||||
this.lines = node?.lineMap
|
||||
return super.visitCompilationUnit(node, p)
|
||||
}
|
||||
|
||||
override fun visitMethod(node: MethodTree?, list: MutableList<Pair<Range, MethodTree>>) {
|
||||
override fun visitMethod(node: MethodTree?, list: MutableList<Pair<Range, TreePath>>) {
|
||||
val result = super.visitMethod(node, list)
|
||||
if (node == null || this.root == null) {
|
||||
return result
|
||||
@@ -58,16 +61,29 @@ class MethodRangeScanner(val task: JavacTaskImpl) :
|
||||
val start = getStartPosition(node)
|
||||
val end = getEndPosition(node)
|
||||
|
||||
list.add(Pair.create(Range(start, end), node))
|
||||
if (start == null || end == null) {
|
||||
log.warn("Method '${node.name}' skipped. Invalid position.")
|
||||
return result
|
||||
}
|
||||
|
||||
list.add(Pair.create(Range(start!!, end!!), currentPath))
|
||||
return result
|
||||
}
|
||||
|
||||
fun getStartPosition(node: Tree): Position {
|
||||
return getPosition(this.pos.getStartPosition(this.root!!, node))
|
||||
fun getStartPosition(node: MethodTree): Position? {
|
||||
val position = this.pos.getStartPosition(this.root!!, node)
|
||||
if (position.toInt() == -1) {
|
||||
return null
|
||||
}
|
||||
return getPosition(position)
|
||||
}
|
||||
|
||||
fun getEndPosition(node: Tree): Position {
|
||||
return getPosition(this.pos.getEndPosition(this.root!!, node))
|
||||
fun getEndPosition(node: MethodTree): Position? {
|
||||
val position = this.pos.getEndPosition(this.root!!, node)
|
||||
if (position.toInt() == -1) {
|
||||
return null
|
||||
}
|
||||
return getPosition(position)
|
||||
}
|
||||
|
||||
fun getPosition(position: Long): Position {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.lsp.java.visitors
|
||||
|
||||
import com.itsaky.androidide.utils.ILogger
|
||||
import com.sun.tools.javac.tree.JCTree
|
||||
import com.sun.tools.javac.tree.JCTree.JCErroneous
|
||||
import com.sun.tools.javac.tree.TreeScanner
|
||||
|
||||
/**
|
||||
* Prints all the trees it visits. For debugging/testing purposes only.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class PrintingVisitor : TreeScanner() {
|
||||
|
||||
private val log = ILogger.newInstance(javaClass.simpleName)
|
||||
|
||||
override fun scan(tree: JCTree?) {
|
||||
log.debug(if (tree != null) tree::class.java else "NullClass", tree)
|
||||
super.scan(tree)
|
||||
}
|
||||
|
||||
override fun visitErroneous(tree: JCErroneous?) {
|
||||
if (tree?.errs != null) {
|
||||
tree.errs.forEach { scan(it) }
|
||||
}
|
||||
super.visitErroneous(tree)
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.lsp.java.visitors;
|
||||
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.tools.javac.tree.EndPosTable;
|
||||
import com.sun.tools.javac.tree.JCTree;
|
||||
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
|
||||
|
||||
/** Helper visitor for partial reparse. Updates tree positions by the given delta. */
|
||||
public class TranslateMethodPositionsVisitor extends ErrorAwareTreeScanner<Void, Void> {
|
||||
|
||||
private final MethodTree changedMethod;
|
||||
private final EndPosTable endPos;
|
||||
private final int delta;
|
||||
boolean active;
|
||||
boolean inMethod;
|
||||
|
||||
public TranslateMethodPositionsVisitor(
|
||||
final MethodTree changedMethod, final EndPosTable endPos, final int delta) {
|
||||
assert endPos != null;
|
||||
this.changedMethod = changedMethod;
|
||||
this.endPos = endPos;
|
||||
this.delta = delta;
|
||||
|
||||
active = changedMethod == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void scan(Tree node, Void p) {
|
||||
if (active && node != null) {
|
||||
if (((JCTree) node).pos >= 0) {
|
||||
((JCTree) node).pos += delta;
|
||||
}
|
||||
}
|
||||
Void result = super.scan(node, p);
|
||||
if (inMethod && node != null) {
|
||||
endPos.replaceTree((JCTree) node, null);
|
||||
}
|
||||
if (active && node != null) {
|
||||
int pos = endPos.replaceTree((JCTree) node, null);
|
||||
int newPos;
|
||||
if (pos < 0) {
|
||||
newPos = pos;
|
||||
} else {
|
||||
newPos = pos + delta;
|
||||
}
|
||||
endPos.storeEnd((JCTree) node, newPos);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitCompilationUnit(CompilationUnitTree node, Void p) {
|
||||
return scan(node.getTypeDecls(), p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitMethod(MethodTree node, Void p) {
|
||||
if (active || inMethod) {
|
||||
scan(node.getModifiers(), p);
|
||||
scan(node.getReturnType(), p);
|
||||
scan(node.getTypeParameters(), p);
|
||||
scan(node.getParameters(), p);
|
||||
scan(node.getThrows(), p);
|
||||
}
|
||||
if (node == changedMethod) {
|
||||
inMethod = true;
|
||||
}
|
||||
if (active || inMethod) {
|
||||
scan(node.getBody(), p);
|
||||
}
|
||||
if (inMethod) {
|
||||
active = true;
|
||||
inMethod = false;
|
||||
}
|
||||
if (active) {
|
||||
scan(node.getDefaultValue(), p);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitVariable(VariableTree node, Void p) {
|
||||
JCVariableDecl varDecl = (JCVariableDecl) node;
|
||||
if (varDecl.sym != null && active && varDecl.sym.pos >= 0) {
|
||||
varDecl.sym.pos += delta;
|
||||
}
|
||||
return super.visitVariable(node, p);
|
||||
}
|
||||
}
|
||||
@@ -17,95 +17,17 @@
|
||||
|
||||
package com.itsaky.lsp.java.visitors;
|
||||
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.MethodTree;
|
||||
import com.sun.source.tree.Tree;
|
||||
import com.sun.source.tree.VariableTree;
|
||||
import com.sun.tools.javac.tree.EndPosTable;
|
||||
import com.sun.tools.javac.tree.JCTree;
|
||||
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
|
||||
import com.sun.tools.javac.tree.TreeScanner;
|
||||
import com.sun.tools.javac.util.List;
|
||||
|
||||
/** Helper visitor for partial reparse. Updates tree positions by the given delta. */
|
||||
public class TranslatePositionsVisitor extends ErrorAwareTreeScanner<Void, Void> {
|
||||
|
||||
private final MethodTree changedMethod;
|
||||
private final EndPosTable endPos;
|
||||
private final int delta;
|
||||
boolean active;
|
||||
boolean inMethod;
|
||||
|
||||
public TranslatePositionsVisitor(
|
||||
final MethodTree changedMethod, final EndPosTable endPos, final int delta) {
|
||||
assert endPos != null;
|
||||
this.changedMethod = changedMethod;
|
||||
this.endPos = endPos;
|
||||
this.delta = delta;
|
||||
|
||||
active = changedMethod == null;
|
||||
}
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class TranslatePositionsVisitor extends TreeScanner {
|
||||
|
||||
@Override
|
||||
public Void scan(Tree node, Void p) {
|
||||
if (active && node != null) {
|
||||
if (((JCTree) node).pos >= 0) {
|
||||
((JCTree) node).pos += delta;
|
||||
}
|
||||
}
|
||||
Void result = super.scan(node, p);
|
||||
if (inMethod && node != null) {
|
||||
endPos.replaceTree((JCTree) node, null);
|
||||
}
|
||||
if (active && node != null) {
|
||||
Integer pos = endPos.replaceTree((JCTree) node, null);
|
||||
if (pos != null) {
|
||||
int newPos;
|
||||
if (pos < 0) {
|
||||
newPos = pos;
|
||||
} else {
|
||||
newPos = pos + delta;
|
||||
}
|
||||
endPos.storeEnd((JCTree) node, newPos);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitCompilationUnit(CompilationUnitTree node, Void p) {
|
||||
return scan(node.getTypeDecls(), p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitMethod(MethodTree node, Void p) {
|
||||
if (active || inMethod) {
|
||||
scan(node.getModifiers(), p);
|
||||
scan(node.getReturnType(), p);
|
||||
scan(node.getTypeParameters(), p);
|
||||
scan(node.getParameters(), p);
|
||||
scan(node.getThrows(), p);
|
||||
}
|
||||
if (node == changedMethod) {
|
||||
inMethod = true;
|
||||
}
|
||||
if (active || inMethod) {
|
||||
scan(node.getBody(), p);
|
||||
}
|
||||
if (inMethod) {
|
||||
active = true;
|
||||
inMethod = false;
|
||||
}
|
||||
if (active) {
|
||||
scan(node.getDefaultValue(), p);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitVariable(VariableTree node, Void p) {
|
||||
JCVariableDecl varDecl = (JCVariableDecl) node;
|
||||
if (varDecl.sym != null && active && varDecl.sym.pos >= 0) {
|
||||
varDecl.sym.pos += delta;
|
||||
}
|
||||
return super.visitVariable(node, p);
|
||||
public void scan(final List<? extends JCTree> trees) {
|
||||
super.scan(trees);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,17 @@ package com.itsaky.lsp.java.partial
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.lsp.java.BaseJavaTest
|
||||
import com.itsaky.lsp.java.JavaLanguageServer
|
||||
import com.itsaky.lsp.java.visitors.FindMethodAt
|
||||
import com.itsaky.lsp.java.compiler.SourceFileObject
|
||||
import com.itsaky.lsp.java.models.CompilationRequest
|
||||
import com.itsaky.lsp.java.models.PartialReparseRequest
|
||||
import com.itsaky.lsp.models.ChangeType.INSERT
|
||||
import com.itsaky.lsp.models.DocumentChangeEvent
|
||||
import com.sun.source.tree.ExpressionStatementTree
|
||||
import com.sun.source.tree.LiteralTree
|
||||
import com.sun.source.tree.MethodTree
|
||||
import com.sun.source.tree.Tree
|
||||
import com.sun.tools.javac.tree.JCTree
|
||||
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit
|
||||
import com.sun.tools.javac.tree.JCTree.JCErroneous
|
||||
import com.sun.tools.javac.tree.JCTree.JCMethodDecl
|
||||
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation
|
||||
import com.sun.tools.javac.tree.JCTree.JCVariableDecl
|
||||
@@ -47,24 +52,56 @@ class PartialReparserImplTest : BaseJavaTest() {
|
||||
fun parseMethod() {
|
||||
openFile("PartialReparserTest")
|
||||
jls.compiler.compile(file).run { task ->
|
||||
val reparser = PartialReparserImpl()
|
||||
val method = FindMethodAt(task.task).scan(task.root(), 133)
|
||||
val reparsed =
|
||||
reparser.reparseMethod(
|
||||
CompilationInfo(task.task, task.diagnosticListener, task.root()),
|
||||
method!!.leaf as MethodTree,
|
||||
"{System.out.println(\"Hello world!\"); var klass = String.class;}"
|
||||
)
|
||||
|
||||
assertThat(reparsed).isTrue()
|
||||
AssertingScanner().scan(task.root() as JCCompilationUnit)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleErrorneousStatement() {
|
||||
openFile("PartialErrReparserTest")
|
||||
jls.compiler
|
||||
.compile(
|
||||
CompilationRequest(
|
||||
listOf(SourceFileObject(file)),
|
||||
PartialReparseRequest(172, contents.toString())
|
||||
)
|
||||
)
|
||||
.run { PrintingScanner().scan(it.root() as JCCompilationUnit) }
|
||||
jls.onContentChange(
|
||||
DocumentChangeEvent(
|
||||
file!!,
|
||||
contents!!.insert(192, "trim().").toString(),
|
||||
2,
|
||||
INSERT,
|
||||
"trim().".length
|
||||
)
|
||||
)
|
||||
jls.compiler.compile(
|
||||
CompilationRequest(
|
||||
listOf(SourceFileObject(file)),
|
||||
PartialReparseRequest(179, contents.toString())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun openFile(fileName: String) {
|
||||
super.openFile("partial/$fileName")
|
||||
}
|
||||
|
||||
class PrintingScanner : TreeScanner() {
|
||||
override fun scan(tree: JCTree?) {
|
||||
if (tree != null) println(tree!!::class.java.name + ", " + tree)
|
||||
super.scan(tree)
|
||||
}
|
||||
|
||||
override fun visitErroneous(tree: JCErroneous?) {
|
||||
if (tree?.errs != null) {
|
||||
tree.errs.forEach { scan(it) }
|
||||
}
|
||||
super.visitErroneous(tree)
|
||||
}
|
||||
}
|
||||
|
||||
class AssertingScanner : TreeScanner() {
|
||||
private var methodCount = 0
|
||||
override fun visitMethodDef(tree: JCMethodDecl?) {
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.itsaky.lsp.java.providers
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.lsp.java.BaseJavaTest
|
||||
import com.itsaky.lsp.models.ChangeType.NEW_TEXT
|
||||
import com.itsaky.lsp.models.DocumentChangeEvent
|
||||
import com.itsaky.lsp.models.ExpandSelectionParams
|
||||
import com.itsaky.lsp.models.Position
|
||||
@@ -44,7 +45,9 @@ class JavaSelectionProviderTest : BaseJavaTest() {
|
||||
openFile("SimpleSelectionExpansionTest")
|
||||
cursor = requireCursor()
|
||||
deleteCursorText()
|
||||
mServer.documentHandler.onContentChange(DocumentChangeEvent(file!!, contents.toString(), 1))
|
||||
mServer.documentHandler.onContentChange(
|
||||
DocumentChangeEvent(file!!, contents.toString(), 1, NEW_TEXT, 0)
|
||||
)
|
||||
|
||||
val range = findRange()
|
||||
val expanded = mServer.expandSelection(ExpandSelectionParams(file!!, range))
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.itsaky.lsp.java.partial;
|
||||
|
||||
public class PartialReparserTest_template {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello World!");
|
||||
var str = "";
|
||||
str.;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.itsaky.lsp.api
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.lsp.models.ChangeType.NEW_TEXT
|
||||
import com.itsaky.lsp.models.DocumentChangeEvent
|
||||
import com.itsaky.lsp.models.Position
|
||||
import io.github.rosemoe.sora.text.Content
|
||||
@@ -44,7 +45,7 @@ abstract class CursorDependentTest : LoggingTest() {
|
||||
|
||||
// As the content has been changed, we have to
|
||||
// Update the content in language server
|
||||
getServer().documentHandler.onContentChange(DocumentChangeEvent(file!!, contents!!, 1))
|
||||
getServer().documentHandler.onContentChange(DocumentChangeEvent(file!!, contents!!, 1, NEW_TEXT, 0))
|
||||
}
|
||||
|
||||
fun cursorPosition(): Position {
|
||||
|
||||
@@ -1597,7 +1597,7 @@
|
||||
name="textEditNoPasteWindowLayout"
|
||||
format="reference" />
|
||||
<!-- Used instead of textEditPasteWindowLayout when the window is moved on the side of the
|
||||
insertion cursor because it would be clipped if it were positioned on top. -->
|
||||
edit cursor because it would be clipped if it were positioned on top. -->
|
||||
<attr
|
||||
name="textEditSidePasteWindowLayout"
|
||||
format="reference" />
|
||||
@@ -2031,7 +2031,7 @@
|
||||
name="textColorLink"
|
||||
format="reference|color" />
|
||||
|
||||
<!-- Reference to a drawable that will be drawn under the insertion cursor. -->
|
||||
<!-- Reference to a drawable that will be drawn under the edit cursor. -->
|
||||
<attr
|
||||
name="textCursorDrawable"
|
||||
format="reference" />
|
||||
@@ -8352,7 +8352,7 @@
|
||||
<!-- Variation of textEditPasteWindowLayout displayed when the clipboard is empty. -->
|
||||
<attr name="textEditNoPasteWindowLayout" />
|
||||
<!-- Used instead of textEditPasteWindowLayout when the window is moved on the side of the
|
||||
insertion cursor because it would be clipped if it were positioned on top. -->
|
||||
edit cursor because it would be clipped if it were positioned on top. -->
|
||||
<attr name="textEditSidePasteWindowLayout" />
|
||||
<!-- Variation of textEditSidePasteWindowLayout displayed when the clipboard is empty. -->
|
||||
<attr name="textEditSideNoPasteWindowLayout" />
|
||||
@@ -8365,7 +8365,7 @@
|
||||
<attr name="textEditSuggestionHighlightStyle" />
|
||||
|
||||
|
||||
<!-- Reference to a drawable that will be drawn under the insertion cursor. -->
|
||||
<!-- Reference to a drawable that will be drawn under the edit cursor. -->
|
||||
<attr name="textCursorDrawable" />
|
||||
|
||||
<!-- Indicates that the content of a non-editable text can be selected. -->
|
||||
|
||||
Reference in New Issue
Block a user