Fixed: Invalid change delta in Java compiler

This commit is contained in:
Akash Yadav
2022-07-07 23:01:50 +05:30
parent 4ff232a3d1
commit 87875f6c84
11 changed files with 102 additions and 121 deletions
@@ -240,6 +240,7 @@ class PartialReparserImpl : PartialReparser {
l.useSource(prevLogged)
}
} catch (err: Throwable) {
log.error("An error occurred while reparsing method", err)
return false
}
@@ -306,7 +306,7 @@ object ProjectManager : EventReceiver {
if (subModule !is ModuleProject) {
continue
}
if (subModule.onDocumentChanged(event)) {
break
}
@@ -125,15 +125,6 @@ public interface ILanguageServer {
*/
void applySettings(@Nullable IServerSettings settings);
/**
* Notify the language server that the project's configuration was changed. Language servers
* decide what type of object they want to receive as configuration.
*
* @param newConfiguration The new configuration object. Only a specific type of object might be
* accepted by observers.
*/
void configurationChanged(Object newConfiguration);
/**
* Setup this language server with the given project. Servers are not expected to keep a reference
* to the provided project. Instead, use {@link ProjectManager#getRootProject()} to obtain the
@@ -44,6 +44,11 @@ public class JavaCompilerProvider {
return JavaCompilerProvider.getInstance().forModule(module);
}
/** For internal use only. */
public JavaCompilerService getCompilerService() {
return compilerService;
}
public synchronized JavaCompilerService forModule(ModuleProject module) {
// A module instance is set to the compiler only in case the project is initialized or
// this method was called with other mdoule instance.
@@ -154,7 +154,7 @@ public class JavaLanguageServer implements ILanguageServer {
capabilities.setSmartSelectionsEnabled(true);
LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu.class);
if (EventBus.getDefault().isRegistered(this)) {
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
initialized = true;
@@ -195,11 +195,13 @@ public class JavaLanguageServer implements ILanguageServer {
this.settings = settings;
}
@Override
public void configurationChanged(Object newConfiguration) {}
@Override
public void setupWithProject(@NonNull final Project project) {
// Make sure we're registered with EventBus
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
// Once we have project initialized
// Destory the NO_MODULE_COMPILER instance
JavaCompilerService.NO_MODULE_COMPILER.destroy();
@@ -306,6 +308,17 @@ public class JavaLanguageServer implements ILanguageServer {
@Subscribe(threadMode = ThreadMode.ASYNC)
@SuppressWarnings("unused")
public void onContentChange(@NonNull DocumentChangeEvent event) {
if (!DocumentUtils.isJavaFile(event.getChangedFile())) {
return;
}
// TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance
JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event);
final JavaCompilerService compilerService =
JavaCompilerProvider.getInstance().getCompilerService();
if (compilerService != null) {
compilerService.onDocumentChange(event);
}
startOrRestartAnalyzeTimer();
}
@@ -21,11 +21,13 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
import com.itsaky.androidide.javac.services.compiler.ReusableCompiler;
import com.itsaky.androidide.javac.services.partial.CompilationInfo;
import com.itsaky.androidide.javac.services.partial.PartialReparser;
import com.itsaky.androidide.javac.services.partial.PartialReparserImpl;
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
import com.itsaky.androidide.lsp.java.models.PartialReparseRequest;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import com.itsaky.androidide.lsp.java.parser.Parser;
import com.itsaky.androidide.lsp.java.utils.Extractors;
@@ -33,11 +35,10 @@ import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarations;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.models.ActiveDocument;
import com.itsaky.androidide.utils.BootstrapClassesProvider;
import com.itsaky.androidide.projects.util.Cache;
import com.itsaky.androidide.projects.util.SourceClassTrie;
import com.itsaky.androidide.projects.util.StringSearch;
import com.itsaky.androidide.utils.BootstrapClassesProvider;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.StopWatch;
import com.sun.source.tree.CompilationUnitTree;
@@ -89,6 +90,7 @@ public class JavaCompilerService implements CompilerProvider {
public ReusableCompiler compiler = new ReusableCompiler();
private CompileBatch cachedCompile;
private int changeDelta = 0;
// The module project must not be null
// It is marked as nullable just for some special cases like tests
@@ -217,27 +219,6 @@ public class JavaCompilerService implements CompilerProvider {
return candidates.toArray(new Path[0]);
}
private boolean containsWord(Path file, String word) {
if (cacheContainsWord.needs(file, word)) {
cacheContainsWord.load(file, word, StringSearch.containsWord(file, word));
}
return cacheContainsWord.get(file, word);
}
private boolean containsImport(Path file, String className) {
String packageName = Extractors.packageName(className);
if (packageNameOrEmpty(file).equals(packageName)) {
return true;
}
String star = packageName + ".*";
for (String i : readImports(file)) {
if (i.equals(className) || i.equals(star)) {
return true;
}
}
return false;
}
@Override
public Path[] findMemberReferences(String className, String memberName) {
List<Path> candidates = new ArrayList<>();
@@ -269,6 +250,52 @@ public class JavaCompilerService implements CompilerProvider {
return compileBatch(request);
}
public synchronized void close() {
if (cachedCompile != null) {
cachedCompile.close();
cachedCompile.borrow.close();
}
}
public void destroy() {
synchronizedTask.post(
() -> {
close();
cachedCompile = null;
cachedModified.clear();
compiler = new ReusableCompiler();
});
}
public SynchronizedTask getSynchronizedTask() {
return synchronizedTask;
}
public void onDocumentChange(@NonNull DocumentChangeEvent event) {
this.changeDelta += event.getChangeDelta();
}
private boolean containsWord(Path file, String word) {
if (cacheContainsWord.needs(file, word)) {
cacheContainsWord.load(file, word, StringSearch.containsWord(file, word));
}
return cacheContainsWord.get(file, word);
}
private boolean containsImport(Path file, String className) {
String packageName = Extractors.packageName(className);
if (packageNameOrEmpty(file).equals(packageName)) {
return true;
}
String star = packageName + ".*";
for (String i : readImports(file)) {
if (i.equals(className) || i.equals(star)) {
return true;
}
}
return false;
}
private SynchronizedTask compileBatch(CompilationRequest request) {
synchronizedTask.post(
() -> {
@@ -321,7 +348,8 @@ public class JavaCompilerService implements CompilerProvider {
private void tryReparse(@NonNull final CompilationRequest request) {
// Satisfy lint
Objects.requireNonNull(request.partialRequest);
final PartialReparseRequest partialRequest = request.partialRequest;
Objects.requireNonNull(partialRequest);
final StopWatch watch = new StopWatch("Method reparse");
final File file = new File(request.sources.iterator().next().toUri());
@@ -334,7 +362,7 @@ public class JavaCompilerService implements CompilerProvider {
}
final Pair<Range, TreePath> currentMethod =
binarySearchCurrentMethod(positions, request.partialRequest.cursor);
binarySearchCurrentMethod(positions, partialRequest.cursor);
if (currentMethod == null) {
LOG.warn("Cannot perform reparse. Unable to find current method");
recompile(request);
@@ -346,13 +374,6 @@ public class JavaCompilerService implements CompilerProvider {
final MethodTree methodTree = (MethodTree) currentMethod.second.getLeaf();
LOG.debug("Trying to reparse method:", methodTree.getName());
final int changeDelta;
final ActiveDocument document = ProjectManager.INSTANCE.getActiveDocument(file.toPath());
if (document != null) {
changeDelta = document.getChangDelta();
} else {
changeDelta = 0;
}
final CompilationInfo info =
new CompilationInfo(
cachedCompile.task, cachedCompile.diagnosticListener, cachedCompile.roots.get(0));
@@ -360,26 +381,26 @@ public class JavaCompilerService implements CompilerProvider {
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()) + changeDelta;
(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()) {
if (end < 0 || end > partialRequest.contents.length()) {
LOG.warn(
"Cannot reparse. Invalid change delta. end:",
end,
"changeDelta:",
changeDelta,
this.changeDelta,
"content.length:",
request.partialRequest.contents.length());
partialRequest.contents.length());
recompile(request);
return;
}
final String newBody = request.partialRequest.contents.substring(start, end);
final String newBody = partialRequest.contents.substring(start, end);
final boolean reparsed =
reparser.reparseMethod(
info, currentMethod.second, newBody, request.partialRequest.contents);
info, currentMethod.second, newBody, partialRequest.contents);
if (!reparsed) {
LOG.error("Failed to reparse");
recompile(request);
@@ -390,10 +411,7 @@ public class JavaCompilerService implements CompilerProvider {
LOG.info("Successfully reparsed method", methodTree.getName());
updateModificationCache(request);
cachedCompile.updatePositions(info.cu, true);
if (document != null) {
document.setChangDelta(0);
}
this.changeDelta = 0;
}
@Nullable
@@ -457,25 +475,6 @@ public class JavaCompilerService implements CompilerProvider {
return new CompileBatch(this, moreSources);
}
public synchronized void close() {
if (cachedCompile != null && !cachedCompile.closed) {
cachedCompile.close();
if (!cachedCompile.borrow.closed) {
cachedCompile.borrow.close();
}
}
}
public void destroy() {
synchronizedTask.post(
() -> {
close();
cachedCompile = null;
cachedModified.clear();
compiler = new ReusableCompiler();
});
}
private boolean containsType(Path file, String className) {
if (cacheContainsType.needs(file, null)) {
CompilationUnitTree root = parse(file).root;
@@ -507,8 +506,4 @@ public class JavaCompilerService implements CompilerProvider {
}
return file;
}
public SynchronizedTask getSynchronizedTask() {
return synchronizedTask;
}
}
@@ -36,8 +36,8 @@ package com.itsaky.androidide.lsp.java.compiler;
import androidx.annotation.NonNull;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.lsp.java.CompilationCancellationException;
import com.itsaky.androidide.utils.ILogger;
import java.util.concurrent.Semaphore;
import java.util.function.Consumer;
@@ -52,9 +52,16 @@ public class SynchronizedTask {
private CompileTask task;
public void run(@NonNull Consumer<CompileTask> taskConsumer) {
semaphore.acquireUninterruptibly();
try {
semaphore.acquire();
} catch (InterruptedException e) {
throw new CompilationCancellationException(e);
}
try {
taskConsumer.accept(this.task);
} catch (Throwable err) {
LOG.error("An error occurred while working with compilation task", err);
throw err;
} finally {
semaphore.release();
}
@@ -69,6 +76,9 @@ public class SynchronizedTask {
try {
return function.invoke(this.task);
} catch (Throwable err) {
LOG.error("An error occurred while working with compilation task", err);
throw err;
} finally {
semaphore.release();
}
@@ -88,6 +98,9 @@ public class SynchronizedTask {
this.task.close();
}
run.run();
} catch (Throwable err) {
LOG.error("An error occurred", err);
throw err;
} finally {
semaphore.release();
isCompiling = false;
@@ -205,8 +205,7 @@ open class CompletionItem(
val start = getIdentifierStart(text.getLine(line), column)
val shift = insertText.contains("$0")
text.beginBatchEdit()
text.delete(line, start, line, column)
if (text.contains("\n")) {
@@ -234,7 +233,8 @@ open class CompletionItem(
editor.text.delete(l, c, l, c + 2)
}
}
text.beginBatchEdit()
if (additionalEditHandler != null) {
additionalEditHandler!!.performEdits(editor, this)
} else if (additionalTextEdits != null && additionalTextEdits!!.isNotEmpty()) {
@@ -1,34 +0,0 @@
/*
* 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.androidide.lsp.models
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
@@ -127,9 +127,6 @@ public class XMLLanguageServer implements ILanguageServer {
this.settings = settings;
}
@Override
public void configurationChanged(Object newConfiguration) {}
@Override
public void setupWithProject(@NonNull final Project project) {}