Implemented partial reparse in JavaCompilerService

This commit is contained in:
Akash Yadav
2022-06-27 21:35:14 +05:30
parent da28a1ba5e
commit 500bd6b190
27 changed files with 667 additions and 243 deletions
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);
@@ -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()
}
}
@@ -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)
}
}
@@ -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 {
+4 -4
View File
@@ -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. -->