build: reorganize and reconfigure modules to resolve dependency resolution errors

Gradle, for some reason, does not the path of modules to identify them uniquely. Instead, the 'build id' is used to do so, which consists of the maven group and artifact ID. So, even if two modules are located in separate directories, but have the name group and artifact ID, including both of them in other modules may result in dependency resolution errors.
This commit is contained in:
Akash Yadav
2024-05-29 13:11:44 +05:30
parent 4ada1c3c65
commit bfbd49dc3b
2457 changed files with 1069 additions and 709 deletions
+1
View File
@@ -0,0 +1 @@
/build
+35
View File
@@ -0,0 +1,35 @@
import com.itsaky.androidide.build.config.BuildConfig
plugins {
id("com.android.library")
id("kotlin-android")
}
groupConfig {
groupIdSuffix.set("java")
}
android {
namespace = "${BuildConfig.packageName}.javac.services"
buildTypes {
release {
isMinifyEnabled = false
}
}
}
dependencies {
api(libs.composite.javac)
implementation(libs.common.kotlin)
implementation(libs.common.utilcode)
implementation(libs.google.guava)
implementation(projects.core.common)
implementation(projects.logging.logger)
testImplementation(libs.tests.junit)
testImplementation(libs.tests.google.truth)
testImplementation(libs.tests.robolectric)
}
@@ -0,0 +1,18 @@
<!--
~ 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/>.
-->
<manifest/>
@@ -0,0 +1,46 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.util.Abort;
/**
* @author Tomas Zezula
*/
public final class CancelAbort extends Abort {
CancelAbort() {}
}
@@ -0,0 +1,73 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.util.Context;
/**
* @author Tomas Zezula
*/
public class CancelService {
/** The context key for the parameter name resolver. */
protected static final Context.Key<CancelService> cancelServiceKey = new Context.Key<>();
protected CancelService() {}
public static CancelService instance(Context context) {
CancelService instance = context.get(cancelServiceKey);
if (instance == null) {
instance = new CancelService();
context.put(cancelServiceKey, instance);
}
return instance;
}
public static void preRegister(Context context, CancelService impl) {
context.put(CancelService.cancelServiceKey, impl);
}
public final void abortIfCanceled() {
if (isCanceled()) {
throw new CancelAbort();
}
}
public boolean isCanceled() {
return false;
}
}
@@ -0,0 +1,165 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import openjdk.tools.javac.code.Type;
import openjdk.tools.javac.comp.Attr;
import openjdk.tools.javac.comp.AttrContext;
import openjdk.tools.javac.comp.Env;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.tree.JCTree.JCBlock;
import openjdk.tools.javac.tree.JCTree.JCCatch;
import openjdk.tools.javac.tree.JCTree.JCClassDecl;
import openjdk.tools.javac.tree.JCTree.JCExpression;
import openjdk.tools.javac.tree.JCTree.JCMethodDecl;
import openjdk.tools.javac.tree.JCTree.JCVariableDecl;
import openjdk.tools.javac.tree.TreeMaker;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.List;
/**
* @author lahvac
*/
public class NBAttr extends Attr {
private final CancelService cancelService;
private final TreeMaker tm;
private boolean fullyAttribute;
private Env<AttrContext> fullyAttributeResult;
public static void preRegister(Context context) {
context.put(attrKey, (Context.Factory<Attr>) NBAttr::new);
}
public NBAttr(Context context) {
super(context);
cancelService = CancelService.instance(context);
tm = TreeMaker.instance(context);
}
@Override
public void visitCatch(JCCatch that) {
super.visitBlock(tm.Block(0, List.of(that.param, that.body)));
}
public Env<AttrContext> attributeAndCapture(JCTree tree, Env<AttrContext> env, JCTree to) {
try {
fullyAttribute = true;
Env<AttrContext> result =
tree instanceof JCExpression
? attribExprToTree((JCExpression) tree, env, to)
: attribStatToTree(tree, env, to);
return fullyAttributeResult != null ? fullyAttributeResult : result;
} finally {
fullyAttribute = false;
}
}
protected void breakTreeFound(Env<AttrContext> env) {
if (fullyAttribute) {
fullyAttributeResult = env;
} else {
try {
MethodHandles.lookup()
.findSpecial(
Attr.class,
"breakTreeFound",
MethodType.methodType(void.class, Env.class),
NBAttr.class)
.invokeExact(this, env);
} catch (Throwable ex) {
sneakyThrows(ex);
}
}
}
private <T extends Throwable> void sneakyThrows(Throwable t) throws T {
throw (T) t;
}
protected void breakTreeFound(Env<AttrContext> env, Type result) {
if (fullyAttribute) {
fullyAttributeResult = env;
} else {
try {
MethodHandles.lookup()
.findSpecial(
Attr.class,
"breakTreeFound",
MethodType.methodType(void.class, Env.class, Type.class),
NBAttr.class)
.invokeExact(this, env, result);
} catch (Throwable ex) {
sneakyThrows(ex);
}
}
}
@Override
public Type attribType(JCTree tree, Env<AttrContext> env) {
cancelService.abortIfCanceled();
return super.attribType(tree, env);
}
@Override
public void visitClassDef(JCClassDecl tree) {
cancelService.abortIfCanceled();
super.visitClassDef(tree);
}
@Override
public void visitMethodDef(JCMethodDecl tree) {
cancelService.abortIfCanceled();
super.visitMethodDef(tree);
}
@Override
public void visitVarDef(JCVariableDecl tree) {
// for erroneous "var", make sure the synthetic make.Error() has an invalid/synthetic position:
tm.at(-1);
super.visitVarDef(tree);
}
@Override
public void visitBlock(JCBlock tree) {
cancelService.abortIfCanceled();
super.visitBlock(tree);
}
}
@@ -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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Locale;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import jdkx.tools.JavaFileObject;
import openjdk.tools.javac.code.ClassFinder;
import openjdk.tools.javac.code.Flags;
import openjdk.tools.javac.code.Kinds.Kind;
import openjdk.tools.javac.code.Symbol;
import openjdk.tools.javac.code.Symbol.Completer;
import openjdk.tools.javac.code.Symbol.CompletionFailure;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.JCDiagnostic;
import openjdk.tools.javac.util.JCDiagnostic.DiagnosticInfo;
import openjdk.tools.javac.util.JCDiagnostic.DiagnosticType;
import openjdk.tools.javac.util.JCDiagnostic.SimpleDiagnosticPosition;
import openjdk.tools.javac.util.Log;
import openjdk.tools.javac.util.Names;
/**
* @author lahvac
*/
public class NBClassFinder extends ClassFinder {
private final Context context;
private final Names names;
private final JCDiagnostic.Factory diagFactory;
private final Log log;
private Completer completer;
public static void preRegister(Context context) {
context.put(classFinderKey, (Context.Factory<ClassFinder>) NBClassFinder::new);
}
public NBClassFinder(Context context) {
super(context);
this.context = context;
this.names = Names.instance(context);
this.diagFactory = JCDiagnostic.Factory.instance(context);
this.log = Log.instance(context);
}
@Override
public Completer getCompleter() {
if (completer == null) {
try {
Class.forName("openjdk.tools.javac.model.LazyTreeLoader");
// patched nb-javac, handles missing java.lang itself:
completer = super.getCompleter();
} catch (ClassNotFoundException e) {
Completer delegate = super.getCompleter();
completer =
sym -> {
delegate.complete(sym);
if (sym.kind == Kind.PCK
&& sym.flatName() == names.java_lang
&& sym.members().isEmpty()) {
sym.flags_field |= Flags.EXISTS;
try {
Class<?> dcfhClass =
Class.forName("openjdk.tools.javac.code.DeferredCompletionFailureHandler");
Constructor<CompletionFailure> constr =
CompletionFailure.class.getDeclaredConstructor(
Symbol.class, Supplier.class, dcfhClass);
Object dcfh =
dcfhClass.getDeclaredMethod("instance", Context.class).invoke(null, context);
throw constr.newInstance(
sym,
(Supplier<JCDiagnostic>)
() ->
diagFactory.create(
log.currentSource(),
new SimpleDiagnosticPosition(0),
DiagnosticInfo.of(
DiagnosticType.ERROR,
"compiler",
"cant.resolve",
"package",
"java.lang")),
dcfh);
} catch (ClassNotFoundException
| NoSuchMethodException
| SecurityException
| IllegalAccessException
| IllegalArgumentException
| InvocationTargetException
| InstantiationException ex) {
Logger.getLogger(NBClassFinder.class.getName()).log(Level.FINE, null, ex);
}
}
};
}
}
return completer;
}
@Override
protected JavaFileObject preferredFileObject(JavaFileObject a, JavaFileObject b) {
if (b.getName().toLowerCase(Locale.ROOT).endsWith(".sig")) {
// do not prefer sources over sig files (unless sources are newer):
boolean prevPreferSource = preferSource;
try {
preferSource = false;
return super.preferredFileObject(a, b);
} finally {
preferSource = prevPreferSource;
}
}
return super.preferredFileObject(a, b);
}
}
@@ -0,0 +1,163 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import static openjdk.tools.javac.jvm.ClassFile.Version.V45_3;
import android.text.TextUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import jdkx.tools.ForwardingJavaFileObject;
import jdkx.tools.JavaFileObject;
import openjdk.tools.javac.code.ClassFinder.BadClassFile;
import openjdk.tools.javac.code.Symbol;
import openjdk.tools.javac.code.Symbol.ClassSymbol;
import openjdk.tools.javac.jvm.ClassFile;
import openjdk.tools.javac.jvm.ClassFile.Version;
import openjdk.tools.javac.jvm.ClassReader;
import openjdk.tools.javac.resources.CompilerProperties.Warnings;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Log;
import openjdk.tools.javac.util.Name;
import org.slf4j.LoggerFactory;
/**
* @author lahvac
*/
public class NBClassReader extends ClassReader {
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(NBClassReader.class);
private final NBNames nbNames;
private final Log log;
public static void preRegister(Context context) {
context.put(classReaderKey, (Context.Factory<ClassReader>) NBClassReader::new);
}
public NBClassReader(Context context) {
super(context);
nbNames = NBNames.instance(context);
log = Log.instance(context);
NBAttributeReader[] readers = {
new NBAttributeReader(
nbNames._org_netbeans_EnclosingMethod, V45_3, CLASS_OR_MEMBER_ATTRIBUTE) {
public void read(Symbol sym, int attrLen) {
int newbp = bp + attrLen;
readEnclosingMethodAttr(sym);
bp = newbp;
}
},
};
for (NBAttributeReader r : readers) {
attributeReaders.put(r.getName(), r);
}
}
@Override
public BadClassFile badClassFile(final String key, final Object... args) {
LOG.debug("Bad class file {} {}", key, TextUtils.join(", ", args), new RuntimeException());
return super.badClassFile(key, args);
}
@Override
public void readClassFile(ClassSymbol c) {
try {
super.readClassFile(c);
} catch (BadClassFile cf) {
if ("compiler.misc.bad.class.file.header".equals(cf.getDiagnostic().getCode())) {
JavaFileObject origFile = c.classfile;
try (InputStream in = origFile.openInputStream()) {
byte[] data = readFile(in);
int major = (Byte.toUnsignedInt(data[6]) << 8) + Byte.toUnsignedInt(data[7]);
int maxMajor = ClassFile.Version.MAX().major;
if (maxMajor < major) {
if (log.currentSourceFile() != null) {
log.warning(0, Warnings.BigMajorVersion(origFile, major, maxMajor));
}
data[6] = (byte) (maxMajor >> 8);
data[7] = (byte) (maxMajor & 0xFF);
c.classfile =
new ForwardingJavaFileObject<JavaFileObject>(origFile) {
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(data);
}
};
super.readClassFile(c);
return;
}
} catch (IOException ex) {
Logger.getLogger(NBClassReader.class.getName()).log(Level.FINE, null, ex);
} finally {
c.classfile = origFile;
}
}
throw cf;
}
}
static byte[] readFile(final InputStream in) throws IOException {
byte[] data = new byte[Math.max(in.available(), 256)];
int off = 0;
int read;
while ((read = in.read(data, off, data.length - off)) != (-1)) {
off += read;
if (data.length == off) {
data = Arrays.copyOf(data, 2 * (data.length + in.available()));
}
}
return Arrays.copyOf(data, off);
}
private abstract class NBAttributeReader extends AttributeReader {
private NBAttributeReader(Name name, Version version, Set<AttributeKind> kinds) {
super(name, version, kinds);
}
private Name getName() {
return name;
}
}
}
@@ -0,0 +1,59 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.code.Types;
import openjdk.tools.javac.jvm.ClassWriter;
import openjdk.tools.javac.util.Context;
/**
* @author lahvac
*/
public class NBClassWriter extends ClassWriter {
private final NBNames nbNames;
private final Types types;
public static void preRegister(Context context) {
context.put(classWriterKey, (Context.Factory<ClassWriter>) NBClassWriter::new);
}
protected NBClassWriter(Context context) {
super(context);
nbNames = NBNames.instance(context);
types = Types.instance(context);
}
}
@@ -0,0 +1,98 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.code.Symbol.TypeSymbol;
import openjdk.tools.javac.code.Symtab;
import openjdk.tools.javac.comp.AttrContext;
import openjdk.tools.javac.comp.Enter;
import openjdk.tools.javac.comp.Env;
import openjdk.tools.javac.main.JavaCompiler;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.tree.JCTree.JCClassDecl;
import openjdk.tools.javac.tree.TreeInfo;
import openjdk.tools.javac.util.Context;
/**
* @author lahvac
*/
public class NBEnter extends Enter {
private final CancelService cancelService;
private final Symtab syms;
private final NBJavaCompiler compiler;
public static void preRegister(Context context) {
context.put(enterKey, (Context.Factory<Enter>) NBEnter::new);
}
public NBEnter(Context context) {
super(context);
cancelService = CancelService.instance(context);
syms = Symtab.instance(context);
JavaCompiler c = JavaCompiler.instance(context);
compiler = c instanceof NBJavaCompiler ? (NBJavaCompiler) c : null;
}
@SuppressWarnings("unused")
public void doUnenter(JCTree.JCCompilationUnit cu, JCTree tree) {
super.unenter(cu, tree);
}
@Override
public Env<AttrContext> getEnv(TypeSymbol sym) {
Env<AttrContext> env = super.getEnv(sym);
if (compiler != null) {
compiler.maybeInvokeDesugarCallback(env);
}
return env;
}
@Override
public void visitTopLevel(JCTree.JCCompilationUnit tree) {
if (TreeInfo.isModuleInfo(tree) && tree.modle == syms.noModule) {
// workaround: when source level == 8, then visitTopLevel crashes for module-info.java
return;
}
super.visitTopLevel(tree);
}
@Override
public void visitClassDef(JCClassDecl tree) {
cancelService.abortIfCanceled();
super.visitClassDef(tree);
}
}
@@ -0,0 +1,108 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import java.util.Collection;
import java.util.Collections;
import java.util.Queue;
import java.util.function.Consumer;
import openjdk.tools.javac.comp.AttrContext;
import openjdk.tools.javac.comp.Env;
import openjdk.tools.javac.main.JavaCompiler;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.List;
import openjdk.tools.javac.util.Pair;
/**
* @author lahvac
*/
public class NBJavaCompiler extends JavaCompiler {
private final CancelService cancelService;
private Consumer<Env<AttrContext>> desugarCallback;
private boolean desugaring;
public static void preRegister(Context context) {
context.put(compilerKey, (Context.Factory<JavaCompiler>) NBJavaCompiler::new);
}
public NBJavaCompiler(Context context) {
super(context);
cancelService = CancelService.instance(context);
}
@Override
public void processAnnotations(
List<JCTree.JCCompilationUnit> roots, Collection<String> classnames) {
if (roots.isEmpty()) {
super.processAnnotations(roots, classnames);
} else {
setOrigin(roots.head.sourcefile.toUri().toString());
try {
super.processAnnotations(roots, classnames);
} finally {
setOrigin("");
}
}
}
private void setOrigin(String origin) {
fileManager.handleOption("apt-origin", Collections.singletonList(origin).iterator());
}
@Override
protected void desugar(
Env<AttrContext> env, Queue<Pair<Env<AttrContext>, JCTree.JCClassDecl>> results) {
boolean prevDesugaring = desugaring;
try {
desugaring = true;
super.desugar(env, results);
} finally {
desugaring = prevDesugaring;
}
}
public void setDesugarCallback(Consumer<Env<AttrContext>> callback) {
this.desugarCallback = callback;
}
void maybeInvokeDesugarCallback(Env<AttrContext> env) {
if (desugaring && desugarCallback != null) {
desugarCallback.accept(env);
}
}
}
@@ -0,0 +1,96 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import java.util.HashMap;
import java.util.Map;
import jdkx.lang.model.element.Element;
import openjdk.source.tree.VariableTree;
import openjdk.source.util.TreePath;
import openjdk.tools.javac.api.JavacTrees;
import openjdk.tools.javac.code.Flags;
import openjdk.tools.javac.code.Symbol;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.tree.JCTree.JCVariableDecl;
import openjdk.tools.javac.tree.TreeInfo;
import openjdk.tools.javac.tree.TreeMaker;
import openjdk.tools.javac.util.Context;
/**
* @author lahvac
*/
public class NBJavacTrees extends JavacTrees {
private final Map<Element, TreePath> element2paths = new HashMap<>();
public static void preRegister(Context context) {
context.put(JavacTrees.class, (Context.Factory<JavacTrees>) NBJavacTrees::new);
}
protected NBJavacTrees(Context context) {
super(context);
}
@Override
public TreePath getPath(Element e) {
TreePath path = super.getPath(e);
return path != null ? path : element2paths.get(e);
}
@Override
public Symbol getElement(TreePath path) {
return TreeInfo.symbolFor((JCTree) path.getLeaf());
}
@Override
protected Copier createCopier(TreeMaker maker) {
return new Copier(maker) {
@Override
public JCTree visitVariable(VariableTree node, JCTree p) {
JCVariableDecl old = (JCVariableDecl) node;
JCVariableDecl nue = (JCVariableDecl) super.visitVariable(node, p);
if (old.sym != null) {
nue.mods.flags |= old.sym.flags_field & Flags.EFFECTIVELY_FINAL;
}
return nue;
}
};
}
void addPathForElement(Element elem, TreePath path) {
element2paths.put(elem, path);
}
}
@@ -0,0 +1,129 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import java.io.PrintWriter;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import jdkx.tools.Diagnostic;
import jdkx.tools.JavaFileObject;
import openjdk.tools.javac.code.Symbol;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.JCDiagnostic;
import openjdk.tools.javac.util.Log;
/**
* @author Tomas Zezula
*/
public class NBLog extends Log {
private static final String ERR_NOT_IN_PROFILE = "not.in.profile"; // NOI18N
private final Map<URI, Collection<Symbol.ClassSymbol>> notInProfiles = new HashMap<>();
private JavaFileObject partialReparseFile;
private final Set<Integer> seenPartialReparsePositions = new HashSet<>();
protected NBLog(final Context context, final PrintWriter output) {
super(context, output);
}
public static NBLog instance(Context context) {
final Log log = Log.instance(context);
if (!(log instanceof NBLog)) {
throw new InternalError("No NBLog instance!"); // NOI18N
}
return (NBLog) log;
}
public static void preRegister(Context context, final PrintWriter output) {
context.put(logKey, (Context.Factory<Log>) c -> new NBLog(c, output));
}
public void startPartialReparse(JavaFileObject inFile) {
partialReparseFile = inFile;
}
public void endPartialReparse(JavaFileObject inFile) {
partialReparseFile = null;
seenPartialReparsePositions.clear(); // TODO: not tested
}
@Override
protected int getDefaultMaxErrors() {
return Integer.MAX_VALUE;
}
@Override
protected int getDefaultMaxWarnings() {
return Integer.MAX_VALUE;
}
@Override
protected boolean shouldReport(JavaFileObject file, int pos) {
if (partialReparseFile != null) {
return file == partialReparseFile && seenPartialReparsePositions.add(pos);
} else {
return super.shouldReport(file, pos);
}
}
@Override
public void report(JCDiagnostic diagnostic) {
// XXX: needs testing!
if (diagnostic.getKind() == Diagnostic.Kind.ERROR
&& ERR_NOT_IN_PROFILE.equals(diagnostic.getCode())) {
final JavaFileObject currentFile = currentSourceFile();
if (currentFile != null) {
final URI uri = currentFile.toUri();
Symbol.ClassSymbol type = (Symbol.ClassSymbol) diagnostic.getArgs()[0];
Collection<Symbol.ClassSymbol> types =
notInProfiles.computeIfAbsent(uri, k -> new ArrayList<>());
types.add(type);
}
}
super.report(diagnostic);
}
Collection<? extends Symbol.ClassSymbol> removeNotInProfile(final URI uri) {
return uri == null ? null : notInProfiles.remove(uri);
}
}
@@ -0,0 +1,98 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.source.util.TreePath;
import openjdk.tools.javac.api.JavacTrees;
import openjdk.tools.javac.comp.MemberEnter;
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit;
import openjdk.tools.javac.tree.JCTree.JCImport;
import openjdk.tools.javac.tree.JCTree.JCMethodDecl;
import openjdk.tools.javac.tree.JCTree.JCVariableDecl;
import openjdk.tools.javac.util.Context;
/**
* @author lahvac
*/
public class NBMemberEnter extends MemberEnter {
private final CancelService cancelService;
private final JavacTrees trees;
private final boolean backgroundScan;
public NBMemberEnter(Context context, boolean backgroundScan) {
super(context);
cancelService = CancelService.instance(context);
trees = JavacTrees.instance(context);
this.backgroundScan = backgroundScan;
}
public static void preRegister(Context context, boolean backgroundScan) {
context.put(
MemberEnter.class,
(Context.Factory<MemberEnter>) c -> new NBMemberEnter(c, backgroundScan));
}
@Override
public void visitTopLevel(JCCompilationUnit tree) {
cancelService.abortIfCanceled();
super.visitTopLevel(tree);
}
@Override
public void visitImport(JCImport tree) {
cancelService.abortIfCanceled();
super.visitImport(tree);
}
@Override
public void visitMethodDef(JCMethodDecl tree) {
cancelService.abortIfCanceled();
super.visitMethodDef(tree);
if (!backgroundScan && trees instanceof NBJavacTrees && !env.enclClass.defs.contains(tree)) {
TreePath path = trees.getPath(env.toplevel, env.enclClass);
if (path != null) {
((NBJavacTrees) trees).addPathForElement(tree.sym, new TreePath(path, tree));
}
}
}
@Override
public void visitVarDef(JCVariableDecl tree) {
cancelService.abortIfCanceled();
super.visitVarDef(tree);
}
}
@@ -0,0 +1,77 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Name;
import openjdk.tools.javac.util.Names;
/**
* @author lahvac
*/
public class NBNames {
public static final Context.Key<NBNames> nbNamesKey = new Context.Key<>();
public final Name _org_netbeans_EnclosingMethod;
public final Name _org_netbeans_TypeSignature;
public final Name _org_netbeans_ParameterNames;
public final Name _org_netbeans_SourceLevelAnnotations;
public final Name _org_netbeans_SourceLevelParameterAnnotations;
public final Name _org_netbeans_SourceLevelTypeAnnotations;
public static void preRegister(Context context) {
context.put(nbNamesKey, (Context.Factory<NBNames>) NBNames::new);
}
protected NBNames(Context context) {
Names n = Names.instance(context);
_org_netbeans_EnclosingMethod = n.fromString("org.netbeans.EnclosingMethod");
_org_netbeans_TypeSignature = n.fromString("org.netbeans.TypeSignature");
_org_netbeans_ParameterNames = n.fromString("org.netbeans.ParameterNames");
_org_netbeans_SourceLevelAnnotations = n.fromString("org.netbeans.SourceLevelAnnotations");
_org_netbeans_SourceLevelParameterAnnotations =
n.fromString("org.netbeans.SourceLevelParameterAnnotations");
_org_netbeans_SourceLevelTypeAnnotations =
n.fromString("org.netbeans.SourceLevelTypeAnnotations");
}
public static NBNames instance(Context context) {
NBNames instance = context.get(nbNamesKey);
if (instance == null) instance = new NBNames(context);
return instance;
}
}
@@ -0,0 +1,252 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.parser.JavacParser;
import openjdk.tools.javac.parser.Lexer;
import openjdk.tools.javac.parser.ParserFactory;
import openjdk.tools.javac.parser.ScannerFactory;
import openjdk.tools.javac.parser.Tokens.Comment;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.tree.JCTree.JCClassDecl;
import openjdk.tools.javac.tree.JCTree.JCEnhancedForLoop;
import openjdk.tools.javac.tree.JCTree.JCExpression;
import openjdk.tools.javac.tree.JCTree.JCModifiers;
import openjdk.tools.javac.tree.JCTree.JCStatement;
import openjdk.tools.javac.tree.JCTree.JCTypeParameter;
import openjdk.tools.javac.tree.TreeInfo;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.List;
import openjdk.tools.javac.util.Name;
import openjdk.tools.javac.util.Names;
import openjdk.tools.javac.util.Options;
import openjdk.tools.javac.util.Position;
/**
* @author lahvac
*/
public class NBParserFactory extends ParserFactory {
public static final String KEEP_COMMENTS_OVERRIDE = "keepCommentsOverride";
public static final String KEEP_COMMENTS_OVERRIDE_KEEP = "keep";
public static final String KEEP_COMMENTS_OVERRIDE_IGNORE = "ignore";
protected final ScannerFactory scannerFactory;
protected final Names names;
protected final CancelService cancelService;
protected final String keepCommentsOverride;
public static void preRegister(Context context) {
context.put(parserFactoryKey, (Context.Factory<ParserFactory>) NBParserFactory::new);
}
protected NBParserFactory(Context context) {
super(context);
this.scannerFactory = ScannerFactory.instance(context);
this.names = Names.instance(context);
this.cancelService = CancelService.instance(context);
this.keepCommentsOverride = Options.instance(context).get(KEEP_COMMENTS_OVERRIDE);
}
@Override
public JavacParser newParser(
CharSequence input,
boolean keepDocComments,
boolean keepEndPos,
boolean keepLineMap,
boolean parseModuleInfo) {
var keepDocCommentsOverride = keepDocComments;
if (this.keepCommentsOverride != null) {
if (KEEP_COMMENTS_OVERRIDE_IGNORE.equals(this.keepCommentsOverride)) {
keepDocCommentsOverride = false;
} else if (KEEP_COMMENTS_OVERRIDE_KEEP.equals(this.keepCommentsOverride)) {
keepDocCommentsOverride = true;
}
}
Lexer lexer = scannerFactory.newScanner(input, keepDocCommentsOverride);
return new NBJavacParser(
this, lexer, keepDocCommentsOverride, keepLineMap, keepEndPos, parseModuleInfo,
cancelService);
}
public static class NBJavacParser extends JavacParser {
private final Names names;
private final CancelService cancelService;
public NBJavacParser(
NBParserFactory fac,
Lexer S,
boolean keepDocComments,
boolean keepLineMap,
boolean keepEndPos,
boolean parseModuleInfo,
CancelService cancelService) {
super(fac, S, keepDocComments, keepLineMap, keepEndPos, parseModuleInfo);
this.names = fac.names;
this.cancelService = cancelService;
}
@Override
protected AbstractEndPosTable newEndPosTable(boolean keepEndPositions) {
AbstractEndPosTable res = super.newEndPosTable(keepEndPositions);
if (keepEndPositions) {
return new EndPosTableImpl(S, this, (SimpleEndPosTable) res);
}
return res;
}
@Override
public int getEndPos(JCTree jctree) {
return TreeInfo.getEndPos(jctree, endPosTable);
}
@Override
public JCStatement parseSimpleStatement() {
JCStatement result = super.parseSimpleStatement();
// workaround: if the code looks like:
// for (name : <collection>) {...}
// the "name" will be made a type of a variable with name "<error>", with
// no end position. Inject the end position for the variable:
if (result instanceof JCEnhancedForLoop) {
JCEnhancedForLoop tree = (JCEnhancedForLoop) result;
if (getEndPos(tree.var) == Position.NOPOS) {
endPosTable.storeEnd(tree.var, getEndPos(tree.var.vartype));
}
}
return result;
}
@Override
protected JCClassDecl classDeclaration(JCModifiers mods, Comment dc) {
if (cancelService != null) {
cancelService.abortIfCanceled();
}
return super.classDeclaration(mods, dc);
}
@Override
protected JCClassDecl interfaceDeclaration(JCModifiers mods, Comment dc) {
if (cancelService != null) {
cancelService.abortIfCanceled();
}
return super.interfaceDeclaration(mods, dc);
}
@Override
protected JCClassDecl enumDeclaration(JCModifiers mods, Comment dc) {
if (cancelService != null) {
cancelService.abortIfCanceled();
}
return super.enumDeclaration(mods, dc);
}
@Override
protected JCTree methodDeclaratorRest(
int pos,
JCModifiers mods,
JCExpression type,
Name name,
List<JCTypeParameter> typarams,
boolean isInterface,
boolean isVoid,
boolean isRecord,
Comment dc) {
if (cancelService != null) {
cancelService.abortIfCanceled();
}
return super.methodDeclaratorRest(
pos, mods, type, name, typarams, isInterface, isVoid, isRecord, dc);
}
public final class EndPosTableImpl extends AbstractEndPosTable {
private final Lexer lexer;
private final SimpleEndPosTable delegate;
private EndPosTableImpl(Lexer lexer, JavacParser parser, SimpleEndPosTable delegate) {
super(parser);
this.lexer = lexer;
this.delegate = delegate;
}
public void resetErrorEndPos() {
delegate.errorEndPos = Position.NOPOS;
errorEndPos = delegate.errorEndPos;
}
@Override
public int getEndPos(JCTree jctree) {
return delegate.getEndPos(jctree);
}
@Override
public void storeEnd(JCTree tree, int endpos) {
if (endpos >= 0) {
delegate.storeEnd(tree, endpos);
}
}
@Override
public int replaceTree(JCTree jctree, JCTree jctree1) {
return delegate.replaceTree(jctree, jctree1);
}
@Override
protected <T extends JCTree> T to(T t) {
storeEnd(t, parser.token().endPos);
return t;
}
@Override
protected <T extends JCTree> T toP(T t) {
storeEnd(t, lexer.prevToken().endPos);
return t;
}
@Override
public void setErrorEndPos(int errPos) {
delegate.setErrorEndPos(errPos);
errorEndPos = delegate.errorEndPos;
}
}
}
}
@@ -0,0 +1,89 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.code.Symbol;
import openjdk.tools.javac.code.Symbol.TypeSymbol;
import openjdk.tools.javac.code.Type;
import openjdk.tools.javac.comp.AttrContext;
import openjdk.tools.javac.comp.Env;
import openjdk.tools.javac.comp.Resolve;
import openjdk.tools.javac.util.Context;
/**
* @author lahvac
*/
public class NBResolve extends Resolve {
private boolean accessibleOverride;
public static NBResolve instance(Context context) {
Resolve instance = context.get(resolveKey);
if (instance == null) instance = new NBResolve(context);
return (NBResolve) instance;
}
public static void preRegister(Context context) {
context.put(resolveKey, (Context.Factory<Resolve>) NBResolve::new);
}
protected NBResolve(Context ctx) {
super(ctx);
}
public static boolean isStatic(Env<AttrContext> env) {
return Resolve.isStatic(env);
}
public void disableAccessibilityChecks() {
accessibleOverride = true;
}
public void restoreAccessbilityChecks() {
accessibleOverride = false;
}
@Override
public boolean isAccessible(Env<AttrContext> env, TypeSymbol c, boolean checkInner) {
if (accessibleOverride) return true;
return super.isAccessible(env, c, checkInner);
}
@Override
public boolean isAccessible(Env<AttrContext> env, Type site, Symbol sym, boolean checkInner) {
if (accessibleOverride) return true;
return super.isAccessible(env, site, sym, checkInner);
}
}
@@ -0,0 +1,76 @@
/*
* 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/>.
*/
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.itsaky.androidide.javac.services;
import openjdk.tools.javac.code.Symtab;
import openjdk.tools.javac.code.Types;
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit;
import openjdk.tools.javac.tree.TreeMaker;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Names;
/**
* @author lahvac
*/
public class NBTreeMaker extends TreeMaker {
private final Names names;
private final Types types;
private final Symtab syms;
protected NBTreeMaker(JCCompilationUnit toplevel, Names names, Types types, Symtab syms) {
super(toplevel, names, types, syms);
this.names = names;
this.types = types;
this.syms = syms;
}
public static void preRegister(Context context) {
context.put(treeMakerKey, (Context.Factory<TreeMaker>) NBTreeMaker::new);
}
protected NBTreeMaker(Context context) {
super(context);
this.names = Names.instance(context);
this.types = Types.instance(context);
this.syms = Symtab.instance(context);
}
@Override
public TreeMaker forToplevel(JCCompilationUnit toplevel) {
return new NBTreeMaker(toplevel, names, types, syms);
}
}
@@ -0,0 +1,43 @@
/*
* 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.javac.services.compiler
import com.itsaky.androidide.javac.services.CancelService
import com.itsaky.androidide.utils.ILogger
import java.util.concurrent.atomic.AtomicBoolean
/**
* Cancel service implementation for the java compiler.
* @author Akash Yadav
*/
class CancelServiceImpl : CancelService() {
val cancelled = AtomicBoolean(false)
/**
* Sets the cancellation flag.
*
* @return `true` if compilation process was running and it was set to be cancelled, `false`
* otherwise.
*/
fun cancel(): Boolean {
ILogger.ROOT.info("...requesting compilation cancellation")
return !cancelled.getAndSet(true)
}
override fun isCanceled(): Boolean = cancelled.get()
}
@@ -0,0 +1,36 @@
/*
* 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.javac.services.compiler;
import androidx.annotation.NonNull;
import jdkx.tools.JavaFileObject;
import openjdk.tools.javac.util.Context;
/**
* @author Akash Yadav
*/
public interface JavacFlowListener {
Context.Key<JavacFlowListener> flowListenerKey = new Context.Key<>();
static JavacFlowListener instance(@NonNull Context context) {
return context.get(flowListenerKey);
}
boolean hasFlowCompleted(final JavaFileObject jfo);
}
@@ -0,0 +1,41 @@
/*
* 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.javac.services.compiler
import openjdk.tools.javac.api.JavacTaskImpl
/** @author Akash Yadav */
class ReusableBorrow
internal constructor(
private val reusableCompiler: ReusableCompiler,
@JvmField val task: JavacTaskImpl
) : AutoCloseable {
private var closed = false
override fun close() {
if (closed) {
return
}
// not returning the context to the pool if task crashes with an exception
// the task/context may be in a broken state
reusableCompiler.currentContext!!.clear()
task.cleanup()
reusableCompiler.checkedOut = false
closed = true
}
}
@@ -0,0 +1,124 @@
/*
* 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/>.
*/
// Forked from JavacTaskImpl
package com.itsaky.androidide.javac.services.compiler
import com.itsaky.androidide.javac.services.CancelService
import jdkx.tools.DiagnosticListener
import jdkx.tools.JavaFileManager
import jdkx.tools.JavaFileObject
import openjdk.tools.javac.api.JavacTaskImpl
import openjdk.tools.javac.api.JavacTool
/**
* A pool of reusable JavacTasks. When a task is no valid anymore, it is returned to the pool, and
* its Context may be reused for future processing in some cases. The reuse is achieved by replacing
* some components (most notably JavaCompiler and Log) with reusable counterparts, and by cleaning
* up leftovers from previous compilation.
*
* For each combination of options, a separate task/context is created and kept, as most option
* values are cached inside components themselves.
*
* When the compilation redefines sensitive classes (e.g. classes in the the java.* packages), the
* task/context is not reused.
*
* When the task is reused, then packages that were already listed won't be listed again.
*
* Care must be taken to only return tasks that won't be used by the original caller.
*
* Care must also be taken when custom components are installed, as those are not cleaned when the
* task/context is reused, and subsequent getTask may return a task based on a context with these
* custom components.
*
* **This is NOT part of any supported API. If you write code that depends on this, you do so at
* your own risk. This code and its internal interfaces are subject to change or deletion without
* notice.**
*/
open class ReusableCompiler {
private val systemProvider = JavacTool.create()
private val currentOptions = mutableListOf<String>()
@JvmField var currentContext: ReusableContext? = null
internal var checkedOut = false
/**
* Creates a new task as if by [jdkx.tools.JavaCompiler.getTask] and runs the provided worker with
* it. The task is only valid while the worker is running. The internal structures may be reused
* from some previous compilation.
*
* @param fileManager a file manager; if `null` use the compiler's standard filemanager
* @param diagnosticListener a diagnostic listener; if `null` use the compiler's default method
* for reporting diagnostics
* @param options compiler options, `null` means no options
* @param classes names of classes to be processed by annotation processing, `null` means no class
* names
* @param compilationUnits the compilation units to compile, `null` means no compilation units
* @return an object representing the compilation
* @throws RuntimeException if an unrecoverable error occurred in a user supplied component. The
* [cause][Throwable.cause] will be the error in user code.
* @throws IllegalArgumentException if any of the options are invalid, or if any of the given
* compilation units are of other kind than [source][JavaFileObject.Kind.SOURCE]
*/
open fun getTask(
fileManager: JavaFileManager?,
diagnosticListener: DiagnosticListener<in JavaFileObject?>?,
options: Iterable<String>,
classes: Iterable<String>,
compilationUnits: Iterable<JavaFileObject?>?
): ReusableBorrow {
if (checkedOut) {
throw RuntimeException("Compiler is already in-use!")
}
checkedOut = true
val opts = options.toList()
if (opts != currentOptions) {
currentOptions.clear()
currentOptions.addAll(opts)
currentContext = onCreateContext()
}
val task =
systemProvider.getTask(
null,
fileManager,
diagnosticListener,
opts,
classes,
compilationUnits,
currentContext
) as JavacTaskImpl
task.addTaskListener(currentContext)
return onCreateBorrow(task)
}
protected open fun onCreateContext(): ReusableContext {
return ReusableContext(cancelService)
}
protected open fun onCreateBorrow(task: JavacTaskImpl): ReusableBorrow {
return ReusableBorrow(this, task)
}
companion object {
@JvmStatic
private val cancelService: CancelService = CancelServiceImpl()
}
}
@@ -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.androidide.javac.services.compiler
import com.itsaky.androidide.javac.services.CancelService
import com.itsaky.androidide.javac.services.NBAttr
import com.itsaky.androidide.javac.services.NBClassFinder
import com.itsaky.androidide.javac.services.NBClassReader
import com.itsaky.androidide.javac.services.NBEnter
import com.itsaky.androidide.javac.services.NBJavacTrees
import com.itsaky.androidide.javac.services.NBMemberEnter
import com.itsaky.androidide.javac.services.NBParserFactory
import com.itsaky.androidide.javac.services.NBResolve
import com.itsaky.androidide.javac.services.NBTreeMaker
import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton
import com.itsaky.androidide.javac.services.fs.JarPackageProviderImpl
import com.itsaky.androidide.utils.VMUtils
import com.itsaky.androidide.zipfs2.JarPackageProvider
import jdkx.tools.DiagnosticListener
import jdkx.tools.JavaFileManager
import jdkx.tools.JavaFileObject
import openjdk.source.util.JavacTask
import openjdk.source.util.TaskEvent
import openjdk.source.util.TaskEvent.Kind.ANALYZE
import openjdk.source.util.TaskListener
import openjdk.tools.javac.api.JavacTrees
import openjdk.tools.javac.api.MultiTaskListener
import openjdk.tools.javac.code.Types
import openjdk.tools.javac.comp.Annotate
import openjdk.tools.javac.comp.Check
import openjdk.tools.javac.comp.CompileStates
import openjdk.tools.javac.comp.Enter
import openjdk.tools.javac.comp.Modules
import openjdk.tools.javac.file.CacheFSInfo
import openjdk.tools.javac.file.FSInfo
import openjdk.tools.javac.main.Arguments
import openjdk.tools.javac.main.JavaCompiler
import openjdk.tools.javac.model.JavacElements
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit
import openjdk.tools.javac.util.Context
import openjdk.tools.javac.util.DefinedBy
import openjdk.tools.javac.util.DefinedBy.Api.COMPILER_TREE
import openjdk.tools.javac.util.Log
import java.net.URI
/**
* Reusable [Context] for [ReusableCompiler].
*
* @author Akash Yadav
*/
class ReusableContext(cancelService: CancelService) : Context(), TaskListener {
private val flowCompleted = mutableSetOf<URI>()
init {
put(Log.logKey, ReusableLog.factory)
put(FSInfo::class.java, if (VMUtils.isJvm()) CacheFSInfo() else CacheFSInfoSingleton)
put(JavaCompiler.compilerKey, ReusableJavaCompiler.factory)
put(JavacFlowListener.flowListenerKey, JavacFlowListener { this.hasFlowCompleted(it) })
put(JarPackageProvider::class.java, JarPackageProviderImpl)
NBAttr.preRegister(this)
NBParserFactory.preRegister(this)
NBTreeMaker.preRegister(this)
NBJavacTrees.preRegister(this)
NBResolve.preRegister(this)
NBEnter.preRegister(this)
NBMemberEnter.preRegister(this, false)
NBClassFinder.preRegister(this)
NBClassReader.preRegister(this)
CancelService.preRegister(this, cancelService)
}
@DefinedBy(COMPILER_TREE)
override fun started(e: TaskEvent) {
// log.debug("Started: $e")
// Do nothing
}
@DefinedBy(COMPILER_TREE)
override fun finished(e: TaskEvent) {
if (e.kind == ANALYZE) {
val cu = e.compilationUnit as JCCompilationUnit
if (cu.sourcefile != null) {
flowCompleted.add(cu.sourcefile.toUri())
}
}
}
fun clear() {
drop(Arguments.argsKey)
drop(DiagnosticListener::class.java)
drop(Log.outKey)
drop(Log.errKey)
drop(JavaFileManager::class.java)
drop(JavacTask::class.java)
drop(JavacTrees::class.java)
drop(JavacElements::class.java)
if (ht[Log.logKey] is ReusableLog) {
// log already init-ed - not first round
(Log.instance(this) as ReusableLog).clear()
Enter.instance(this).newRound()
(JavaCompiler.instance(this) as ReusableJavaCompiler).clear()
Types.instance(this).newRound()
Check.instance(this).newRound()
Modules.instance(this).newRound()
Annotate.instance(this).newRound()
CompileStates.instance(this).clear()
MultiTaskListener.instance(this).clear()
}
}
/** **FOR INTERNAL USE ONLY!** */
fun <T> drop(k: Key<T>?) {
ht.remove(k)
}
/** **FOR INTERNAL USE ONLY!** */
fun <T> drop(c: Class<T>?) {
drop(key(c))
}
private fun hasFlowCompleted(fo: JavaFileObject?): Boolean {
return if (fo == null) {
false
} else {
try {
this.flowCompleted.contains(fo.toUri())
} catch (e: Exception) {
false
}
}
}
}
@@ -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.androidide.javac.services.compiler
import com.itsaky.androidide.javac.services.NBJavaCompiler
import openjdk.tools.javac.main.JavaCompiler
import openjdk.tools.javac.util.Context
/**
* Reusable JavaCompiler; exposes a method to clean up the component from leftovers associated
* with previous compilations.
*/
open class ReusableJavaCompiler(context: Context?) : NBJavaCompiler(context) {
companion object {
val factory = Context.Factory<JavaCompiler> { ReusableJavaCompiler(it) }
}
override fun checkReusable() {
// Do nothing
}
override fun close() {
// Do nothing
}
fun clear() {
newRound()
}
}
@@ -0,0 +1,64 @@
/*
* 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.javac.services.compiler
import com.itsaky.androidide.javac.services.NBLog
import jdkx.tools.Diagnostic
import jdkx.tools.DiagnosticListener
import jdkx.tools.JavaFileObject
import openjdk.tools.javac.util.Context
import openjdk.tools.javac.util.DefinedBy
import openjdk.tools.javac.util.DefinedBy.Api.COMPILER
import openjdk.tools.javac.util.Log
import java.io.PrintWriter
/**
* Reusable Log; exposes a method to clean up the component from leftovers associated with
* previous compilations.
*/
internal class ReusableLog(var context: Context) : NBLog(context, PrintWriter(System.err)) {
fun clear() {
recorded.clear()
sourceMap.clear()
nerrors = 0
nwarnings = 0
// Set a fake listener that will lazily lookup the context for the 'real' listener.
// Since
// this field is never updated when a new task is created, we cannot simply reset
// the field
// or keep old value. This is a hack to workaround the limitations in the current
// infrastructure.
diagListener = object : DiagnosticListener<JavaFileObject?> {
var cachedListener: DiagnosticListener<JavaFileObject>? = null
@Suppress("UNCHECKED_CAST")
@DefinedBy(COMPILER)
override fun report(diagnostic: Diagnostic<out JavaFileObject>) {
if (cachedListener == null) {
cachedListener = context.get(DiagnosticListener::class.java) as DiagnosticListener<JavaFileObject>?
}
cachedListener!!.report(diagnostic)
}
}
}
companion object {
val factory = Context.Factory<Log> { context: Context -> ReusableLog(context) }
}
}
@@ -0,0 +1,37 @@
/*
* 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.javac.services.fs
import com.itsaky.androidide.zipfs2.AndroidFsProvider
import java.nio.file.spi.FileSystemProvider
/** @author Akash Yadav */
object AndroidFsProviderImpl : AndroidFsProvider() {
init {
INSTANCE = this
}
fun init() {
// Used to instantiate this class and override the AndroidFsProvider instance
}
override fun zipFsProvider(): FileSystemProvider {
return CachingJarFileSystemProvider
}
}
@@ -0,0 +1,62 @@
/*
* 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.javac.services.fs
import com.itsaky.androidide.utils.VMUtils
import openjdk.tools.javac.file.CacheFSInfo
import org.slf4j.LoggerFactory
import java.nio.file.Path
/**
* Singleton class for [CacheFSInfo] to avoid reading attributes of same file multiple times.
*
* @author Akash Yadav
*/
object CacheFSInfoSingleton : CacheFSInfo() {
const val TEST_PROP_ENABLED_ON_JVM = "ide.testing.javac.fsCache.isEnabledOnJVM"
private val log = LoggerFactory.getLogger(CacheFSInfoSingleton::class.java)
/**
* Caches information about the given [Path].
*/
@JvmOverloads
fun cache(file: Path, cacheJarClasspath: Boolean = true) {
if (System.getProperty(TEST_PROP_ENABLED_ON_JVM, null) != "true") {
if (VMUtils.isJvm()) {
return
}
}
try {
// Cache canonical path
getCanonicalFile(file)
// Cache attributes
getAttributes(file)
// Cache jar classpath if requested
if (cacheJarClasspath) {
getJarClassPath(file)
}
} catch (err: Throwable) {
log.warn("Failed to cache jar file: {}", file, err)
}
}
}
@@ -0,0 +1,70 @@
/*
* 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.javac.services.fs
import com.itsaky.androidide.zipfs2.ZipFileSystem
import com.itsaky.androidide.zipfs2.ZipFileSystemProvider
import jdkx.lang.model.SourceVersion
import openjdk.tools.javac.file.RelativePath.RelativeDirectory
import java.io.IOException
import java.nio.file.Path
/**
* A cached file system for JAR files.
*
* @author Akash Yadav
*/
class CachedJarFileSystem(
provider: ZipFileSystemProvider?,
zfpath: Path?,
env: MutableMap<String, *>?
) : ZipFileSystem(provider, zfpath, env) {
internal val packages = mutableMapOf<RelativeDirectory, Path>()
override fun close() {
// Do nothing
// This is called manually by the Java LSP
}
@Throws(IOException::class)
fun doClose() {
super.close()
}
fun storeJARPackageDir(dir: Path?): Boolean {
if (isValid(dir?.fileName)) {
packages[RelativeDirectory(rootDir.relativize(dir!!).toString())] = dir
return true
}
return false
}
private fun isValid(fileName: Path?): Boolean {
return if (fileName == null) {
true
} else {
var name = fileName.toString()
if (name.endsWith("/")) {
name = name.substring(0, name.length - 1)
}
SourceVersion.isIdentifier(name)
}
}
}
@@ -0,0 +1,99 @@
/*
* 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.javac.services.fs
import com.itsaky.androidide.zipfs2.JarFileSystemProvider
import com.itsaky.androidide.zipfs2.ZipFileSystem
import org.slf4j.LoggerFactory
import java.nio.file.FileSystem
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.ConcurrentHashMap
import kotlin.io.path.pathString
/**
* An implementation of [JarFileSystemProvider] that caches the created [CachedJarFileSystem] so
* that it can be (re)used in multiple compilations.
*
* @author Akash Yadav
*/
object CachingJarFileSystemProvider : JarFileSystemProvider() {
private val cachedFs = ConcurrentHashMap<String, CachedJarFileSystem>()
private val log = LoggerFactory.getLogger(CachingJarFileSystemProvider::class.java)
override fun createFs(path: Path, env: MutableMap<String, *>?): ZipFileSystem {
val cached = cachedFs[path.normalize().pathString]
if (cached != null) {
return cached
}
return createAndCache(path, env)
}
fun newFileSystem(path: Path): FileSystem? {
return newFileSystem(path, mutableMapOf<String, Any>())
}
fun clearCache() {
cachedFs.values.forEach(this::closeFs)
cachedFs.clear()
}
fun clearCaches(predicate: (Path) -> Boolean) {
return clearCachesForPaths { predicate(Paths.get(it)) }
}
fun clearCachesForPaths(predicate: (String) -> Boolean) {
val toRemove =
this.cachedFs.keys.mapNotNull {
return@mapNotNull if (predicate(it)) {
it
} else null
}
if (toRemove.isNotEmpty()) {
toRemove.forEach(this::clearCache)
}
}
fun clearCache(path: Path) {
clearCache(path.normalize().pathString)
}
fun clearCache(path: String) {
val fs = cachedFs.remove(path)
if (fs != null) {
log.debug("Clearing cached JAR file system for path: {}", path)
closeFs(fs)
}
}
private fun closeFs(fs: CachedJarFileSystem) {
try {
fs.doClose()
} catch (err: Throwable) {
log.error("Failed to close cached zip file system: {}", fs, err)
}
}
private fun createAndCache(path: Path, env: MutableMap<String, *>?): CachedJarFileSystem {
val fs = CachedJarFileSystem(this, path, env)
cachedFs[path.normalize().pathString] = fs
return fs
}
}
@@ -0,0 +1,34 @@
/*
* 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.javac.services.fs
import com.itsaky.androidide.zipfs2.JarPackageProvider
import openjdk.tools.javac.file.RelativePath.RelativeDirectory
import java.nio.file.Path
/**
* Provides cached packages from arhives.
*
* @author Akash Yadav
*/
object JarPackageProviderImpl : JarPackageProvider {
override fun getPackages(archivePath: Path): MutableMap<RelativeDirectory, Path> {
val fs = CachingJarFileSystemProvider.newFileSystem(archivePath) as CachedJarFileSystem
return fs.packages
}
}
@@ -0,0 +1,37 @@
/*
* 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.javac.services.partial
import jdkx.tools.DiagnosticListener
import jdkx.tools.JavaFileObject
import openjdk.source.tree.CompilationUnitTree
import openjdk.tools.javac.api.JavacTaskImpl
import openjdk.tools.javac.api.JavacTrees
/**
* Information about a compilation.
*
* @author Akash Yadav
*/
data class CompilationInfo(
@JvmField val task: JavacTaskImpl,
@JvmField val diagnosticListener: DiagnosticListener<JavaFileObject>,
@JvmField val cu: CompilationUnitTree,
) {
val trees: JavacTrees = JavacTrees.instance(task)
}
@@ -0,0 +1,311 @@
/*
* 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.javac.services.partial;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import jdkx.tools.Diagnostic;
import jdkx.tools.DiagnosticListener;
import jdkx.tools.JavaFileObject;
import openjdk.tools.javac.api.ClientCodeWrapper;
import openjdk.tools.javac.util.JCDiagnostic;
/**
* @author Akash Yadav
*/
public class DiagnosticListenerImpl implements DiagnosticListener<JavaFileObject> {
private final Map<JavaFileObject, Diagnostics> source2Errors;
private final JavaFileObject jfo;
private volatile List<Diagnostic<? extends JavaFileObject>> partialReparseErrors;
/** true if the partialReparseErrors contain some non-warning */
private volatile boolean partialReparseRealErrors;
private volatile List<Diagnostic<? extends JavaFileObject>> affectedErrors;
private volatile int currentDelta;
public DiagnosticListenerImpl(@Nullable final JavaFileObject jfo) {
this.jfo = jfo;
this.source2Errors = new HashMap<>();
}
@Override
public void report(Diagnostic<? extends JavaFileObject> message) {
if (partialReparseErrors != null) {
if (this.jfo != null && this.jfo == message.getSource()) {
partialReparseErrors.add(message);
if (message.getKind() == Diagnostic.Kind.ERROR) {
partialReparseRealErrors = true;
}
}
} else {
Diagnostics errors = getErrors(message.getSource());
errors.add((int) message.getPosition(), message);
}
}
private Diagnostics getErrors(JavaFileObject file) {
Diagnostics errors;
if (isIncompleteClassPath()) {
// if (root != null && JavaIndex.hasSourceCache(root.toURL(), false)) {
// errors = source2Errors.get(file);
// if (errors == null) {
// source2Errors.put(file, errors = new Diagnostics());
// if (this.jfo != null && this.jfo == file) {
// errors.add(0, new IncompleteClassPath(this.jfo));
// }
// }
// } else {
errors = new Diagnostics();
if (this.jfo != null && this.jfo == file) {
errors.add(0, new IncompleteClassPath(this.jfo));
// }
}
} else {
errors = source2Errors.get(file);
if (errors == null) {
source2Errors.put(file, errors = new Diagnostics());
}
}
return errors;
}
private boolean isIncompleteClassPath() {
return false;
}
public final boolean hasPartialReparseErrors() {
return this.partialReparseErrors != null && partialReparseRealErrors;
}
public final void startPartialReparse(int from, int to) {
if (partialReparseErrors == null) {
partialReparseErrors = new ArrayList<>();
Diagnostics errors = getErrors(jfo);
SortedMap<Integer, Collection<DiagNode>> subMap = errors.subMap(from, to);
subMap.values().forEach(value -> value.forEach(errors::unlink));
subMap.clear(); // Remove errors in changed method durring the partial reparse
Map<Integer, Collection<DiagNode>> tail = errors.tailMap(to);
this.affectedErrors = new ArrayList<>(tail.size());
HashSet<DiagNode> tailNodes = new HashSet<>();
for (Iterator<Map.Entry<Integer, Collection<DiagNode>>> it = tail.entrySet().iterator();
it.hasNext(); ) {
tailNodes.addAll(it.next().getValue());
it.remove();
}
DiagNode node = errors.first;
while (node != null) {
if (tailNodes.contains(node)) {
errors.unlink(node);
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(
"diagnostic == null " + mapArraysToLists(Thread.getAllStackTraces())); // NOI18N
}
this.affectedErrors.add(new D(diagnostic));
}
node = node.next;
}
} else {
this.partialReparseErrors.clear();
}
partialReparseRealErrors = false;
}
private static <A, B> Map<A, List<B>> mapArraysToLists(final Map<? extends A, B[]> map) {
final Map<A, List<B>> result = new HashMap<>();
for (Map.Entry<? extends A, B[]> entry : map.entrySet()) {
result.put(entry.getKey(), Arrays.asList(entry.getValue()));
}
return result;
}
public final void endPartialReparse(final int delta) {
this.currentDelta += delta;
}
private static final class D implements Diagnostic {
private final JCDiagnostic delegate;
public D(final JCDiagnostic delegate) {
assert delegate != null;
this.delegate = delegate;
}
@Override
public Kind getKind() {
return this.delegate.getKind();
}
@Override
public Object getSource() {
return this.delegate.getSource();
}
@Override
public long getPosition() {
return this.delegate.getPosition();
}
@Override
public long getStartPosition() {
return this.delegate.getStartPosition();
}
@Override
public long getEndPosition() {
return this.delegate.getEndPosition();
}
@Override
public long getLineNumber() {
return -1;
}
@Override
public long getColumnNumber() {
return -1;
}
@Override
public String getCode() {
return this.delegate.getCode();
}
@Override
public String getMessage(Locale locale) {
return this.delegate.getMessage(locale);
}
}
private static final class IncompleteClassPath implements Diagnostic<JavaFileObject> {
private final JavaFileObject file;
IncompleteClassPath(final JavaFileObject file) {
this.file = file;
}
@Override
public Kind getKind() {
return Kind.WARNING;
}
@Override
public JavaFileObject getSource() {
return file;
}
@Override
public long getPosition() {
return 0;
}
@Override
public long getStartPosition() {
return getPosition();
}
@Override
public long getEndPosition() {
return getPosition();
}
@Override
public long getLineNumber() {
return getPosition();
}
@Override
public long getColumnNumber() {
return getPosition();
}
@Override
public String getCode() {
return "nb.classpath.incomplete"; // NOI18N
}
@Override
public String getMessage(Locale locale) {
return "Incomplete classpath";
}
}
private static final class Diagnostics extends TreeMap<Integer, Collection<DiagNode>> {
private DiagNode first;
private DiagNode last;
public void add(int pos, Diagnostic<? extends JavaFileObject> diag) {
Collection<DiagNode> nodes = get((int) diag.getPosition());
if (nodes == null) {
put((int) diag.getPosition(), nodes = new ArrayList<>());
}
DiagNode node = new DiagNode(last, diag, null);
nodes.add(node);
if (last != null) {
last.next = node;
}
last = node;
if (first == null) {
first = node;
}
}
private void unlink(DiagNode node) {
if (node.next == null) {
last = node.prev;
} else {
node.next.prev = node.prev;
}
if (node.prev == null) {
first = node.next;
} else {
node.prev.next = node.next;
}
}
}
private static final class DiagNode {
private final Diagnostic<? extends JavaFileObject> diag;
private DiagNode next;
private DiagNode prev;
private DiagNode(DiagNode prev, Diagnostic<? extends JavaFileObject> diag, DiagNode next) {
this.diag = diag;
this.next = next;
this.prev = prev;
}
}
}
@@ -0,0 +1,29 @@
/*
* 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.javac.services.partial
import openjdk.source.util.TreePath
/**
* A parser reparser parses part of code.
*
* @author Akash Yadav
*/
interface PartialReparser {
fun reparseMethod(ci: CompilationInfo, methodPath: TreePath, newBody: String, fileContents: CharSequence): Boolean
}
@@ -0,0 +1,359 @@
/*
* 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.javac.services.partial
import com.itsaky.androidide.javac.services.compiler.JavacFlowListener
import com.itsaky.androidide.javac.services.util.ReparserUtils
import com.itsaky.androidide.javac.services.visitors.FindAnonymousVisitor
import com.itsaky.androidide.javac.services.visitors.TranslateMethodPositionsVisitor
import openjdk.source.tree.BlockTree
import openjdk.source.tree.ClassTree
import openjdk.source.tree.CompilationUnitTree
import openjdk.source.tree.Tree.Kind.BLOCK
import openjdk.source.tree.Tree.Kind.METHOD
import openjdk.source.util.TreePath
import openjdk.tools.javac.api.JavacScope
import openjdk.tools.javac.api.JavacTrees
import openjdk.tools.javac.code.Flags
import openjdk.tools.javac.code.Symbol
import openjdk.tools.javac.code.Symtab
import openjdk.tools.javac.comp.Attr
import openjdk.tools.javac.comp.AttrContext
import openjdk.tools.javac.comp.Enter
import openjdk.tools.javac.comp.Env
import openjdk.tools.javac.comp.Flow
import openjdk.tools.javac.parser.JavacParser
import openjdk.tools.javac.parser.LazyDocCommentTable.Entry
import openjdk.tools.javac.parser.ScannerFactory
import openjdk.tools.javac.tree.DocCommentTable
import openjdk.tools.javac.tree.EndPosTable
import openjdk.tools.javac.tree.JCTree
import openjdk.tools.javac.tree.JCTree.JCBlock
import openjdk.tools.javac.tree.JCTree.JCClassDecl
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit
import openjdk.tools.javac.tree.JCTree.JCMethodDecl
import openjdk.tools.javac.tree.TreeInfo
import openjdk.tools.javac.tree.TreeMaker
import openjdk.tools.javac.util.Context
import openjdk.tools.javac.util.List
import openjdk.tools.javac.util.Log
import openjdk.tools.javac.util.Names
import org.slf4j.LoggerFactory
import java.nio.CharBuffer
import java.util.Arrays
/**
* Partial reparser implementation.
*
* @author Akash Yadav
*/
class PartialReparserImpl : PartialReparser {
private var allowPartialReparse: Boolean = ReparserUtils.canReparse()
companion object {
private val log = LoggerFactory.getLogger(PartialReparserImpl::class.java)
}
override fun reparseMethod(
ci: CompilationInfo,
methodPath: TreePath,
newBody: String,
fileContents: CharSequence
): Boolean {
if (!allowPartialReparse) {
log.debug("Partial reparse is disabled")
return false
}
val cu = ci.cu
val fo = cu.sourceFile
val task = ci.task
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, method.body))
val jt = JavacTrees.instance(task)
val origStartPos = jt.sourcePositions.getStartPosition(cu, method.body).toInt()
val origEndPos = jt.sourcePositions.getEndPosition(cu, method.body).toInt()
if (origStartPos < 0) {
log.warn("Javac returned start position {} < 0", origStartPos)
return false
}
if (origStartPos > origEndPos) {
log.warn("Java returned start position: {} > end position: {}", origStartPos, origEndPos)
return false
}
val fav = FindAnonymousVisitor()
fav.scan(method.body, null)
if (fav.hasLocalClass) {
log.debug("Skipped reparse method. Old local classes: {}", fo)
return false
}
val noInner = fav.noInner
val context = task.context
try {
val l = com.itsaky.androidide.javac.services.NBLog.instance(context)
l.startPartialReparse(fo)
val prevLogged = l.useSource(fo)
val block: JCBlock?
try {
val dl = ci.diagnosticListener as DiagnosticListenerImpl
dl.startPartialReparse(origStartPos, origEndPos)
val docComments = HashMap<JCTree, Entry>()
log.debug("Reparse method...")
block = reparseMethodBody(context, cu, method, newBody, docComments)
val endPosTable = (cu as JCCompilationUnit).endPositions
if (block == null) {
log.debug("Skipped reparse method. Invalid position, newBody: {}", newBody)
return false
}
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)
val newNoInner = fav.noInner
if (fav.hasLocalClass || noInner != newNoInner) {
log.debug("Skipped method reparse (new local class): {}", fo)
return false
}
val docCommentsTable = getLazyDocCommentsTable(cu)
docCommentsTable.keys.removeAll(fav.docOwners)
docCommentsTable.putAll(docComments)
val delta = newEndPos - origEndPos
val tpv = TranslateMethodPositionsVisitor(method, endPosTable, delta)
tpv.scan(cu, null)
doUnenter(context, cu, method)
method.body = block
log.debug("ReAttr method...")
reAttrMethodBody(context, methodScope, method, block)
if (!dl.hasPartialReparseErrors()) {
val fl = JavacFlowListener.instance(context)
if (fl != null && fl.hasFlowCompleted(cu.sourceFile)) {
log.debug("Reflow method...")
val tp = TreePath.getPath(cu, method)
val t = tp.parentPath.leaf as ClassTree
reflowMethodBody(context, t, method)
}
}
val arr = CharArray(fileContents.length)
for (index in fileContents.indices) {
arr[index] = fileContents[index]
}
buildLineMap(cu, arr)
dl.endPartialReparse(delta)
} finally {
l.endPartialReparse(cu.sourceFile)
l.useSource(prevLogged)
}
} catch (err: Throwable) {
log.error("An error occurred while reparsing method", err)
return false
}
return true
}
private fun buildLineMap(cu: JCCompilationUnit, arr: CharArray) {
ReparserUtils.buildLineMap(cu.getLineMap(), arr, arr.size)
}
private fun getLazyDocCommentsTable(cu: JCCompilationUnit): MutableMap<JCTree, Entry> {
return getLazyDocCommentsTable(cu.docComments)
}
private fun getLazyDocCommentsTable(docs: DocCommentTable): MutableMap<JCTree, Entry> {
return ReparserUtils.getLazyDocCommentsTable(docs)!!
}
private fun doUnenter(context: Context?, cu: CompilationUnitTree, method: JCMethodDecl) {
if (cu !is JCCompilationUnit) {
throw IllegalStateException()
}
ReparserUtils.unenter(context, cu, method.body)
}
private fun reparseMethodBody(
context: Context,
cu: CompilationUnitTree,
method: JCMethodDecl,
newBody: String,
docComments: MutableMap<JCTree, Entry>,
): JCBlock? {
val startPos = method.body.pos
val body = CharArray(startPos + newBody.length + 1)
Arrays.fill(body, ' ')
for (index in newBody.indices) {
body[startPos + index] = newBody[index]
}
body[startPos + newBody.length] = '\u0000'
val buf = CharBuffer.wrap(body, 0, body.size - 1)
val parser = newParser(context, buf, (cu as JCCompilationUnit).endPositions)
val statement = parser.parseStatement()
if (statement.kind == BLOCK) {
docComments.putAll(getLazyDocCommentsTable(getParserDocComments(parser)))
return statement as JCBlock
}
log.warn("JavacParser parsed invalid statment. Block statement was expected...")
return null
}
private fun getParserDocComments(parser: JavacParser): DocCommentTable {
return ReparserUtils.getDocComments(parser)!!
}
private fun newParser(
context: Context,
buf: CharBuffer?,
endPositions: EndPosTable?,
): JavacParser {
val factory =
com.itsaky.androidide.javac.services.NBParserFactory.instance(context)
as com.itsaky.androidide.javac.services.NBParserFactory
val scannerFactory = ScannerFactory.instance(context)
val cancelService = com.itsaky.androidide.javac.services.CancelService.instance(context)
val lexer = scannerFactory.newScanner(buf, true)
if (
endPositions
is com.itsaky.androidide.javac.services.NBParserFactory.NBJavacParser.EndPosTableImpl
) {
endPositions.resetErrorEndPos()
}
return object :
com.itsaky.androidide.javac.services.NBParserFactory.NBJavacParser(
factory,
lexer,
true,
false,
true,
false,
cancelService
) {
override fun newEndPosTable(keepEndPositions: Boolean): AbstractEndPosTable {
return object : AbstractEndPosTable(this) {
override fun storeEnd(tree: JCTree?, endpos: Int) {
(endPositions as EndPosTableImpl).storeEnd(tree, endpos)
}
override fun <T : JCTree?> to(t: T): T {
storeEnd(t, token.endPos)
return t
}
override fun <T : JCTree?> toP(t: T): T {
storeEnd(t, S.prevToken().endPos)
return t
}
override fun getEndPos(tree: JCTree?): Int {
return endPositions!!.getEndPos(tree)
}
override fun replaceTree(oldtree: JCTree?, newtree: JCTree?): Int {
return endPositions!!.replaceTree(oldtree, newtree)
}
override fun setErrorEndPos(errPos: Int) {
super.setErrorEndPos(errPos)
(endPositions as EndPosTableImpl).setErrorEndPos(errPos)
}
}
}
}
}
private fun reAttrMethodBody(
context: Context,
scope: JavacScope,
tree: JCMethodDecl,
block: JCBlock,
): JCBlock {
val attr = Attr.instance(context)
val names: Names = Names.instance(context)
val syms: Symtab = Symtab.instance(context)
val log: Log = Log.instance(context)
val make: TreeMaker = TreeMaker.instance(context)
val env: Env<AttrContext> = scope.env // this is a copy anyway...
val owner: Symbol.ClassSymbol = env.enclClass.sym
if (tree.name === names.init && !owner.type.isErroneous && owner.type !== syms.objectType) {
val body = tree.body
if (body.stats.isEmpty() || !TreeInfo.isSelfCall(body.stats.head)) {
body.stats =
body.stats.prepend(
make.at(body.pos).Exec(make.Apply(List.nil(), make.Ident(names._super), List.nil()))
)
} else if (
(env.enclClass.sym.flags() and Flags.ENUM.toLong()) != 0L &&
(tree.mods.flags and Flags.GENERATEDCONSTR) == 0L &&
TreeInfo.isSuperCall(body.stats.head)
) {
// enum constructors are not allowed to call super
// directly, so make sure there aren't any super calls
// in enum constructorObjectCheckers, except in the compiler
// generated one.
log.error(
tree.body.stats.head.pos(),
openjdk.tools.javac.util.JCDiagnostic.Error(
"compiler",
"call.to.super.not.allowed.in.enum.ctor",
env.enclClass.sym
)
)
}
}
attr.attribStat(block, env)
return block
}
private fun reflowMethodBody(
context: Context,
ownerClass: ClassTree,
decl: JCMethodDecl
): BlockTree {
val flow = Flow.instance(context)
val maker = TreeMaker.instance(context)
val enter = Enter.instance(context)
flow.analyzeTree(enter.getEnv((ownerClass as JCClassDecl).sym), maker)
return decl.body
}
}
@@ -0,0 +1,99 @@
/*
* 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.javac.services.partial;
import java.util.Locale;
import jdkx.tools.Diagnostic;
import openjdk.tools.javac.api.DiagnosticFormatter;
import openjdk.tools.javac.util.JCDiagnostic;
/**
* @author Akash Yadav
*/
public class RichDiagnostic implements Diagnostic {
private final JCDiagnostic delegate;
private final DiagnosticFormatter<JCDiagnostic> formatter;
public RichDiagnostic(JCDiagnostic delegate, DiagnosticFormatter<JCDiagnostic> formatter) {
this.delegate = delegate;
this.formatter = formatter;
}
@Override
public Diagnostic.Kind getKind() {
return delegate.getKind();
}
@Override
public Object getSource() {
return delegate.getSource();
}
@Override
public long getPosition() {
return delegate.getPosition();
}
@Override
public long getStartPosition() {
return delegate.getStartPosition();
}
@Override
public long getEndPosition() {
return delegate.getEndPosition();
}
@Override
public long getLineNumber() {
return delegate.getLineNumber();
}
@Override
public long getColumnNumber() {
return delegate.getColumnNumber();
}
@Override
public String getCode() {
return delegate.getCode();
}
@Override
public String getMessage(Locale locale) {
return formatter.format(delegate, locale);
}
@Override
public String toString() {
return delegate.toString();
}
JCDiagnostic getDelegate() {
return delegate;
}
public static Diagnostic wrap(Diagnostic d, DiagnosticFormatter<JCDiagnostic> df) {
if (d instanceof JCDiagnostic) {
return new RichDiagnostic((JCDiagnostic) d, df);
} else {
return d;
}
}
}
@@ -0,0 +1,59 @@
/*
* 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.javac.services.util
import jdkx.tools.Diagnostic
import jdkx.tools.JavaFileObject
import openjdk.tools.javac.api.ClientCodeWrapper
import openjdk.tools.javac.util.JCDiagnostic
/** @author Akash Yadav */
class JavaDiagnosticUtils {
companion object {
@JvmStatic
fun asJCDiagnostic(diagnostic: Diagnostic<out JavaFileObject>): JCDiagnostic? {
if (diagnostic is JCDiagnostic) {
return diagnostic
} else if (diagnostic is ClientCodeWrapper.DiagnosticSourceUnwrapper) {
return diagnostic.d
}
return null
}
@JvmStatic
fun asUnwrapper(
diagnostic: Diagnostic<out JavaFileObject>
): ClientCodeWrapper.DiagnosticSourceUnwrapper? {
if (diagnostic is ClientCodeWrapper.DiagnosticSourceUnwrapper) {
return diagnostic
} else if (diagnostic is JCDiagnostic) {
return wrap(diagnostic)
}
return null
}
private fun wrap(diagnostic: JCDiagnostic): ClientCodeWrapper.DiagnosticSourceUnwrapper {
val klass = ClientCodeWrapper.DiagnosticSourceUnwrapper::class.java
val construct = klass.getDeclaredConstructor(JCDiagnostic::class.java)
construct.isAccessible = true
return construct.newInstance(diagnostic)
}
}
}
@@ -0,0 +1,204 @@
/*
* 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.javac.services.util;
import androidx.annotation.Nullable;
import com.itsaky.androidide.javac.services.visitors.UnEnter;
import com.itsaky.androidide.utils.VMUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import openjdk.tools.javac.comp.Enter;
import openjdk.tools.javac.parser.JavacParser;
import openjdk.tools.javac.parser.LazyDocCommentTable;
import openjdk.tools.javac.tree.DocCommentTable;
import openjdk.tools.javac.tree.JCTree;
import openjdk.tools.javac.util.Context;
import openjdk.tools.javac.util.Position;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Akash Yadav
*/
public class ReparserUtils {
private static final Logger LOG = LoggerFactory.getLogger(ReparserUtils.class);
private static final boolean isAndroid = !VMUtils.isJvm();
private static boolean needUnenterScanner = true;
private static Method unenter = null;
private static Method lineMapBuild = null;
private static Field lazyDocCommentsTable = null;
private static Field parserDocComments = null;
public static boolean canReparse() {
if (isAndroid) {
return true;
}
initIfNecessary();
return unenter != null
&& lineMapBuild != null
&& lazyDocCommentsTable != null
&& parserDocComments != null;
}
public static void buildLineMap(Position.LineMap lines, char[] arr, int size) {
if (!canReparse()) {
LOG.warn("Cannot perform reparse");
return;
}
if (isAndroid) {
((Position.LineMapImpl) lines).build(arr, size);
return;
}
try {
lineMapBuild.invoke(lines, arr, size);
} catch (Throwable e) {
LOG.error("Unable to build line map", e);
}
}
@Nullable
@SuppressWarnings("unchecked")
public static Map<JCTree, LazyDocCommentTable.Entry> getLazyDocCommentsTable(
DocCommentTable docs) {
if (!canReparse()) {
LOG.warn("Cannot perform reparse");
return null;
}
if (isAndroid) {
return ((LazyDocCommentTable) docs).table;
}
try {
return (Map<JCTree, LazyDocCommentTable.Entry>) lazyDocCommentsTable.get(docs);
} catch (Throwable e) {
LOG.error("Cannot get doc comment table", e);
throw new RuntimeException(e);
}
}
public static void unenter(Context context, JCTree.JCCompilationUnit cu, JCTree tree) {
if (!canReparse()) {
LOG.warn("Cannot perform reparse");
return;
}
if (isAndroid) {
Enter.instance(context).unenter(cu, tree);
return;
}
if (needUnenterScanner) {
new UnEnter(Enter.instance(context), cu.modle).scan(tree);
return;
}
try {
unenter.invoke(Enter.instance(context), cu, tree);
} catch (Throwable err) {
LOG.error("Cannot perform unenter", err);
}
}
@Nullable
public static DocCommentTable getDocComments(JavacParser parser) {
if (!canReparse()) {
LOG.warn("Cannot perform reparse");
return null;
}
if (isAndroid) {
return parser.getDocComments();
}
try {
return (DocCommentTable) parserDocComments.get(parser);
} catch (Throwable e) {
LOG.error("Unablet to get DocCommentTable", e);
throw new RuntimeException(e);
}
}
private static void initIfNecessary() {
if (isAndroid) {
return;
}
if (unenter == null) {
unenter =
tryReflectMethod(Enter.class, "unenter", JCTree.JCCompilationUnit.class, JCTree.class);
needUnenterScanner = unenter == null;
}
if (lineMapBuild == null) {
lineMapBuild =
tryReflectMethod(
"openjdk.tools.javac.util.Position$LineMapImpl", "build", char[].class, int.class);
}
if (lazyDocCommentsTable == null) {
lazyDocCommentsTable = tryReflectField(LazyDocCommentTable.class, "table");
}
if (parserDocComments == null) {
parserDocComments = tryReflectField(JavacParser.class, "docComments");
}
}
@SuppressWarnings("SameParameterValue")
@Nullable
private static Method tryReflectMethod(String klass, String methodName, Class<?>... argTypes) {
try {
return tryReflectMethod(Class.forName(klass), methodName, argTypes);
} catch (ClassNotFoundException noMethod) {
return null;
}
}
@Nullable
private static Method tryReflectMethod(Class<?> klass, String methodName, Class<?>... argTypes) {
try {
final Method method = klass.getDeclaredMethod(methodName, argTypes);
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method;
} catch (NoSuchMethodException noMethod) {
return null;
}
}
@Nullable
private static Field tryReflectField(Class<?> klass, String fieldName) {
try {
final Field field = klass.getDeclaredField(fieldName);
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException noMethod) {
return null;
}
}
}
@@ -0,0 +1,31 @@
/*
* 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.javac.services.visitors
import openjdk.source.tree.ErroneousTree
import openjdk.source.util.TreeScanner
/** @author Akash Yadav */
open class ErrorAwareTreeScanner<R, P> : TreeScanner<R, P>() {
override fun visitErroneous(node: ErroneousTree?, p: P): R {
if (node == null) {
return super.visitErroneous(node, p)
}
return scan(node.errorTrees, p)
}
}
@@ -0,0 +1,75 @@
/*
* 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.javac.services.visitors
import com.itsaky.androidide.javac.services.visitors.FindAnonymousVisitor.Mode.CHECK
import com.itsaky.androidide.javac.services.visitors.FindAnonymousVisitor.Mode.COLLECT
import openjdk.source.tree.ClassTree
import openjdk.source.tree.MethodTree
import openjdk.source.tree.Tree
import openjdk.source.tree.VariableTree
/**
* Partial reparse helper visitor. Finds anonymous and local classes in given method tree.
*
* @author Akash Yadav
*/
class FindAnonymousVisitor : ErrorAwareTreeScanner<Unit, Unit>() {
private enum class Mode {
COLLECT,
CHECK
}
var noInner = 0
var hasLocalClass = false
val docOwners: MutableSet<Tree> = HashSet<Tree>()
private var mode = COLLECT
fun reset() {
noInner = 0
hasLocalClass = false
mode = CHECK
}
override fun visitClass(node: ClassTree, p: Unit?): Unit? {
if (node.simpleName.isNotEmpty()) {
hasLocalClass = true
}
noInner++
handleDoc(node)
return super.visitClass(node, p)
}
override fun visitMethod(node: MethodTree, p: Unit?): Unit? {
handleDoc(node)
return super.visitMethod(node, p)
}
override fun visitVariable(node: VariableTree, p: Unit?): Unit? {
handleDoc(node)
return super.visitVariable(node, p)
}
private fun handleDoc(tree: Tree) {
if (mode == COLLECT) {
docOwners.add(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.androidide.javac.services.visitors;
import openjdk.source.tree.CompilationUnitTree;
import openjdk.source.tree.MethodTree;
import openjdk.source.tree.Tree;
import openjdk.source.tree.VariableTree;
import openjdk.tools.javac.tree.EndPosTable;
import openjdk.tools.javac.tree.JCTree;
import openjdk.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);
}
}
@@ -0,0 +1,47 @@
/*
* 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.javac.services.visitors
import com.blankj.utilcode.util.ReflectUtils
import openjdk.tools.javac.code.Symbol.ModuleSymbol
import openjdk.tools.javac.comp.Enter
import openjdk.tools.javac.tree.JCTree.JCClassDecl
import openjdk.tools.javac.tree.TreeScanner
/**
* `Enter.unenter` method is not available in JDK 11. This visitor does the same thing as calling
* `Enter.unenter` in JDK 17.
*
* This is never used in the Android Runtime.
*
* @author Akash Yadav
*/
class UnEnter(private val enter: Enter, private val msym: ModuleSymbol) : TreeScanner() {
override fun visitClassDef(tree: JCClassDecl) {
val csym = tree.sym ?: return
val etr = ReflectUtils.reflect(enter)
ReflectUtils.reflect(etr.field("typeEnvs")).method("remove", csym)
val chk = etr.field("chk")
ReflectUtils.reflect(chk).method("removeCompiled", csym)
ReflectUtils.reflect(chk).method("clearLocalClassNameIndexes", csym)
ReflectUtils.reflect(etr.field("syms")).method("removeClass", msym, csym.flatname)
super.visitClassDef(tree)
}
}
+1
View File
@@ -0,0 +1 @@
/build
+83
View File
@@ -0,0 +1,83 @@
/*
* 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/>.
*/
import com.itsaky.androidide.build.config.BuildConfig
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-kapt")
id("realm-android")
}
groupConfig {
groupIdSuffix.set("event")
}
android {
namespace = "${BuildConfig.packageName}.lsp.java"
sourceSets {
getByName("androidTest") {
assets.srcDirs(rootProject.file("subprojects/framework-stubs/libs"))
}
}
}
kapt {
arguments {
arg("eventBusIndex", "${BuildConfig.packageName}.events.LspJavaEventsIndex")
}
}
dependencies {
kapt(projects.annotation.processors)
implementation(libs.androidide.ts)
implementation(libs.androidide.ts.java)
implementation(libs.androidx.appcompat)
implementation(libs.common.editor)
implementation(libs.common.javaparser)
implementation(libs.common.utilcode)
implementation(libs.androidx.annotation)
implementation(libs.google.guava)
implementation(libs.google.gson)
implementation(libs.google.material)
implementation(projects.core.actions)
implementation(projects.core.common)
implementation(projects.core.indexing)
implementation(projects.core.lspApi)
implementation(projects.core.resources)
implementation(projects.editor.api)
implementation(projects.java.javacServices)
implementation(libs.composite.javac)
implementation(libs.composite.javapoet)
implementation(libs.composite.jaxp)
implementation(libs.composite.jdkJdeps)
implementation(libs.composite.googleJavaFormat)
implementation(libs.androidx.core.ktx)
implementation(libs.common.kotlin)
testImplementation(projects.testing.commonTest)
testImplementation(projects.testing.lspTest)
androidTestImplementation(projects.testing.androidTest)
androidTestImplementation(projects.utilities.shared)
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,120 @@
/*
* 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.java.indexing
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaClass
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
import io.realm.RealmList
import io.realm.RealmObject
import openjdk.tools.classfile.AccessFlags
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* @author Akash Yadav
*/
@RunWith(AndroidJUnit4::class)
class JavaClassCRUDTest {
@Rule
@JvmField
val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule())
@Test
fun testSimpleJavaClassCRUD() {
dbTestRule.withDb("java-class-CRUD") {
val klass = ModelBuilder.createJavaClass()
// CREATE
executeTransaction {
it.insert(klass)
}
var dbKlass: JavaClass? = null
executeTransaction {
// READ
dbKlass = where(JavaClass::class.java)
.equalTo("fqn", "com/itsaky/androidide/indexing/TestClass")
.findFirst()
assertThat(dbKlass).isNotNull()
assertThat(dbKlass?.fqn).isEqualTo("com/itsaky/androidide/indexing/TestClass")
assertThat(dbKlass?.name).isEqualTo("TestClass")
assertThat(dbKlass?.packageName).isEqualTo("com/itsaky/androidide/indexing")
assertThat(dbKlass?.accessFlags).isEqualTo(AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL)
assertThat(dbKlass?.superClassFqn).isEqualTo("java/lang/Object")
assertThat(dbKlass?.superInterfacesFqn).isEqualTo(RealmList<String>())
assertThat(dbKlass?.fields).isNotEmpty()
assertThat(dbKlass?.fields).hasSize(1)
dbKlass?.fields?.get(0).apply {
assertThat(this).isNotNull()
assertThat(this?.name).isEqualTo("someString")
assertThat(this?.type).isEqualTo(JavaType.STRING)
assertThat(this?.accessFlags).isEqualTo(AccessFlags.ACC_PRIVATE)
}
assertThat(dbKlass?.methods).isNotEmpty()
assertThat(dbKlass?.methods).hasSize(1)
dbKlass?.methods?.get(0).apply {
assertThat(this).isNotNull()
assertThat(this?.name).isEqualTo("getSomeString")
assertThat(this?.paramsTypes).isEqualTo(RealmList<String>())
assertThat(this?.returnType).isEqualTo(JavaType.STRING)
assertThat(this?.accessFlags).isEqualTo(AccessFlags.ACC_PUBLIC)
}
}
assertThat(dbKlass).isNotNull()
// UPDATE
executeTransaction {
dbKlass?.accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_ABSTRACT
}
// verify UPDATE
executeTransaction {
assertThat(
where(JavaClass::class.java)
.equalTo("fqn", "com/itsaky/androidide/indexing/TestClass")
.findFirst()?.accessFlags
).isEqualTo(AccessFlags.ACC_PUBLIC or AccessFlags.ACC_ABSTRACT)
}
// DELETE
executeTransaction {
RealmObject.deleteFromRealm(dbKlass!!)
}
// verify DELTE
executeTransaction {
assertThat(
where(JavaClass::class.java)
.equalTo("fqn", "com/itsaky/androidide/indexing/TestClass")
.findFirst()
).isNull()
}
}
}
}
@@ -0,0 +1,100 @@
/*
* 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.java.indexing
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.blankj.utilcode.util.ConvertUtils
import com.google.common.truth.Truth
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
import io.realm.RealmModel
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
/**
* @author Akash Yadav
*/
@RunWith(AndroidJUnit4::class)
class JavaIndexerTest {
@Rule
@JvmField
val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule())
val androidJar: File by lazy {
val context = ApplicationProvider.getApplicationContext<Context>()
val file = File.createTempFile("ajar", null, context.cacheDir)
context.assets.open("android.jar").use { asset ->
Truth.assertThat(asset).isNotNull()
file.outputStream().buffered().use { out ->
asset.copyTo(out)
out.flush()
}
}
file
}
@Test
fun testSimpleAndroidJarIndexingDurationCheck() {
val worker = JavaJarModelBuilder(androidJar)
val batches = mutableMapOf<Class<*>, MutableList<RealmModel>>()
var dbWriteDuration = 0L
dbTestRule.withDb("android-jar-classes") {
val totalStart = System.currentTimeMillis()
worker.consumeTypes { type ->
val batched = batches.computeIfAbsent(type.javaClass) { mutableListOf() }
batched.add(type)
if (batched.size >= 100) {
val start = System.currentTimeMillis()
executeTransaction {
insertOrUpdate(batched)
}
dbWriteDuration += System.currentTimeMillis() - start
batched.clear()
}
}
for ((_, batched) in batches) {
if (batched.isNotEmpty()) {
val start = System.currentTimeMillis()
executeTransaction {
insertOrUpdate(batched)
}
dbWriteDuration += System.currentTimeMillis() - start
}
}
println(
"Took ${dbWriteDuration}ms to write android.jar (${
ConvertUtils.byte2FitMemorySize(
androidJar.length()
)
}) classes to Realm"
)
println("Total time: ${System.currentTimeMillis() - totalStart}ms")
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.java.indexing
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaClass
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaField
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaMethod
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
import io.realm.RealmList
import openjdk.tools.classfile.AccessFlags
/**
* @author Akash Yadav
*/
object ModelBuilder {
fun createJavaClass() = JavaClass.newInstance(
fqn = "com/itsaky/androidide/indexing/TestClass",
name = "TestClass",
packageName = "com/itsaky/androidide/indexing",
accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL,
superClassFqn = "java/lang/Object",
superInterfacesFqn = RealmList(),
fields = RealmList<JavaField>().apply {
add(JavaField.newField("someString", JavaType.STRING, AccessFlags.ACC_PRIVATE))
},
methods = RealmList<JavaMethod>().apply {
add(
JavaMethod.newInstance(
name = "getSomeString",
paramsTypes = RealmList(),
returnType = JavaType.STRING,
accessFlags = AccessFlags.ACC_PUBLIC
)
)
})
}
@@ -0,0 +1,276 @@
/*
* 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.java.indexing
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
import com.itsaky.androidide.lsp.java.indexing.classfile.ArrayAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.ClassAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.EnumAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.IAnnotationElementValue
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaConstant
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
import com.itsaky.androidide.lsp.java.indexing.classfile.PrimitiveAnnotationElementValue
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
import io.realm.RealmAny
import io.realm.RealmList
import io.realm.exceptions.RealmPrimaryKeyConstraintException
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* @author Akash Yadav
*/
@RunWith(AndroidJUnit4::class)
class SharedModelCRUDTest {
@Rule
@JvmField
val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule())
@Test
fun testApiInfoCreate() {
dbTestRule.withDb("api-info") {
val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
assertInsertUnique(apiInfo)
val apiInfo2 = apiInfo.clone().update(removedIn = 26)
assertInsertion(allowUpdate = true, apiInfo, apiInfo2) { models ->
assertThat(models).hasSize(2)
assertThat(models).containsExactly(apiInfo, apiInfo2)
}
}
}
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testApiInfoDuplicationError() {
dbTestRule.withDb("api-info-duplication-error") {
val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
assertInsertSingle(apiInfo)
assertInsertSingle(apiInfo)
}
}
@Test
fun testConstantCreate() {
dbTestRule.withDb("java-constant-create") {
val constant =
JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(1.toInt()))
val constant2 =
JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(2.toInt()))
assertInsertUnique(constant)
assertInsertion(allowUpdate = true, constant, constant2) { models ->
assertThat(models).hasSize(2)
assertThat(models).containsExactly(constant, constant2)
}
}
}
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testConstantDuplicationError() {
dbTestRule.withDb("java-constant-duplication-error") {
val constant =
JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(1.toInt()))
assertInsertSingle(constant)
assertInsertSingle(constant)
}
}
@Test
fun testJavaTypeCreate() {
dbTestRule.withDb("java-type-create") {
assertInsertUnique(JavaType.INT)
assertInsertion(JavaType.STRING, JavaType.OBJECT) { models ->
assertThat(models).hasSize(3)
assertThat(models).containsExactly(JavaType.INT, JavaType.STRING, JavaType.OBJECT)
}
}
}
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testJavaTypeDuplicationError() {
dbTestRule.withDb("java-type-duplication-error") {
assertInsertSingle(JavaType.OBJECT)
assertInsertSingle(JavaType.OBJECT)
}
}
@Test
fun testPrimitiveAnnotationValueCreate() {
dbTestRule.withDb("annotation-value-primitive") {
val prim1 = PrimitiveAnnotationElementValue.newInstance(
IAnnotationElementValue.KIND_INT,
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(1.toInt()))
)
val prim2 = PrimitiveAnnotationElementValue.newInstance(
IAnnotationElementValue.KIND_STRING,
JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something"))
)
assertInsertUnique(prim1)
assertInsertion(allowUpdate = true, prim1, prim2) { models ->
assertThat(models).hasSize(2)
assertThat(models).containsExactly(prim1, prim2)
}
}
}
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testPrimitiveAnnotationValueDuplicationError() {
dbTestRule.withDb("annotation-value-primitive-duplication") {
val prim1 = PrimitiveAnnotationElementValue.newInstance(
IAnnotationElementValue.KIND_INT,
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(1.toInt()))
)
assertInsertSingle(prim1)
assertInsertSingle(prim1)
}
}
@Test
fun testArrayAnnotationValueCreate() {
dbTestRule.withDb("annotation-value-array") {
val obj1 = ArrayAnnotationElementValue.newInstance(
RealmList()
)
val obj2 = ArrayAnnotationElementValue.newInstance(
RealmList<RealmAny>().apply {
add(
RealmAny.valueOf(
PrimitiveAnnotationElementValue.newInstance(
IAnnotationElementValue.KIND_STRING,
JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something"))
)
)
)
RealmAny.valueOf(
PrimitiveAnnotationElementValue.newInstance(
IAnnotationElementValue.KIND_INT,
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(123.toInt()))
)
)
}
)
assertInsertUnique(obj1)
assertInsertion(allowUpdate = true, obj1, obj2) { models ->
assertThat(models).hasSize(2)
assertThat(models).containsExactly(obj1, obj2)
}
}
}
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testArrayAnnotationValueDuplicationError() {
dbTestRule.withDb("annotation-value-array-duplication") {
val obj1 = ArrayAnnotationElementValue.newInstance(
RealmList<RealmAny>().apply {
add(
RealmAny.valueOf(
PrimitiveAnnotationElementValue.newInstance(
IAnnotationElementValue.KIND_STRING,
JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something"))
)
)
)
RealmAny.valueOf(
PrimitiveAnnotationElementValue.newInstance(
IAnnotationElementValue.KIND_INT,
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(123.toInt()))
)
)
}
)
assertInsertSingle(obj1)
assertInsertSingle(obj1)
}
}
@Test
fun testClassAnnotationValueCreate() {
dbTestRule.withDb("annotation-value-class") {
val obj1 = ClassAnnotationElementValue.newInstance(
JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF)
)
val obj2 = ClassAnnotationElementValue.newInstance(
JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF, arrayDims = 5)
)
assertInsertUnique(obj1)
assertInsertion(allowUpdate = true, obj1, obj2) { models ->
assertThat(models).hasSize(2)
assertThat(models).containsExactly(obj1, obj2)
}
}
}
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testClassAnnotationValueDuplicationError() {
dbTestRule.withDb("annotation-value-class-duplication") {
val obj1 = ClassAnnotationElementValue.newInstance(
JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF)
)
assertInsertSingle(obj1)
assertInsertSingle(obj1)
}
}
@Test
fun testEnumAnnotationValueCreate() {
dbTestRule.withDb("annotation-value-enum") {
val obj1 = EnumAnnotationElementValue.newInstance(
"SomeEnum_ENTRY",
JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF),
)
val obj2 = EnumAnnotationElementValue.newInstance(
"SomeEnum_ENTRY_2",
JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF),
)
assertInsertUnique(obj1)
assertInsertion(allowUpdate = true, obj1, obj2) { models ->
assertThat(models).hasSize(2)
assertThat(models).containsExactly(obj1, obj2)
}
}
}
@Test(expected = RealmPrimaryKeyConstraintException::class)
fun testEnumAnnotationValueDuplicationError() {
dbTestRule.withDb("annotation-value-enum-duplication") {
val obj1 = EnumAnnotationElementValue.newInstance(
"SomeEnum_ENTRY",
JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF),
)
assertInsertSingle(obj1)
assertInsertSingle(obj1)
}
}
}
@@ -0,0 +1,72 @@
/*
* 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.java.indexing
import com.google.common.truth.Truth.assertThat
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmResults
inline fun <reified T : RealmModel> Realm.assertInsertion(
vararg models: T,
crossinline verify: Realm.(models: RealmResults<T>) -> Unit
) {
assertInsertion(false, *models, verify = verify)
}
inline fun <reified T : RealmModel> Realm.assertInsertion(
allowUpdate: Boolean = false,
vararg models: T,
crossinline verify: Realm.(models: RealmResults<T>) -> Unit
) {
val modelList = models.toList()
assertThat(modelList).isNotEmpty()
executeTransaction {
if (allowUpdate) {
it.insertOrUpdate(modelList)
} else {
it.insert(modelList)
}
}
executeTransaction {
verify(it.where(modelList[0].javaClass).findAll())
}
}
fun Realm.assertInsertSingle(model: RealmModel) {
assertInsertion(model) { models ->
assertThat(models).isNotNull()
assertThat(models).isNotEmpty()
assertThat(models).hasSize(1)
assertThat(models[0]).isEqualTo(model)
}
}
fun Realm.assertInsertUnique(model: RealmModel) {
assertInsertSingle(model)
assertInsertion(true, model) { models ->
assertThat(models).isNotNull()
assertThat(models).isNotEmpty()
assertThat(models).hasSize(1)
assertThat(models[0]).isEqualTo(model)
}
}
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<manifest/>
@@ -0,0 +1,36 @@
{
"lcomm": {
"desc": "Create a line comment",
"body": [
"// $0"
]
},
"bcomm": {
"desc": "Create a block comment",
"body": [
"/*",
" * $0",
" */"
]
},
"todo": {
"desc": "Create a TODO comment",
"body": [
"// TODO: $0"
]
},
"fixme": {
"desc": "Create a FIXME comment",
"body": [
"// FIXME: $0"
]
},
"jdoc": {
"desc": "Create a JavaDoc comment",
"body": [
"/**",
" * $0",
" */"
]
}
}
@@ -0,0 +1,103 @@
{
"for": {
"desc": "Indexed for loop",
"body": [
"for(int ${1:i} = 0; $1 < ${2:count}; ++$1) {",
"\t$0",
"}"
]
},
"foreach": {
"desc": "For-each loop",
"body": [
"for(${1:element} : ${2:iterable}) {",
"\t$0",
"}"
]
},
"forr": {
"desc": "Reverse-indexed for loop",
"body": [
"for(int ${1:i} = ${2:count} - 1; $1 >= 0; --$1) {",
"\t$0",
"}"
]
},
"while": {
"desc": "While loop",
"body": [
"while(${1:condition}) {",
"\t$0",
"}"
]
},
"dowhile": {
"desc": "Do-While loop",
"body": [
"do {",
"\t$0",
"} while(${1:condition});"
]
},
"if": {
"desc": "If statement",
"body": [
"if(${1:condition}) {",
"\t$0",
"}"
]
},
"ifelse": {
"desc": "If-else statement",
"body": [
"if(${1:condition}) {",
"\t$0",
"} else {",
"\t",
"}"
]
},
"ifnull": {
"desc": "If-null",
"body": [
"if(${1:object} == null) {",
"\t$0",
"}"
]
},
"ifnotnull": {
"desc": "If-not-null",
"body": [
"if(${1:object} != null) {",
"\t$0",
"}"
]
},
"new_object": {
"desc": "Create a new object",
"body": [
"${1:Object} ${2:foo} = new $1($3);",
"$0"
]
},
"trycatch": {
"desc": "A try-catch statement",
"body": [
"try {",
"\t$0",
"} catch(${1:Exception} ${2:err}) {",
"\t",
"}"
]
},
"tryresources": {
"desc": "A try-with-resources statement",
"body": [
"try ($1) {",
"\t$0",
"} catch(${2:Exception} ${3:err}) {",
"\t",
"}"
]
}
}
@@ -0,0 +1,56 @@
{
"const": {
"desc": "Create a constant field",
"body": [
"public static final ${1:type} ${2/(.*)/${1:/upcase}/} = ${3:value};"
]
},
"ctor": {
"desc": "Public constructor",
"body": [
"public ${1:$TM_FILENAME_BASE}($2) {",
"\t${3:super();}$0",
"}"
]
},
"public_method": {
"desc": "Create a new public method",
"body": [
"public ${1:void} ${2:method}($3) {",
"\t$0",
"}"
]
},
"protected_method": {
"desc": "Create a new protected method",
"body": [
"protected ${1:void} ${2:method}($3) {",
"\t$0",
"}"
]
},
"private_method": {
"desc": "Create a new private method",
"body": [
"private ${1:void} ${2:method}($3) {",
"\t$0",
"}"
]
},
"public_static_method": {
"desc": "Create a new public static method",
"body": [
"public static ${1:void} ${2:method}($3) {",
"\t$0",
"}"
]
},
"private_static_method": {
"desc": "Create a new private stataic method",
"body": [
"private static ${1:void} ${2:method}($3) {",
"\t$0",
"}"
]
}
}
@@ -0,0 +1,26 @@
{
"class": {
"desc": "Create a new class",
"body" : [
"public class ${1:$TM_FILENAME_BASE} {",
"\t$0",
"}"
]
},
"interface": {
"desc": "Create a new interface",
"body" : [
"public interface ${1:$TM_FILENAME_BASE} {",
"\t$0",
"}"
]
},
"enum": {
"desc": "Create a new enum class",
"body" : [
"public enum ${1:$TM_FILENAME_BASE} {",
"\t$0",
"}"
]
}
}
@@ -0,0 +1,28 @@
/*
* 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.java
import java.util.concurrent.CancellationException
/**
* Thrown when a compilation process is cancelled.
* @author Akash Yadav
*/
class CompilationCancellationException @JvmOverloads constructor(
override val cause: Throwable? = null
) : CancellationException()
@@ -0,0 +1,75 @@
/*
* 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.java;
import androidx.annotation.NonNull;
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
import com.itsaky.androidide.projects.api.ModuleProject;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Provides {@link JavaCompilerService} instances for different {@link ModuleProject}s.
*
* @author Akash Yadav
*/
public class JavaCompilerProvider {
private static JavaCompilerProvider sInstance;
private final Map<ModuleProject, JavaCompilerService> mCompilers = new ConcurrentHashMap<>();
private JavaCompilerProvider() {}
@NonNull
public static JavaCompilerService get(ModuleProject module) {
return JavaCompilerProvider.getInstance().forModule(module);
}
public static JavaCompilerProvider getInstance() {
if (sInstance == null) {
sInstance = new JavaCompilerProvider();
}
return sInstance;
}
@NonNull
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.
final JavaCompilerService cached = mCompilers.get(module);
if (cached != null && cached.getModule() != null) {
return cached;
}
final JavaCompilerService newInstance = new JavaCompilerService(module);
mCompilers.put(module, newInstance);
return newInstance;
}
// TODO This currently destroys all the compiler instances
// We must have a method to destroy only the required instance in
// JavaLanguageServer.handleFailure(LSPFailure)
public synchronized void destroy() {
for (final JavaCompilerService compiler : mCompilers.values()) {
compiler.destroy();
}
mCompilers.clear();
}
}
@@ -0,0 +1,318 @@
/*
* 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.java
import androidx.annotation.RestrictTo
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent
import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent
import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton
import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCache
import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCachesForPaths
import com.itsaky.androidide.lsp.api.ILanguageClient
import com.itsaky.androidide.lsp.api.ILanguageServer
import com.itsaky.androidide.lsp.api.IServerSettings
import com.itsaky.androidide.lsp.internal.model.CachedCompletion
import com.itsaky.androidide.lsp.java.actions.JavaCodeActionsMenu
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.compiler.SourceFileManager
import com.itsaky.androidide.lsp.java.models.JavaServerSettings
import com.itsaky.androidide.lsp.java.providers.CodeFormatProvider
import com.itsaky.androidide.lsp.java.providers.CompletionProvider
import com.itsaky.androidide.lsp.java.providers.DefinitionProvider
import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider
import com.itsaky.androidide.lsp.java.providers.JavaSelectionProvider
import com.itsaky.androidide.lsp.java.providers.ReferenceProvider
import com.itsaky.androidide.lsp.java.providers.SignatureProvider
import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository.init
import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer
import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled
import com.itsaky.androidide.lsp.models.CodeFormatResult
import com.itsaky.androidide.lsp.models.CompletionParams
import com.itsaky.androidide.lsp.models.CompletionResult
import com.itsaky.androidide.lsp.models.DefinitionParams
import com.itsaky.androidide.lsp.models.DefinitionResult
import com.itsaky.androidide.lsp.models.DiagnosticResult
import com.itsaky.androidide.lsp.models.ExpandSelectionParams
import com.itsaky.androidide.lsp.models.FailureType
import com.itsaky.androidide.lsp.models.FormatCodeParams
import com.itsaky.androidide.lsp.models.LSPFailure
import com.itsaky.androidide.lsp.models.ReferenceParams
import com.itsaky.androidide.lsp.models.ReferenceResult
import com.itsaky.androidide.lsp.models.SignatureHelp
import com.itsaky.androidide.lsp.models.SignatureHelpParams
import com.itsaky.androidide.lsp.util.LSPEditorActions
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.FileManager.getActiveDocumentCount
import com.itsaky.androidide.projects.IProjectManager.Companion.getInstance
import com.itsaky.androidide.projects.api.ModuleProject
import com.itsaky.androidide.projects.api.Project
import com.itsaky.androidide.utils.DocumentUtils
import com.itsaky.androidide.utils.VMUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.util.Objects
class JavaLanguageServer : ILanguageServer {
private val completionProvider: CompletionProvider = CompletionProvider()
private val diagnosticProvider: JavaDiagnosticProvider?
override var client: ILanguageClient? = null
private set
private var _settings: IServerSettings? = null
private var selectedFile: Path? = null
private val timer = AnalyzeTimer { analyzeSelected() }
private var cachedCompletion: CachedCompletion
val settings: IServerSettings
get() {
return _settings ?: JavaServerSettings.getInstance()
.also { _settings = it }
}
override val serverId: String = SERVER_ID
companion object {
const val SERVER_ID = "ide.lsp.java"
private val log = LoggerFactory.getLogger(JavaLanguageServer::class.java)
}
init {
diagnosticProvider = JavaDiagnosticProvider()
cachedCompletion = CachedCompletion.EMPTY
applySettings(JavaServerSettings.getInstance())
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this)
}
init()
}
override fun shutdown() {
JavaCompilerProvider.getInstance().destroy()
SourceFileManager.clearCache()
CacheFSInfoSingleton.clearCache()
clearCache()
EventBus.getDefault().unregister(this)
timer.cancel()
}
override fun connectClient(client: ILanguageClient?) {
this.client = client
}
override fun applySettings(settings: IServerSettings?) {
this._settings = settings
}
override fun setupWithProject(project: Project) {
LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu)
// Once we have project initialized
// Destory the NO_MODULE_COMPILER instance
JavaCompilerService.NO_MODULE_COMPILER.destroy()
// Clear cached file managers
SourceFileManager.clearCache()
// Clear cached JAR file system for R.jar
// Using the cached instance will result in completions not being updated for updated resources
// TODO Clearing caches for JAR files ending with '/R.jar' is probably not a good idea
// Maybe this could be improved by using data from the AndroidModule project model
clearCachesForPaths { path: String -> path.endsWith("/R.jar") }
// Clear cached module-specific compilers
JavaCompilerProvider.getInstance().destroy()
// Cache classpath locations
for (subModule in project.subProjects) {
if (subModule !is ModuleProject || subModule.path == project.rootProject.path) {
continue
}
SourceFileManager.forModule(subModule)
}
startOrRestartAnalyzeTimer()
}
override fun complete(params: CompletionParams?): CompletionResult {
val compiler = getCompiler(params!!.file)
if (!settings.completionsEnabled() || !completionProvider.canComplete(params.file)
) {
return CompletionResult.EMPTY
}
if (diagnosticProvider!!.isAnalyzing()) {
log.warn("Cancelling source code analysis due to completion request")
diagnosticProvider.cancel()
}
completionProvider.reset(
compiler, settings, cachedCompletion) { cachedCompletion: CachedCompletion ->
updateCachedCompletion(cachedCompletion)
}
return completionProvider.complete(params)
}
override suspend fun findReferences(params: ReferenceParams): ReferenceResult {
val compiler = getCompiler(params.file)
return if (!settings.referencesEnabled()) {
ReferenceResult(emptyList())
} else ReferenceProvider(compiler, params.cancelChecker).findReferences(params)
}
override suspend fun findDefinition(params: DefinitionParams): DefinitionResult {
val compiler = getCompiler(params.file)
return if (!settings.definitionsEnabled()) {
DefinitionResult(emptyList())
} else DefinitionProvider(compiler, settings, params.cancelChecker).findDefinition(params)
}
override suspend fun expandSelection(params: ExpandSelectionParams): Range {
val compiler = getCompiler(params.file)
return if (!settings.smartSelectionsEnabled()) {
params.selection
} else JavaSelectionProvider(compiler).expandSelection(params)
}
override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp {
val compiler = getCompiler(params.file)
return if (!settings.signatureHelpEnabled()) {
SignatureHelp(emptyList(), -1, -1)
} else SignatureProvider(compiler, params.cancelChecker).signatureHelp(params)
}
override suspend fun analyze(file: Path): DiagnosticResult {
if (!settings.diagnosticsEnabled() || !DocumentUtils.isJavaFile(file)) {
return DiagnosticResult.NO_UPDATE
}
return if (!settings.codeAnalysisEnabled()) {
DiagnosticResult.NO_UPDATE
} else diagnosticProvider!!.analyze(file)
}
override fun formatCode(params: FormatCodeParams?): CodeFormatResult {
return CodeFormatProvider(settings).format(params)
}
override fun handleFailure(failure: LSPFailure?): Boolean {
return when (failure!!.type) {
FailureType.COMPLETION -> {
if (isCancelled(failure.error)) {
return true
}
JavaCompilerProvider.getInstance().destroy()
true
}
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun getCompiler(file: Path?): JavaCompilerService {
if (!DocumentUtils.isJavaFile(file)) {
return JavaCompilerService.NO_MODULE_COMPILER
}
val root = getInstance().rootProject
?: return JavaCompilerService.NO_MODULE_COMPILER
val module = root.findModuleForFile(file!!) ?: return JavaCompilerService.NO_MODULE_COMPILER
return JavaCompilerProvider.get(module)
}
private fun updateCachedCompletion(cachedCompletion: CachedCompletion) {
Objects.requireNonNull(cachedCompletion)
this.cachedCompletion = cachedCompletion
}
private fun startOrRestartAnalyzeTimer() {
if (VMUtils.isJvm()) {
return
}
if (!timer.isStarted) {
timer.start()
} else {
timer.restart()
}
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onContentChange(event: DocumentChangeEvent) {
if (!DocumentUtils.isJavaFile(event.changedFile)) {
return
}
// TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance
JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event)
val module = getInstance()
.findModuleForFile(event.changedFile)
if (module != null) {
val compiler = JavaCompilerProvider.get(module)
compiler.onDocumentChange(event)
}
startOrRestartAnalyzeTimer()
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onFileSelected(event: DocumentSelectedEvent) {
selectedFile = event.selectedFile
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onFileOpened(event: DocumentOpenEvent) {
selectedFile = event.openedFile
startOrRestartAnalyzeTimer()
}
@Subscribe(threadMode = ThreadMode.ASYNC)
@Suppress("unused")
fun onFileClosed(event: DocumentCloseEvent) {
diagnosticProvider?.clearTimestamp(event.closedFile)
if (getActiveDocumentCount() == 0) {
selectedFile = null
timer.cancel()
}
}
private fun analyzeSelected() {
if (selectedFile == null || client == null) {
return
}
CoroutineScope(Dispatchers.Default).launch {
val result = analyze(selectedFile!!)
withContext(Dispatchers.Main) {
client?.publishDiagnostics(result)
}
}
}
}
@@ -0,0 +1,114 @@
/*
* 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.java.actions
import android.content.Context
import android.graphics.drawable.Drawable
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.EditorActionItem
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.lsp.api.ILanguageClient
import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.rewrite.Rewrite
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.utils.DocumentUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.flashError
import java.io.File
/**
* Base class for java code actions
*
* @author Akash Yadav
*/
abstract class BaseJavaCodeAction : EditorActionItem {
override var visible: Boolean = true
override var enabled: Boolean = true
override var icon: Drawable? = null
override var requiresUIThread: Boolean = false
override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS
protected abstract val titleTextRes: Int
override fun prepare(data: ActionData) {
super.prepare(data)
if (
!data.hasRequiredData(Context::class.java, JavaLanguageServer::class.java, File::class.java)
) {
markInvisible()
return
}
if (titleTextRes != -1) {
label = data[Context::class.java]!!.getString(titleTextRes)
}
val file = data.requireFile()
visible = DocumentUtils.isJavaFile(file.toPath())
enabled = visible
}
fun performCodeAction(data: ActionData, result: Rewrite) {
val compiler = data.requireCompiler()
val actions =
try {
result.asCodeActions(compiler, label)
} catch (e: Exception) {
flashError(e.cause?.message ?: e.message)
ILogger.ROOT.error(e.cause?.message ?: e.message, e)
return
}
if (actions == null) {
onPerformCodeActionFailed(data)
return
}
data.getLanguageClient()?.performCodeAction(actions)
}
protected open fun onPerformCodeActionFailed(data: ActionData) {
flashError(R.string.msg_codeaction_failed)
}
protected fun ActionData.requireLanguageServer(): JavaLanguageServer {
return ILanguageServerRegistry.getDefault().getServer(JavaLanguageServer.SERVER_ID)
as JavaLanguageServer
}
protected fun ActionData.getLanguageClient(): ILanguageClient? {
return requireLanguageServer().client
}
protected fun ActionData.requireCompiler(): JavaCompilerService {
val module = IProjectManager.getInstance().findModuleForFile(requireFile(), false)
requireNotNull(module) {
"Cannot get compiler instance. Unable to find module for file: ${requireFile().name}"
}
return JavaCompilerProvider.get(module)
}
}
@@ -0,0 +1,200 @@
/*
* 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.java.actions
import android.content.Context
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.newDialogBuilder
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.flashInfo
import io.github.rosemoe.sora.widget.CodeEditor
import jdkx.lang.model.element.Modifier.STATIC
import openjdk.source.tree.ClassTree
import openjdk.source.tree.Tree.Kind.VARIABLE
import openjdk.source.tree.VariableTree
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
/**
* Any action that has to work with fields in the current class can inherit this action.
*
* @author Akash Yadav
*/
abstract class FieldBasedAction : BaseJavaCodeAction() {
companion object {
private val log = LoggerFactory.getLogger(FieldBasedAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (
!visible ||
!data.hasRequiredData(
com.itsaky.androidide.models.Range::class.java,
CodeEditor::class.java
) ||
IProjectManager.getInstance().rootProject == null
) {
markInvisible()
return
}
visible = true
enabled = true
}
override suspend fun execAction(data: ActionData): Any {
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
val module = IProjectManager.getInstance().findModuleForFile(file, false) ?: return Any()
return JavaCompilerProvider.get(module).compile(file).get { task ->
val triple = findFields(task, file, range)
val type = triple.second
val fields = triple.third
val fieldNames = fields.map { "${it.name}: ${it.type}" } // Get the names
log.debug("Found {} fields in class {}", fieldNames.size, type.simpleName)
return@get fieldNames
}
}
protected fun findFields(
task: CompileTask,
file: Path,
range: com.itsaky.androidide.models.Range
): Triple<FindTypeDeclarationAt, ClassTree, MutableList<VariableTree>> {
// 1-based line and column index
val startLine = range.start.line + 1
val startColumn = range.start.column + 1
val endLine = range.end.line + 1
val endColumn = range.end.column + 1
val lines = task.root().lineMap
val start = lines.getPosition(startLine.toLong(), startColumn.toLong())
val end = lines.getPosition(endLine.toLong(), endColumn.toLong())
if (start == (-1).toLong() || end == (-1).toLong()) {
throw CompletionException(
RuntimeException("Unable to find position for the given selection range")
)
}
val typeFinder = FindTypeDeclarationAt(task.task)
var type = typeFinder.scan(task.root(file), start)
if (type == null) {
type = typeFinder.scan(task.root(file), end)
}
if (type == null) {
throw CompletionException(
RuntimeException("Unable to find class declaration within cursor range")
)
}
val fields =
type.members
.filter { it.kind == VARIABLE }
.map { it as VariableTree }
.filter { !it.modifiers.flags.contains(STATIC) }
.toMutableList()
return Triple(typeFinder, type, fields)
}
@Suppress("UNCHECKED_CAST")
override fun postExec(data: ActionData, result: Any) {
if (result !is List<*>) {
log.error("Unable to find fields in the current class")
return
}
if (result.isEmpty()) {
flashInfo(data[Context::class.java]!!.getString(R.string.msg_no_fields_found))
return
}
onGetFields(result as List<String>, data)
}
/**
* Called when the fields of the current class are found. As this method is called inside
* [postExec], the current thread is the UI thread.
*/
abstract fun onGetFields(fields: List<String>, data: ActionData)
/**
* Shows the field selector dialog. Returns a [CompletableFuture] which is completed when the user
* confirms the selected fields.
*/
protected fun showFieldSelector(
fields: List<String>,
data: ActionData,
listener: OnFieldsSelectedListener?
) {
val names = fields.toTypedArray()
val checkedNames = mutableSetOf<String>()
val builder = newDialogBuilder(data)
builder.setTitle(data[Context::class.java]!!.getString(R.string.msg_select_fields))
builder.setMultiChoiceItems(names, BooleanArray(fields.size)) { _, which, checked ->
checkedNames.apply {
val item = names[which]
if (checked) {
add(item)
} else {
remove(item)
}
}
}
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
if (checkedNames.isEmpty()) {
flashInfo(data[Context::class.java]!!.getString(R.string.msg_no_fields_selected))
return@setPositiveButton
}
listener?.onFieldsSelected(checkedNames)
}
builder.setNegativeButton(android.R.string.cancel, null)
builder.show()
}
/** Listener to get callback when fields are selected by the user. */
fun interface OnFieldsSelectedListener {
/**
* Called when the user is done selecting fields.
*
* @param fields The selected field names.
*/
fun onFieldsSelected(fields: MutableSet<String>)
}
}
@@ -0,0 +1,76 @@
/*
* 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.java.actions
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.lsp.actions.IActionsMenuProvider
import com.itsaky.androidide.lsp.java.actions.common.CommentAction
import com.itsaky.androidide.lsp.java.actions.common.FindReferencesAction
import com.itsaky.androidide.lsp.java.actions.common.GoToDefinitionAction
import com.itsaky.androidide.lsp.java.actions.common.OrganizeImportsAction
import com.itsaky.androidide.lsp.java.actions.common.RemoveUnusedImportsAction
import com.itsaky.androidide.lsp.java.actions.common.UncommentAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.AddImportAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.AddThrowsAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.AutoFixImportsAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.CreateMissingMethodAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.FieldToBlockAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.ImplementAbstractMethodsAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveClassAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveMethodAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveUnusedThrowsAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.SuppressUncheckedWarningAction
import com.itsaky.androidide.lsp.java.actions.diagnostics.VariableToStatementAction
import com.itsaky.androidide.lsp.java.actions.generators.GenerateConstructorAction
import com.itsaky.androidide.lsp.java.actions.generators.GenerateMissingConstructorAction
import com.itsaky.androidide.lsp.java.actions.generators.GenerateSettersAndGettersAction
import com.itsaky.androidide.lsp.java.actions.generators.GenerateToStringMethodAction
import com.itsaky.androidide.lsp.java.actions.generators.OverrideSuperclassMethodsAction
/**
* Java code actions.
* @author Akash Yadav
*/
object JavaCodeActionsMenu : IActionsMenuProvider {
override val actions: List<ActionItem> =
listOf(
CommentAction(),
UncommentAction(),
GoToDefinitionAction(),
FindReferencesAction(),
AddImportAction(),
AutoFixImportsAction(),
ImplementAbstractMethodsAction(),
VariableToStatementAction(),
FieldToBlockAction(),
RemoveClassAction(),
RemoveMethodAction(),
RemoveUnusedThrowsAction(),
CreateMissingMethodAction(),
SuppressUncheckedWarningAction(),
AddThrowsAction(),
GenerateSettersAndGettersAction(),
OverrideSuperclassMethodsAction(),
GenerateMissingConstructorAction(),
GenerateConstructorAction(),
GenerateToStringMethodAction(),
RemoveUnusedImportsAction(),
OrganizeImportsAction()
)
}
@@ -0,0 +1,48 @@
/*
* 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.java.actions.common
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireEditor
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class CommentAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.commentLine"
override var label: String = ""
override val titleTextRes: Int = R.string.action_comment_line
override var requiresUIThread: Boolean = true
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.requireEditor()
val text = editor.text
val cursor = editor.cursor
text.beginBatchEdit()
for (line in cursor.leftLine..cursor.rightLine) {
text.insert(line, 0, "//")
}
text.endBatchEdit()
return true
}
override fun dismissOnAction() = false
}
@@ -0,0 +1,53 @@
/*
* 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.java.actions.common
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.editor.api.ILspEditor
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.resources.R
import io.github.rosemoe.sora.widget.CodeEditor
import java.io.File
/**
* Action that allows the user to find references to a variable, field, method or class.
*
* @author Akash Yadav
*/
class FindReferencesAction : BaseJavaCodeAction() {
override val titleTextRes: Int = R.string.action_find_references
override val id: String = "ide.editor.lsp.java.findReferences"
override var label: String = ""
override var requiresUIThread: Boolean = true
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !data.hasRequiredData(CodeEditor::class.java, File::class.java)) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val editor = data[CodeEditor::class.java]!!
return (editor as? ILspEditor)?.findReferences() ?: false
}
}
@@ -0,0 +1,54 @@
/*
* 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.java.actions.common
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.editor.api.ILspEditor
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.resources.R
import io.github.rosemoe.sora.widget.CodeEditor
import java.io.File
/**
* Action that allows the user to navigate to the definition of a variable, field, method, class,
* etc.
*
* @author Akash Yadav
*/
class GoToDefinitionAction : BaseJavaCodeAction() {
override val titleTextRes: Int = R.string.action_goto_definition
override val id: String = "ide.editor.lsp.java.gotoDefinition"
override var label: String = ""
override var requiresUIThread: Boolean = true
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !data.hasRequiredData(CodeEditor::class.java, File::class.java)) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val editor = data[CodeEditor::class.java]!!
return (editor as? ILspEditor)?.findDefinition() ?: false
}
}
@@ -0,0 +1,78 @@
package com.itsaky.androidide.lsp.java.actions.common
import com.google.googlejavaformat.java.FormatterException
import com.google.googlejavaformat.java.ImportOrderer
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireEditor
import com.itsaky.androidide.editor.api.IEditor
import com.itsaky.androidide.lsp.java.JavaLanguageServer
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.JavaServerSettings
import com.itsaky.androidide.resources.R.string
import io.github.rosemoe.sora.widget.CodeEditor
import org.slf4j.LoggerFactory
class OrganizeImportsAction : BaseJavaCodeAction() {
override val id: String = "lsp_java_organizeImports"
override var label: String = ""
override val titleTextRes: Int = string.action_organize_imports
companion object {
private val log = LoggerFactory.getLogger(OrganizeImportsAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
if (!data.hasRequiredData(CodeEditor::class.java)) {
markInvisible()
return
}
visible = true
enabled = true
}
override suspend fun execAction(data: ActionData): Any {
val watch = com.itsaky.androidide.utils.StopWatch("Organize imports")
return try {
val editor = data.requireEditor()
val content = editor.text
val server = data[JavaLanguageServer::class.java]
val settings = server!!.settings as JavaServerSettings
val output = ImportOrderer.reorderImports(content.toString(), settings.style)
watch.log()
output
} catch (e: FormatterException) {
log.error("Failed to reorder imports", e)
false
}
}
override fun postExec(data: ActionData, result: Any) {
super.postExec(data, result)
if (result is String) {
if (result.isNotEmpty()) {
val editor = data.requireEditor()
val cursor = editor.cursor.left()
editor.text.apply {
val endLine = getLine(lineCount - 1)
replace(0, 0, lineCount - 1, endLine.length + endLine.lineSeparator.length, result)
}
(editor as? IEditor?)?.also {
it.setSelectionAround(cursor)
editor.ensureSelectionVisible()
}
}
}
}
}
@@ -0,0 +1,60 @@
package com.itsaky.androidide.lsp.java.actions.common
import com.google.googlejavaformat.java.FormatterException
import com.google.googlejavaformat.java.RemoveUnusedImports
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireEditor
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.resources.R.string
import io.github.rosemoe.sora.widget.CodeEditor
import org.slf4j.LoggerFactory
class RemoveUnusedImportsAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.removeUnusedImports"
override var label: String = ""
override val titleTextRes: Int = string.action_remove_unused_imports
companion object {
private val log = LoggerFactory.getLogger(RemoveUnusedImportsAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
if (!data.hasRequiredData(CodeEditor::class.java)) {
markInvisible()
return
}
visible = true
enabled = true
}
override suspend fun execAction(data: ActionData): Any {
val watch = com.itsaky.androidide.utils.StopWatch("Remove unused imports")
return try {
val editor = data.requireEditor()
val content = editor.text
val output = RemoveUnusedImports.removeUnusedImports(content.toString())
watch.log()
output
} catch (e: FormatterException) {
log.error("Failed to remove unused imports", e)
false
}
}
override fun postExec(data: ActionData, result: Any) {
if (result is String && result.isNotEmpty()) {
val editor = data.requireEditor()
editor.setText(result)
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.java.actions.common
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireEditor
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class UncommentAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.uncommentLine"
override var label: String = ""
override val titleTextRes: Int = R.string.action_uncomment_line
override var requiresUIThread: Boolean = true
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.requireEditor()
val text = editor.text
val cursor = editor.cursor
text.beginBatchEdit()
for (line in cursor.leftLine..cursor.rightLine) {
val l = text.getLineString(line)
if (l.trim().startsWith("//")) {
val i = l.indexOf("//")
text.delete(line, i, line, i + 2)
}
}
text.endBatchEdit()
return true
}
override fun dismissOnAction() = false
}
@@ -0,0 +1,181 @@
/*
* 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.java.actions.diagnostics
import com.google.common.collect.Iterables.toArray
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.newDialogBuilder
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.AddImport
import com.itsaky.androidide.lsp.java.rewrite.Rewrite
import com.itsaky.androidide.lsp.models.CodeActionItem
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import jdkx.tools.Diagnostic
import jdkx.tools.JavaFileObject
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class AddImportAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.addImport"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.NOT_IMPORTED.id
override val titleTextRes: Int = R.string.action_import_classes
companion object {
private val log = LoggerFactory.getLogger(AddImportAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !data.hasRequiredData(DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code || diagnostic.extra !is Diagnostic<*>) {
markInvisible()
return
}
val file = data.requireFile()
val module =
IProjectManager.getInstance().findModuleForFile(file, false)
?: run {
markInvisible()
return
}
val compiler = JavaCompilerProvider.get(module)
@Suppress("UNCHECKED_CAST")
val jcDiagnostic =
JavaDiagnosticUtils.asJCDiagnostic(diagnostic.extra as Diagnostic<out JavaFileObject>)
if (jcDiagnostic == null) {
markInvisible()
return
}
val found =
jcDiagnostic.args[1]?.toString()?.let { compiler.findQualifiedNames(it, true).isNotEmpty() }
?: false
visible = found
enabled = found
}
override suspend fun execAction(data: ActionData): Any {
@Suppress("UNCHECKED_CAST")
val diagnostic =
JavaDiagnosticUtils.asUnwrapper(
data.get(DiagnosticItem::class.java)!!.extra as Diagnostic<out JavaFileObject>
)!!
val file = data.requireFile()
val module =
IProjectManager.getInstance().findModuleForFile(file, false)
?: run {
markInvisible()
return Any()
}
val compiler = JavaCompilerProvider.get(module)
val titles = mutableListOf<String>()
val rewrites = mutableListOf<AddImport>()
val simpleName = diagnostic.d.args[1]
for (name in compiler.publicTopLevelTypes()) {
var klass = name
if (klass.contains('/')) {
klass = klass.replace('/', '.')
}
if (!klass.endsWith(".$simpleName")) {
continue
}
titles.add(klass)
rewrites.add(AddImport(data.requirePath(), klass))
}
if (rewrites.isEmpty()) {
return false
}
return Pair(titles, rewrites)
}
@Suppress("UNCHECKED_CAST")
override fun postExec(data: ActionData, result: Any) {
if (result !is Pair<*, *>) {
return
}
val file = data.requireFile()
val module =
IProjectManager.getInstance().findModuleForFile(file, false)
?: run {
markInvisible()
return
}
val compiler = JavaCompilerProvider.get(module)
val client = data.getLanguageClient() ?: return
val actions = mutableListOf<CodeActionItem>()
val titles = result.first as List<String>
val rewrites = result.second as List<Rewrite>
for (index in rewrites.indices) {
val name = titles[index]
val rewrite = rewrites[index]
rewrite.asCodeActions(compiler, name)?.let { actions.add(it) }
}
when (actions.size) {
0 -> {
log.warn("No rewrites found. Cannot perform action")
}
1 -> {
client.performCodeAction(actions[0])
}
else -> {
val builder = newDialogBuilder(data)
builder.setTitle(label)
builder.setItems(toArray(titles, String::class.java)) { d, w ->
d.dismiss()
client.performCodeAction(actions[w])
}
builder.show()
}
}
}
}
@@ -0,0 +1,89 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.AddException
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class AddThrowsAction : BaseJavaCodeAction() {
override val id = "ide.editor.lsp.java.diagnostics.addThrows"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.NOT_THROWN.id
override val titleTextRes: Int = R.string.action_add_throws
companion object {
private val log = LoggerFactory.getLogger(AddThrowsAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !data.hasRequiredData(DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data[DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get { task ->
val needsThrow = CodeActionUtils.findMethod(task, diagnostic.range)
val exceptionName = CodeActionUtils.extractExceptionName(diagnostic.message)
return@get AddException(
needsThrow.className,
needsThrow.methodName,
needsThrow.erasedParameterTypes,
exceptionName
)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is AddException) {
log.warn("Unable to add 'throws' expression")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,204 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.R
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.utils.positionForImports
import com.itsaky.androidide.lsp.models.CodeActionItem
import com.itsaky.androidide.lsp.models.CodeActionKind
import com.itsaky.androidide.lsp.models.DocumentChange
import com.itsaky.androidide.lsp.models.TextEdit
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.flashInfo
import org.slf4j.LoggerFactory
import java.nio.file.Path
/**
* Analyzes the source file for unresolved names and tries to import all of them at once.
*
* @author Akash Yadav
*/
class AutoFixImportsAction : BaseJavaCodeAction() {
override val titleTextRes: Int = R.string.title_fix_imports
override val id: String = "ide.editor.lsp.java.diagnostics.autoFixImports"
override var label: String = ""
companion object {
private val log = LoggerFactory.getLogger(AutoFixImportsAction::class.java)
}
override suspend fun execAction(data: ActionData): Result {
val path = data.requirePath()
val compiler = data.requireCompiler()
return compiler.compile(path).get { task ->
val classes = mutableMapOf<String, List<String>>()
// find all unresolved simple names
unresolvedNames(path, task).forEach { simpleName ->
// if we have already looked for this simple name
// we do not need to look it up again
if (classes[simpleName] != null) return@forEach
// find classes with those names
compiler.findQualifiedNames(simpleName).let { names ->
// if we find classes with that specific simple name, map them to the simple name
if (names.isNotEmpty()) {
classes[simpleName] = names
}
}
}
// return the result
Result(getFileImports(task, path), classes)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is Result) {
log.error("Invalid result returned from execAction: {}", result)
return
}
if (result.classes.isEmpty()) {
flashInfo(R.string.msg_no_unresolved_classes)
return
}
// if there are multiple classes with same simple name
// ask the user to choose the appropriate class
if (result.classes.any { it.value.size > 1 }) {
finalizeClassNames(data, result)
} else {
performEdits(data, result)
}
}
private fun finalizeClassNames(data: ActionData, result: Result) {
var e: Map.Entry<String, List<String>>? = null
for (entry in result.classes) {
if (entry.value.size > 1) {
e = entry
break
}
}
if (e == null) {
performEdits(data, result)
return
}
val context = data.requireContext()
DialogUtils.newMaterialDialogBuilder(context)
.setCancelable(true)
.setItems(e.value.toTypedArray()) { dialog, which ->
dialog.dismiss()
result.classes[e.key] = listOf(e.value[which])
// once the user decides which class to import for this simple name,
// call this method again to see if there any other simple names with multiple options
finalizeClassNames(data, result)
}
.setTitle(context.getString(R.string.title_class_chooser, e.key))
.show()
}
private fun performEdits(data: ActionData, result: Result) {
val path = data.requirePath()
val compiler = data.requireCompiler()
val client =
data.getLanguageClient()
?: run {
log.warn("No language client found. Cannot perform edits.")
return
}
val classes = result.classes.mapNotNull { it.value.firstOrNull() }
if (classes.isEmpty()) {
flashInfo(R.string.msg_no_unresolved_classes)
return
}
val insertText = StringBuilder()
if (result.fileImports.isEmpty() && classes.isNotEmpty()) {
// if there are no file imports, the new imports will be added just after the package
// declaration. To avoid this, add a new line before the imports
insertText.append("\n")
}
for (klass in classes) {
insertText.append("import ${klass};\n")
}
val position = compiler.compile(path).get { positionForImports(classes[0], it) }
val change = DocumentChange()
change.file = path
change.edits = listOf(TextEdit(Range.pointRange(position), insertText.toString()))
val action = CodeActionItem()
action.title = data.requireContext().getString(R.string.title_fix_imports)
action.kind = CodeActionKind.QuickFix
action.changes = listOf(change)
client.performCodeAction(action)
}
/**
* Walks through the diagnostics of the compilation task, looks for [DiagnosticCode.NOT_IMPORTED]
* errors and returns a list of simple names of all not imported classes.
*/
private fun unresolvedNames(file: Path, task: CompileTask): List<String> {
val names = mutableListOf<String>()
var docContents: CharSequence? = null
val diagnostics =
task.diagnostics.filter {
it.source.toUri() == file.toUri() && it.code == DiagnosticCode.NOT_IMPORTED.id
}
for (diagnostic in diagnostics) {
val content =
try {
docContents ?: diagnostic.source.getCharContent(true).also { docContents = it }
} catch (e: Exception) {
log.error("Failed to get contents of file {}", file, e)
continue
}
val name =
content.subSequence(diagnostic.startPosition.toInt(), diagnostic.endPosition.toInt())
names.add(name.toString())
}
return names
}
private fun getFileImports(task: CompileTask, file: Path): Set<String> {
return task.root(file).imports.map { it.qualifiedIdentifier }.map { it.toString() }.toSet()
}
inner class Result(val fileImports: Set<String>, val classes: MutableMap<String, List<String>>)
}
@@ -0,0 +1,84 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.CreateMissingMethod
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class CreateMissingMethodAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.createMissingMethod"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.MISSING_METHOD.id
override val titleTextRes: Int = R.string.action_create_missing_method
companion object {
private val log = LoggerFactory.getLogger(CreateMissingMethodAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (
!visible ||
!data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
) {
markInvisible()
return
}
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get {
CreateMissingMethod(file, findPosition(it, diagnostic.range.start))
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is CreateMissingMethod) {
log.warn("Unable to create missing method")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,87 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.ConvertFieldToBlock
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class FieldToBlockAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.fieldToBlock"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.UNUSED_FIELD.id
override val titleTextRes: Int = R.string.action_convert_to_block
companion object {
private val log = LoggerFactory.getLogger(FieldToBlockAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
if (!data.hasRequiredData(DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val diagnostic = data[DiagnosticItem::class.java]!!
val file = data.requirePath()
return compiler.compile(file).get {
ConvertFieldToBlock(file, findPosition(it, diagnostic.range.start))
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is ConvertFieldToBlock) {
log.warn("Unable to convert field to block")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,92 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.ImplementAbstractMethods
import com.itsaky.androidide.resources.R
import jdkx.tools.Diagnostic
import jdkx.tools.JavaFileObject
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class ImplementAbstractMethodsAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.implementAbstractMethods"
override var label: String = ""
private var diagnosticCode = DiagnosticCode.DOES_NOT_OVERRIDE_ABSTRACT.id
override val titleTextRes: Int = R.string.action_implement_abstract_methods
companion object {
private val log = LoggerFactory.getLogger(ImplementAbstractMethodsAction::class.java)
}
@Suppress("UNCHECKED_CAST")
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
if (!data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code || diagnostic.extra !is Diagnostic<*>) {
markInvisible()
return
}
JavaDiagnosticUtils.asJCDiagnostic(diagnostic.extra as Diagnostic<out JavaFileObject>)
?: run {
markInvisible()
return
}
visible = true
enabled = true
}
@Suppress("UNCHECKED_CAST")
override suspend fun execAction(data: ActionData): Any {
val diagnostic =
JavaDiagnosticUtils.asJCDiagnostic(
data.get(
com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!.extra as Diagnostic<out JavaFileObject>
)
return ImplementAbstractMethods(diagnostic!!)
}
override fun postExec(data: ActionData, result: Any) {
if (result !is ImplementAbstractMethods) {
log.warn("Unable to perform action. Invalid result from execAction(..)")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,84 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.RemoveClass
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class RemoveClassAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.removeClass"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.UNUSED_CLASS.id
override val titleTextRes: Int = R.string.action_remove_class
companion object {
private val log = LoggerFactory.getLogger(RemoveClassAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !data.hasRequiredData(
com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
) {
markInvisible()
return
}
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get {
RemoveClass(file, findPosition(it, diagnostic.range.start))
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is RemoveClass) {
log.warn("Unable to remove class")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,89 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.RemoveMethod
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findMethod
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class RemoveMethodAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.removeMethod"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.UNUSED_METHOD.id
override val titleTextRes: Int = R.string.action_remove_method
companion object {
private val log = LoggerFactory.getLogger(RemoveMethodAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !data.hasRequiredData(
com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
) {
markInvisible()
return
}
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get {
val unusedMethod = findMethod(it, diagnostic.range)
RemoveMethod(
unusedMethod.className,
unusedMethod.methodName,
unusedMethod.erasedParameterTypes
)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is RemoveMethod) {
log.warn("Unable to remove method")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,92 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.RemoveException
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class RemoveUnusedThrowsAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.removeUnusedThrows"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.UNUSED_THROWS.id
override val titleTextRes: Int = R.string.action_remove_unused_throws
companion object {
private val log = LoggerFactory.getLogger(RemoveUnusedThrowsAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (
!visible ||
!data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
) {
markInvisible()
return
}
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val d = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get { task ->
val notThrown = CodeActionUtils.extractNotThrownExceptionName(d.message)
val methodWithExtraThrow = CodeActionUtils.findMethod(task, d.range)
return@get RemoveException(
methodWithExtraThrow.className,
methodWithExtraThrow.methodName,
methodWithExtraThrow.erasedParameterTypes,
notThrown
)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is RemoveException) {
log.warn("Unable to remove unused throws")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,88 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.AddSuppressWarningAnnotation
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class SuppressUncheckedWarningAction : BaseJavaCodeAction() {
override val id = "ide.editor.lsp.java.diagnostics.suppressUncheckedWarning"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.UNCHECKED.id
override val titleTextRes: Int = R.string.action_suppress_unchecked_warning
companion object {
private val log = LoggerFactory.getLogger(SuppressUncheckedWarningAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible || !data.hasRequiredData(
com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
) {
markInvisible()
return
}
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get { task ->
val warnedMethod = CodeActionUtils.findMethod(task, diagnostic.range)
return@get AddSuppressWarningAnnotation(
warnedMethod.className,
warnedMethod.methodName,
warnedMethod.erasedParameterTypes
)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is AddSuppressWarningAnnotation) {
log.warn("Unable to suppress 'unchecked' warning")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,89 @@
/*
* 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.java.actions.diagnostics
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.ConvertVariableToStatement
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class VariableToStatementAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.diagnostics.variableToStatement"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.UNUSED_LOCAL.id
override val titleTextRes: Int = R.string.action_convert_to_statement
companion object {
private val log = LoggerFactory.getLogger(VariableToStatementAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
if (!data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
markInvisible()
return
}
val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
visible = true
enabled = true
}
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val path = data.requirePath()
return compiler.compile(path).get {
ConvertVariableToStatement(path, findPosition(it, diagnostic.range.start))
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is ConvertVariableToStatement) {
log.warn("Unable to convert variable to statement")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,176 @@
/*
* 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.java.actions.generators
import android.content.Context
import com.blankj.utilcode.util.ThreadUtils
import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.body.ConstructorDeclaration
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.FieldBasedAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.java.utils.ShortTypePrinter.NO_PACKAGE
import com.itsaky.androidide.preferences.utils.indentationString
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.utils.flashError
import io.github.rosemoe.sora.widget.CodeEditor
import openjdk.source.tree.ClassTree
import openjdk.source.tree.VariableTree
import openjdk.source.util.TreePath
import openjdk.tools.javac.api.JavacTrees
import openjdk.tools.javac.code.Symbol.ClassSymbol
import openjdk.tools.javac.code.Symbol.VarSymbol
import openjdk.tools.javac.code.Type
import openjdk.tools.javac.tree.JCTree
import openjdk.tools.javac.tree.TreeInfo
import openjdk.tools.javac.util.ListBuffer
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletableFuture
/**
* Allows the user to select fields and generate a constructor which has parameters same as the
* selected fields.
*
* The generated constructor has statements which assigns the fields to the parameter types.
*
* @author Akash Yadav
*/
class GenerateConstructorAction : FieldBasedAction() {
override val titleTextRes: Int = string.action_generate_constructor
override val id: String = "ide.editor.lsp.java.generator.constructor"
override var label: String = ""
companion object {
private val log = LoggerFactory.getLogger(GenerateConstructorAction::class.java)
}
override fun onGetFields(fields: List<String>, data: ActionData) {
showFieldSelector(fields, data) { selected ->
CompletableFuture.runAsync { generateConstructor(data, selected) }
.whenComplete { _, error ->
if (error != null) {
log.error("Unable to generate constructor for the selected fields", error)
flashError(string.msg_cannot_generate_constructor)
}
}
}
}
private fun generateConstructor(data: ActionData, selected: MutableSet<String>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
compiler.compile(file).run { task ->
val triple = findFields(task, file, range)
val typeFinder = triple.first
val type = triple.second
val fields = triple.third
fields.removeIf { !selected.contains("${it.name}: ${it.type}") }
log.debug("Creating toString() method with fields: {}", fields.map { it.name })
generateForFields(data, task, type, fields.map { TreePath(typeFinder.path, it) })
}
}
private fun generateForFields(
data: ActionData,
task: CompileTask,
type: ClassTree,
paths: List<TreePath>
) {
val editor = data[CodeEditor::class.java]!!
val trees = JavacTrees.instance(task.task)
val sym = TreeInfo.symbolFor(type as JCTree) as ClassSymbol
val varTypes = mapTypes(paths)
val varNames = paths.map { it.leaf as VariableTree }.map { it.name.toString() }
if (paths.isEmpty() || trees.findConstructor(sym, varTypes) != null) {
log.warn(
"A constructor with same parameter types is already available in class {}", type.simpleName
)
flashError(data[Context::class.java]!!.getString(string.msg_constructor_available))
return
}
val stopWatch = com.itsaky.androidide.utils.StopWatch("generateConstructorForFields()")
val constructor =
newConstructor(type.simpleName.toString(), varTypes.toTypedArray(), varNames.toTypedArray())
val body = constructor.createBody()
for (varName in varNames) {
body.addStatement(StaticJavaParser.parseStatement("this.$varName = $varName;"))
}
stopWatch.lap("Constructor generated")
log.info("Inserting constructor into editor...")
val insertAt = EditHelper.insertAfter(task.task, task.root(), paths.last().leaf)
val indent = EditHelper.indent(task.task, task.root(), paths.last().leaf)
var text = constructor.toString()
text = text.replace("\n", "\n${indentationString(indent)}")
text += "\n"
ThreadUtils.runOnUiThread {
editor.text.insert(insertAt.line, insertAt.column, text)
editor.formatCodeAsync()
stopWatch.log()
}
}
private fun newConstructor(
name: String,
paramTypes: Array<Type>,
paramNames: Array<String>
): ConstructorDeclaration {
val constructor = ConstructorDeclaration()
constructor.setName(name)
constructor.addModifier(Modifier.Keyword.PUBLIC)
for (i in paramTypes.indices) {
val paramType = paramTypes[i]
val paramName = paramNames[i]
constructor.addParameter(NO_PACKAGE.print(paramType), paramName)
}
return constructor
}
private fun mapTypes(paths: List<TreePath>): openjdk.tools.javac.util.List<Type> {
val buffer = ListBuffer<Type>()
for (path in paths) {
val leaf = path.leaf
val sym = TreeInfo.symbolFor(leaf as JCTree) as VarSymbol
buffer.add(sym.type)
}
return buffer.toList()
}
}
@@ -0,0 +1,85 @@
/*
* 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.java.actions.generators
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
import com.itsaky.androidide.lsp.java.rewrite.GenerateRecordConstructor
import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class GenerateMissingConstructorAction : BaseJavaCodeAction() {
override val id = "ide.editor.lsp.java.generator.missingConstructor"
override var label: String = ""
private val diagnosticCode = DiagnosticCode.MISSING_CONSTRUCTOR.id
override val titleTextRes: Int = R.string.action_generate_missing_constructor
companion object {
private val log = LoggerFactory.getLogger(GenerateMissingConstructorAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (
!visible ||
!data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
) {
markInvisible()
return
}
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
if (diagnosticCode != diagnostic.code) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get { task ->
val needsConstructor =
CodeActionUtils.findClassNeedingConstructor(task, diagnostic.range) ?: return@get false
return@get GenerateRecordConstructor(needsConstructor)
}
}
override fun postExec(data: ActionData, result: Any) {
if (result !is GenerateRecordConstructor) {
log.warn("Unable to generate constructor")
return
}
performCodeAction(data, result)
}
}
@@ -0,0 +1,196 @@
/*
* 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.java.actions.generators
import android.content.Context
import com.blankj.utilcode.util.ThreadUtils
import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.body.MethodDeclaration
import com.github.javaparser.ast.expr.SimpleName
import com.github.javaparser.ast.stmt.BlockStmt
import com.github.javaparser.ast.type.Type
import com.github.javaparser.ast.type.VoidType
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.FieldBasedAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.java.utils.JavaParserUtils
import com.itsaky.androidide.lsp.java.utils.TypeUtils.toType
import com.itsaky.androidide.preferences.internal.EditorPreferences
import com.itsaky.androidide.preferences.utils.indentationString
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.flashError
import io.github.rosemoe.sora.widget.CodeEditor
import jdkx.lang.model.element.Modifier.FINAL
import jdkx.lang.model.element.VariableElement
import openjdk.source.tree.ClassTree
import openjdk.source.util.TreePath
import openjdk.source.util.Trees
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletableFuture
/**
* Allows the user to select fields from the current class, then generates setters and getters for
* the selected fields.
*
* @author Akash Yadav
*/
class GenerateSettersAndGettersAction : FieldBasedAction() {
override val id: String = "ide.editor.lsp.java.generator.settersAndGetters"
override var label: String = ""
override val titleTextRes: Int = R.string.action_generate_setters_getters
companion object {
private val log = LoggerFactory.getLogger(GenerateSettersAndGettersAction::class.java)
}
override fun onGetFields(fields: List<String>, data: ActionData) {
showFieldSelector(fields, data) { checkedNames ->
CompletableFuture.runAsync { generateForFields(data, checkedNames) }
.whenComplete {
_, error,
->
if (error != null) {
log.error("Unable to generate setters and getters", error)
ThreadUtils.runOnUiThread {
flashError(
data[Context::class.java]!!.getString(R.string.msg_cannot_generate_setters_getters)
)
}
return@whenComplete
}
}
}
}
private fun generateForFields(data: ActionData, names: MutableSet<String>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
compiler.compile(file).run { task ->
val triple = findFields(task, file, range)
val typeFinder = triple.first
val type = triple.second
val fields = triple.third
fields.removeIf { !names.contains("${it.name}: ${it.type}") }
log.debug("Creating setters/getters for fields: {}", fields.map { it.name })
generateForFields(data, task, type, fields.map { TreePath(typeFinder.path, it) })
}
}
private fun generateForFields(
data: ActionData,
task: CompileTask,
type: ClassTree,
paths: List<TreePath>,
) {
val file = data.requirePath()
val editor = data[CodeEditor::class.java]!!
val trees = Trees.instance(task.task)
val insert = EditHelper.insertAtEndOfClass(task.task, task.root(file), type)
val sb = StringBuilder()
for (path in paths) {
val element = trees.getElement(path) ?: continue
if (element !is VariableElement) {
continue
}
val leaf = path.leaf
val indent = EditHelper.indent(task.task, task.root(file), leaf) + EditorPreferences.tabSize
sb.append(createGetter(element, indent))
if (!element.modifiers.contains(FINAL)) {
sb.append(createSetter(element, indent))
}
}
ThreadUtils.runOnUiThread {
editor.text.insert(insert.line, insert.column, sb)
editor.formatCodeAsync()
}
}
private fun createGetter(variable: VariableElement, indent: Int): String {
val name = variable.simpleName.toString()
val method =
createMethod(variable, "get", toType(variable.asType())) { _, body ->
body.addStatement(createReturnStmt(name))
}
var text = "\n" + JavaParserUtils.prettyPrint(method) { false }
text = text.replace("\n", "\n${indentationString(indent)}")
return text
}
private fun createReturnStmt(name: String) = StaticJavaParser.parseStatement("return this.$name;")
private fun createSetter(variable: VariableElement, indent: Int): String {
val name: String = variable.simpleName.toString()
val method =
createMethod(variable, "set", VoidType()) { method, body ->
method.addParameter(toType(variable.asType()), name)
body.addStatement(createAssignmentStmt(name))
}
var text = "\n" + JavaParserUtils.prettyPrint(method) { false }
text = text.replace("\n", "\n${indentationString(indent)}")
return text
}
private fun createMethod(
variable: VariableElement,
prefix: String,
returnType: Type,
vararg modifiers: Modifier.Keyword = arrayOf(Modifier.Keyword.PUBLIC),
block: (MethodDeclaration, BlockStmt) -> Unit
): MethodDeclaration {
val name = variable.simpleName.toString()
val method = MethodDeclaration()
val body = method.createBody()
method.name = SimpleName(createName(name, prefix))
method.type = returnType
method.addModifier(*modifiers)
block(method, body)
return method
}
private fun createAssignmentStmt(name: String) =
StaticJavaParser.parseStatement("this.$name = $name;")
private fun createName(name: String, prefix: String): String {
val sb = StringBuilder(name)
sb.setCharAt(0, Character.toUpperCase(sb[0]))
sb.insert(0, prefix)
return sb.toString()
}
}
@@ -0,0 +1,189 @@
/*
* 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.java.actions.generators
import android.content.Context
import com.blankj.utilcode.util.ThreadUtils
import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.Modifier
import com.github.javaparser.ast.body.MethodDeclaration
import com.github.javaparser.ast.stmt.ReturnStmt
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.FieldBasedAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.preferences.internal.EditorPreferences
import com.itsaky.androidide.preferences.utils.indentationString
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.utils.flashError
import io.github.rosemoe.sora.widget.CodeEditor
import jdkx.lang.model.element.VariableElement
import openjdk.source.tree.ClassTree
import openjdk.source.tree.VariableTree
import openjdk.source.util.TreePath
import openjdk.tools.javac.api.JavacTrees
import openjdk.tools.javac.code.Symbol.ClassSymbol
import openjdk.tools.javac.code.Symbol.MethodSymbol
import openjdk.tools.javac.tree.JCTree
import openjdk.tools.javac.tree.TreeInfo
import openjdk.tools.javac.util.Names
import org.slf4j.LoggerFactory
import java.util.concurrent.CompletableFuture
/**
* Generates the `toString()` method for the current class.
*
* @author Akash Yadav
*/
class GenerateToStringMethodAction : FieldBasedAction() {
override val titleTextRes: Int = R.string.action_generate_toString
override val id: String = "ide.editor.lsp.java.generator.toString"
override var label: String = ""
companion object {
private val log = LoggerFactory.getLogger(GenerateToStringMethodAction::class.java)
}
override fun onGetFields(fields: List<String>, data: ActionData) {
showFieldSelector(fields, data) { selected ->
CompletableFuture.runAsync { generateToString(data, selected) }
.whenComplete { _, error ->
if (error != null) {
log.error("Unable to generate toString() implementation", error)
ThreadUtils.runOnUiThread {
flashError(
data[Context::class.java]!!.getString(R.string.msg_cannot_generate_toString)
)
}
return@whenComplete
}
}
}
}
private fun generateToString(data: ActionData, selected: MutableSet<String>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
compiler.compile(file).run { task ->
val triple = findFields(task, file, range)
val typeFinder = triple.first
val type = triple.second
val fields = triple.third
fields.removeIf { !selected.contains("${it.name}: ${it.type}") }
log.debug("Creating toString() method with fields: {}", fields.map { it.name })
generateForFields(data, task, type, fields.map { TreePath(typeFinder.path, it) })
}
}
private fun generateForFields(
data: ActionData,
task: CompileTask,
type: ClassTree,
paths: List<TreePath>
) {
if (isToStringOverridden(task, type)) {
ThreadUtils.runOnUiThread {
flashError(data[Context::class.java]!!.getString(string.msg_toString_overridden))
}
log.warn("toString() method has already been overridden in class {}", type.simpleName)
return
}
val file = data.requirePath()
val editor = data[CodeEditor::class.java]!!
val trees = JavacTrees.instance(task.task)
val indent = EditHelper.indent(task.task, task.root(), type) + EditorPreferences.tabSize
val insert = EditHelper.insertAtEndOfClass(task.task, task.root(file), type)
val string = StringBuilder()
var isFirst = true
string.append("\"")
string.append(type.simpleName)
string.append('[')
for (path in paths) {
val element = trees.getElement(path) ?: continue
if (element !is VariableElement) {
continue
}
val leaf = path.leaf as VariableTree
if (!isFirst) {
string.append(", ")
}
string.append(leaf.name)
string.append("=")
string.append("\" + ")
string.append(leaf.name)
string.append(" + \"")
// "ClassName[field1=' + field1 + ', field2=' + field2 + ']"
isFirst = false
}
string.append("]\"")
val method = overrideToString()
val body = method.createBody()
body.addStatement(createReturnStatement(string.toString()))
var text = "\n" + method.toString()
text = text.replace("\n", "\n${indentationString(indent)}")
text += "\n"
ThreadUtils.runOnUiThread {
editor.text.insert(insert.line, insert.column, text)
editor.formatCodeAsync()
}
}
private fun isToStringOverridden(task: CompileTask, type: ClassTree): Boolean {
val names = Names.instance(task.task.context)
val sym = TreeInfo.symbolFor(type as JCTree) as ClassSymbol
val toStrings =
sym.members().getSymbolsByName(names.toString).filterIsInstance<MethodSymbol>().filter {
it.params.isEmpty()
}
return toStrings.isNotEmpty()
}
private fun createReturnStatement(string: String): ReturnStmt {
return StaticJavaParser.parseStatement("return $string;") as ReturnStmt
}
private fun overrideToString(): MethodDeclaration {
val method = MethodDeclaration()
method.addMarkerAnnotation("Override")
method.addModifier(Modifier.Keyword.PUBLIC)
method.setType("String")
method.setName("toString")
return method
}
}
@@ -0,0 +1,336 @@
/*
* 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.java.actions.generators
import android.content.Context
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.newDialogBuilder
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.actions.requirePath
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
import com.itsaky.androidide.lsp.java.compiler.CompileTask
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider
import com.itsaky.androidide.lsp.java.parser.ParseTask
import com.itsaky.androidide.lsp.java.rewrite.AddImport
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.java.utils.FindHelper
import com.itsaky.androidide.lsp.java.utils.JavaParserUtils
import com.itsaky.androidide.lsp.java.utils.MethodPtr
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.preferences.internal.EditorPreferences
import com.itsaky.androidide.preferences.utils.indentationString
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.flashError
import io.github.rosemoe.sora.widget.CodeEditor
import jdkx.lang.model.element.ElementKind
import jdkx.lang.model.element.ExecutableElement
import jdkx.lang.model.element.Modifier
import jdkx.lang.model.element.TypeElement
import jdkx.lang.model.type.DeclaredType
import jdkx.lang.model.type.ExecutableType
import jdkx.tools.JavaFileObject
import openjdk.source.tree.MethodTree
import openjdk.source.util.Trees
import org.slf4j.LoggerFactory
import java.util.Arrays
import java.util.Optional
import java.util.concurrent.CompletableFuture
/**
* Allows the user to override multiple methods from superclass at once.
*
* @author Akash Yadav
*/
class OverrideSuperclassMethodsAction : BaseJavaCodeAction() {
override val titleTextRes: Int = R.string.action_override_superclass_methods
override val id: String = "ide.editor.lsp.java.generator.overrideSuperclassMethods"
override var label: String = ""
private var position: Long = -1
companion object {
private val log = LoggerFactory.getLogger(OverrideSuperclassMethodsAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (
!visible ||
!data.hasRequiredData(
com.itsaky.androidide.models.Range::class.java,
CodeEditor::class.java
)
) {
markInvisible()
return
}
visible = true
enabled = true
}
override suspend fun execAction(data: ActionData): Any {
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
val file = data.requirePath()
return compiler.compile(file).get { task ->
// 1-based line and column index
val startLine = range.start.line + 1
val startColumn = range.start.column + 1
val endLine = range.end.line + 1
val endColumn = range.end.column + 1
val lines = task.root().lineMap
val start = lines.getPosition(startLine.toLong(), startColumn.toLong())
val end = lines.getPosition(endLine.toLong(), endColumn.toLong())
if (start == (-1).toLong() || end == (-1).toLong()) {
return@get false
}
this.position = start
val typeFinder = FindTypeDeclarationAt(task.task)
var type = typeFinder.scan(task.root(file), start)
if (type == null) {
type = typeFinder.scan(task.root(file), end)
position = end
}
if (type == null) {
return@get false
}
val overridable = mutableListOf<MethodPtr>()
val trees = Trees.instance(task.task)
val elements = task.task.elements
val classPath = typeFinder.path
val element = trees.getElement(classPath) as TypeElement
for (member in elements.getAllMembers(element)) {
if (member.modifiers.contains(Modifier.FINAL) || member.kind != ElementKind.METHOD) {
continue
}
val method = member as ExecutableElement
val methodSource = member.getEnclosingElement() as TypeElement
if (
methodSource.qualifiedName.contentEquals("java.lang.Object") || methodSource == element
) {
continue
}
val pointer = MethodPtr(task.task, method)
overridable.add(pointer)
}
return@get overridable
}
}
@Suppress("UNCHECKED_CAST")
override fun postExec(data: ActionData, result: Any) {
if (result !is List<*> || result.isEmpty() || position < 0) {
log.warn("Unable to find any overridable method")
flashError(data[Context::class.java]!!.getString(R.string.msg_no_overridable_methods))
return
}
val methods = (result as List<MethodPtr>).sortedBy { it.methodName }
val checkedMethods = mutableListOf<MethodPtr>()
val names = mapMethodPointers(methods)
val builder = newDialogBuilder(data)
builder.setTitle(data[Context::class.java]!!.getString(R.string.msg_select_methods))
builder.setMultiChoiceItems(names, BooleanArray(names.size)) { _, which, isChecked ->
checkedMethods.apply {
if (isChecked) {
add(methods[which])
} else {
remove(methods[which])
}
}
}
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
if (checkedMethods.isEmpty()) {
flashError(data[Context::class.java]!!.getString(R.string.msg_no_methods_selected))
return@setPositiveButton
}
CompletableFuture.runAsync { overrideMethods(data, checkedMethods) }
.whenComplete {
_, error,
->
if (error != null) {
log.error("An error occurred overriding methods")
ThreadUtils.runOnUiThread {
flashError(
data[Context::class.java]!!.getString(R.string.msg_cannot_override_methods)
)
}
}
}
}
builder.setNegativeButton(android.R.string.cancel, null)
builder.show()
}
private fun overrideMethods(data: ActionData, checkedMethods: MutableList<MethodPtr>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
val file = data.requirePath()
compiler.compile(file).run { task ->
val types = task.task.types
val trees = Trees.instance(task.task)
val sb = StringBuilder()
val imports = mutableSetOf<String>()
val typeFinder = FindTypeDeclarationAt(task.task)
val classTree = typeFinder.scan(task.root(), position)
val thisClass = trees.getElement(typeFinder.path) as TypeElement
val indent = EditHelper.indent(task.task, task.root(), classTree) + EditorPreferences.tabSize
val fileImports = task.root(file).imports.map { it.qualifiedIdentifier.toString() }.toSet()
val filePackage = task.root(file).`package`.packageName.toString()
for (pointer in checkedMethods) {
val superMethod =
FindHelper.findMethod(
task,
pointer.className,
pointer.methodName,
pointer.erasedParameterTypes
) ?: continue
val thisDeclaredType = thisClass.asType() as DeclaredType
val executableType = types.asMemberOf(thisDeclaredType, superMethod) as ExecutableType
val source = findSource(compiler, task, superMethod)
val method =
if (source != null) {
JavaParserUtils.printMethod(superMethod, executableType, source)
} else {
JavaParserUtils.printMethod(superMethod, executableType, superMethod)
}
val newImports = JavaParserUtils.collectImports(executableType)
sb.append("\n")
sb.append(method.toString())
sb.replace(Regex(Regex.escape("\n")), "\n${indentationString(indent)}")
sb.append("\n")
newImports.removeIf {
it.startsWith("java.lang.") || it.startsWith(filePackage) || fileImports.contains(it)
}
imports.addAll(newImports)
}
ThreadUtils.runOnUiThread {
performEdits(
data,
sb,
imports,
EditHelper.insertAtEndOfClass(task.task, task.root(file), classTree)
)
}
}
}
private fun performEdits(
data: ActionData,
sb: StringBuilder,
imports: MutableSet<String>,
position: Position,
) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
val editor = data[CodeEditor::class.java]!!
val file = data.requirePath()
val text = editor.text
text.beginBatchEdit()
text.insert(position.line, position.column, sb)
for (name in imports) {
val rewrite = AddImport(file, name)
val edits = rewrite.rewrite(compiler)[file]
if (edits.isNullOrEmpty()) {
continue
}
for (edit in edits) {
if (edit.range.start == edit.range.end) {
text.insert(edit.range.start.line, edit.range.start.column, edit.newText)
} else {
text.replace(
edit.range.start.line,
edit.range.start.column,
edit.range.end.line,
edit.range.end.column,
edit.newText
)
}
}
}
text.endBatchEdit()
editor.formatCodeAsync()
}
private fun mapMethodPointers(methods: List<MethodPtr>): Array<CharSequence> {
return methods
.map { Arrays.toString(it.simplifiedErasedParameterTypes) }
.map {
val arr = it.toCharArray()
arr[0] = '('
arr[arr.size - 1] = ')'
String(arr)
}
.mapIndexed { index, params -> "${methods[index].methodName}$params" }
.toTypedArray()
}
private fun findSource(
compiler: CompilerProvider,
task: CompileTask,
method: ExecutableElement,
): MethodTree? {
val superClass = method.enclosingElement as TypeElement
val superClassName = superClass.qualifiedName.toString()
val methodName = method.simpleName.toString()
val erasedParameterTypes = FindHelper.erasedParameterTypes(task, method)
val sourceFile: Optional<JavaFileObject> = compiler.findAnywhere(superClassName)
if (!sourceFile.isPresent) return null
val parse: ParseTask = compiler.parse(sourceFile.get())
return FindHelper.findMethod(parse, superClassName, methodName, erasedParameterTypes)
}
}
@@ -0,0 +1,38 @@
/*
* 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.java.compiler
import openjdk.source.tree.CompilationUnitTree
import openjdk.tools.javac.api.JavacTaskImpl
import java.util.function.*
/**
* A compilation task processor process the [JavacTaskImpl]. Usually, a processor decides what files
* should be parsed and analyzed.
*
* @author Akash Yadav
*/
fun interface CompilationTaskProcessor {
/**
* Process the given [JavacTaskImpl]. The processor is responsible for parsing and analyzing the
* task. For each parsed [CompilationUnitTree], [processCompilationUnit] must be called.
*/
@Throws(Throwable::class)
fun process(task: JavacTaskImpl, processCompilationUnit: Consumer<CompilationUnitTree>)
}
@@ -0,0 +1,275 @@
/*
* 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.java.compiler;
import static com.itsaky.androidide.javac.config.JavacConfigProvider.PROP_ANDROIDIDE_JAVA_HOME;
import static com.itsaky.androidide.javac.config.JavacConfigProvider.disableModules;
import static com.itsaky.androidide.javac.config.JavacConfigProvider.enableModules;
import static com.itsaky.androidide.javac.config.JavacConfigProvider.setLatestSourceVersion;
import static com.itsaky.androidide.javac.config.JavacConfigProvider.setLatestSupportedSourceVersion;
import static com.itsaky.androidide.utils.Environment.JAVA_HOME;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import com.itsaky.androidide.builder.model.IJavaCompilerSettings;
import com.itsaky.androidide.javac.services.compiler.ReusableBorrow;
import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
import com.itsaky.androidide.lsp.java.visitors.MethodRangeScanner;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.util.StringSearch;
import com.itsaky.androidide.tooling.api.ProjectType;
import com.itsaky.androidide.utils.ClassTrie;
import com.itsaky.androidide.utils.SourceClassTrie;
import com.itsaky.androidide.utils.StopWatch;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import jdkx.lang.model.SourceVersion;
import jdkx.tools.Diagnostic;
import jdkx.tools.JavaFileObject;
import openjdk.source.tree.CompilationUnitTree;
import openjdk.source.util.TreePath;
import openjdk.tools.javac.api.ClientCodeWrapper;
import openjdk.tools.javac.api.JavacTaskImpl;
import openjdk.tools.javac.code.Kinds;
import openjdk.tools.javac.util.JCDiagnostic;
public class CompileBatch implements AutoCloseable {
public static final String DEFAULT_COMPILER_SOURCE_AND_TARGET_VERSION = "11";
protected final JavaCompilerService parent;
protected final ReusableBorrow borrow;
protected final JavacTaskImpl task;
protected final List<CompilationUnitTree> roots;
protected final Map<String, List<Pair<Range, TreePath>>> methodPositions = new HashMap<>();
protected DiagnosticListenerImpl diagnosticListener;
/** Indicates the task that requested the compilation is finished with it. */
boolean closed;
CompileBatch(
JavaCompilerService parent,
Collection<? extends JavaFileObject> files,
CompilationRequest compilationRequest) {
this.parent = parent;
this.borrow = batchTask(parent, files);
this.task = borrow.task;
this.roots = new ArrayList<>();
final var context = task.getContext();
final var config = JavaCompilerConfig.instance(context);
config.setFiles(files);
if (compilationRequest.configureContext != null) {
compilationRequest.configureContext.accept(context);
}
Objects.requireNonNull(compilationRequest, "A task processor is required");
try {
compilationRequest.compilationTaskProcessor.process(borrow.task, this::processCompilationUnit);
} catch (Throwable e) {
throw new RuntimeException(e);
}
config.setFiles(null);
}
private void processCompilationUnit(final CompilationUnitTree root) {
roots.add(root);
// updatePositions(root, false);
}
void updatePositions(CompilationUnitTree tree, boolean allowDuplicate) {
final StopWatch watch = 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());
}
watch.log();
}
private ReusableBorrow batchTask(
@NonNull JavaCompilerService parent, @NonNull Collection<? extends JavaFileObject> sources) {
parent.diagnostics.clear();
final Iterable<String> options = options();
diagnosticListener =
new DiagnosticListenerWrapper(parent.diagnostics::add, sources.iterator().next());
final ReusableBorrow borrow =
parent.compiler.getTask(
parent.fileManager, diagnosticListener, options, Collections.emptyList(), sources);
if (parent.fileManager != null) {
parent.fileManager.setContext(borrow.task.getContext());
}
return borrow;
}
@NonNull
private List<String> options() {
List<String> options = new ArrayList<>();
// This won't be used if the current module is Android module project
System.setProperty(PROP_ANDROIDIDE_JAVA_HOME, JAVA_HOME.getAbsolutePath());
if (this.parent.module != null && this.parent.module.getType() == ProjectType.Android) {
setLatestSourceVersion(SourceVersion.RELEASE_8);
setLatestSupportedSourceVersion(SourceVersion.RELEASE_11);
disableModules();
} else {
setLatestSourceVersion(SourceVersion.RELEASE_11);
setLatestSupportedSourceVersion(SourceVersion.RELEASE_11);
enableModules();
}
setupCompileOptions(parent.module, options);
Collections.addAll(options, "-proc:none", "-g");
Collections.addAll(
options,
"-XDcompilePolicy=byfile",
"-XD-Xprefer=source",
"-XDide",
"-XDkeepCommentsOverride=ignore",
"-XDsuppressAbortOnBadClassFile",
"-XDshould-stop.at=GENERATE",
"-XDdiags.formatterOptions=-source",
"-XDdiags.layout=%L%m|%L%m|%L%m",
"-XDbreakDocCommentParsingOnError=false",
"-Xlint:cast",
"-Xlint:deprecation",
"-Xlint:empty",
"-Xlint:fallthrough",
"-Xlint:finally",
"-Xlint:path",
"-Xlint:unchecked",
"-Xlint:varargs",
"-Xlint:static");
return options;
}
protected void setupCompileOptions(final ModuleProject module, final List<String> options) {
if (module == null) {
Collections.addAll(
options,
"-source",
DEFAULT_COMPILER_SOURCE_AND_TARGET_VERSION,
"-target",
DEFAULT_COMPILER_SOURCE_AND_TARGET_VERSION);
return;
}
final IJavaCompilerSettings compilerSettings = module.getCompilerSettings();
options.add("-source");
options.add(compilerSettings.getJavaSourceVersion());
options.add("-target");
options.add(compilerSettings.getJavaBytecodeVersion());
}
@Override
public void close() {
closed = true;
}
/**
* If the compilation failed because javac didn't find some package-private files in source files
* with different names, list those source files.
*/
Set<Path> needsAdditionalSources() {
if (parent.getModule() == null) {
return Collections.emptySet();
}
// Check for "class not found errors" that refer to package private classes
Set<Path> addFiles = new HashSet<>();
for (Diagnostic<? extends JavaFileObject> err : parent.diagnostics) {
if (!err.getCode().equals("compiler.err.cant.resolve.location")) {
continue;
}
if (!isValidFileRange(err)) {
continue;
}
String packageName = packageName(err);
ClassTrie.Node node = parent.getModule().compileJavaSourceClasses.findNode(packageName);
if (node != null && node.isClass() && node instanceof SourceClassTrie.SourceNode) {
addFiles.add(((SourceClassTrie.SourceNode) node).getFile());
}
}
return addFiles;
}
private String packageName(Diagnostic<? extends JavaFileObject> err) {
if (err instanceof ClientCodeWrapper.DiagnosticSourceUnwrapper) {
JCDiagnostic diagnostic = ((ClientCodeWrapper.DiagnosticSourceUnwrapper) err).d;
JCDiagnostic.DiagnosticPosition pos = diagnostic.getDiagnosticPosition();
Object[] args = diagnostic.getArgs();
Kinds.KindName kind = (Kinds.KindName) args[0];
if (kind == Kinds.KindName.CLASS) {
if (pos.toString().contains(".")) {
return pos.toString().substring(0, pos.toString().lastIndexOf('.'));
}
}
}
Path file = Paths.get(err.getSource().toUri());
return StringSearch.packageName(file);
}
private boolean isValidFileRange(Diagnostic<? extends JavaFileObject> d) {
return d.getSource().toUri().getScheme().equals("file")
&& d.getStartPosition() >= 0
&& d.getEndPosition() >= 0;
}
public static class DiagnosticListenerWrapper extends DiagnosticListenerImpl {
private final Consumer<Diagnostic<? extends JavaFileObject>> consumer;
public DiagnosticListenerWrapper(
final Consumer<Diagnostic<? extends JavaFileObject>> consumer, JavaFileObject jfo) {
super(jfo);
this.consumer = consumer;
}
@Override
public void report(final Diagnostic<? extends JavaFileObject> diagnostic) {
consumer.accept(diagnostic);
super.report(diagnostic);
}
}
}
@@ -0,0 +1,73 @@
/*
* 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.java.compiler;
import androidx.annotation.NonNull;
import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
import java.nio.file.Path;
import java.util.List;
import jdkx.tools.Diagnostic;
import jdkx.tools.JavaFileObject;
import openjdk.source.tree.CompilationUnitTree;
import openjdk.tools.javac.api.JavacTaskImpl;
public class CompileTask implements AutoCloseable {
public final JavacTaskImpl task;
public final List<CompilationUnitTree> roots;
public final List<Diagnostic<? extends JavaFileObject>> diagnostics;
public final CompileBatch compileBatch;
public final DiagnosticListenerImpl diagnosticListener;
public CompileTask(
@NonNull CompileBatch compileBatch, List<Diagnostic<? extends JavaFileObject>> diagnostics) {
this.compileBatch = compileBatch;
this.task = compileBatch.task;
this.roots = compileBatch.roots;
this.diagnostics = diagnostics;
this.diagnosticListener = compileBatch.diagnosticListener;
}
public CompilationUnitTree root() {
if (roots.size() != 1) {
throw new RuntimeException("No compilation units found. Roots: " + roots.size());
}
return roots.get(0);
}
public CompilationUnitTree root(Path file) {
for (CompilationUnitTree root : roots) {
if (root.getSourceFile().toUri().equals(file.toUri())) {
return root;
}
}
throw new RuntimeException("Compilation unit not found");
}
public CompilationUnitTree root(JavaFileObject file) {
for (CompilationUnitTree root : roots) {
if (root.getSourceFile().toUri().equals(file.toUri())) {
return root;
}
}
throw new RuntimeException("Compilation unit not found");
}
@Override
public void close() {}
}
@@ -0,0 +1,66 @@
/*
* 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.java.compiler;
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
import com.itsaky.androidide.lsp.java.parser.ParseTask;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.TreeSet;
import java.util.stream.Collectors;
import jdkx.tools.JavaFileObject;
public interface CompilerProvider {
Path NOT_FOUND = Paths.get("");
TreeSet<String> publicTopLevelTypes();
TreeSet<String> packagePrivateTopLevelTypes(String packageName);
Optional<JavaFileObject> findAnywhere(String className);
Path findTypeDeclaration(String className);
Path[] findTypeReferences(String className);
Path[] findMemberReferences(String className, String memberName);
default List<String> findQualifiedNames(String simpleName) {
return findQualifiedNames(simpleName, false);
}
List<String> findQualifiedNames(String simpleName, boolean onlyOne);
ParseTask parse(Path file);
ParseTask parse(JavaFileObject file);
default SynchronizedTask compile(Path... files) {
return compile(Arrays.stream(files).map(SourceFileObject::new).collect(Collectors.toList()));
}
default SynchronizedTask compile(Collection<? extends JavaFileObject> sources) {
return compile(new CompilationRequest(sources));
}
SynchronizedTask compile(CompilationRequest request);
}
@@ -0,0 +1,47 @@
/*
* 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.java.compiler
import com.itsaky.androidide.utils.StopWatch
import openjdk.source.tree.CompilationUnitTree
import openjdk.tools.javac.api.JavacTaskImpl
import java.util.function.Consumer
/**
* Default implementation of [CompilationTaskProcessor].
*
* @author Akash Yadav
*/
class DefaultCompilationTaskProcessor : CompilationTaskProcessor {
override fun process(task: JavacTaskImpl, processCompilationUnit: Consumer<CompilationUnitTree>) {
val watch = StopWatch("Process compilation task")
val trees = task.parse()
watch.lapFromLast("Parsed treees")
trees.forEach(processCompilationUnit::accept)
watch.lapFromLast("Processed trees")
// val entered = JavacTaskUtil.enterTrees(task, trees)
// watch.lapFromLast("Entered trees")
//
// val analyzed = JavacTaskUtil.analyze(task, entered)
task.analyze()
watch.lapFromLast("Analyzed all trees")
}
}
@@ -0,0 +1,36 @@
/*
* 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.java.compiler
import com.itsaky.androidide.javac.services.compiler.ReusableCompiler
import com.itsaky.androidide.javac.services.compiler.ReusableContext
/**
* Implementation of the [ReusableCompiler] which replaces necessary components from the
* [ReusableContext].
*
* @author Akash Yadav
*/
class JCReusableCompiler : ReusableCompiler() {
override fun onCreateContext(): ReusableContext {
return super.onCreateContext().also {
JavaCompilerImpl.preRegister(context = it, replace = true)
}
}
}
@@ -0,0 +1,60 @@
/*
* 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.java.compiler
import com.itsaky.androidide.models.Position
import jdkx.tools.JavaFileObject
import openjdk.tools.javac.util.Context
/**
* Configuration for the [JavaCompilerImpl].
*
* @property files The main files that are being compiled/parsed.
* @property completionInfo Information about the completion
* @author Akash Yadav
*/
class JavaCompilerConfig(context: Context) {
init {
context.put(compilerConfigKey, this)
}
var files: Collection<JavaFileObject>? = null
var completionInfo: CompletionInfo? = null
companion object {
@JvmField val compilerConfigKey = Context.Key<JavaCompilerConfig>()
@JvmStatic
fun instance(context: Context): JavaCompilerConfig {
var instance = context.get(compilerConfigKey)
if (instance == null) {
instance = JavaCompilerConfig(context)
}
return instance
}
}
}
/**
* Information about the completion request initiated by the Java completion provider.
*
* @property cursor The cursor position for the completion.
* @author Akash Yadav
*/
data class CompletionInfo(val cursor: Position)
@@ -0,0 +1,90 @@
/*
* 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.java.compiler
import com.itsaky.androidide.javac.services.compiler.ReusableContext
import com.itsaky.androidide.javac.services.compiler.ReusableJavaCompiler
import com.itsaky.androidide.lsp.java.parser.ts.TSJavaParser
import com.itsaky.androidide.lsp.java.parser.ts.TSMethodPruner.prune
import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.utils.VMUtils
import com.itsaky.androidide.utils.withStopWatch
import jdkx.tools.JavaFileObject
import jdkx.tools.JavaFileObject.Kind.SOURCE
import openjdk.tools.javac.api.ClientCodeWrapper
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit
import openjdk.tools.javac.util.Context
import kotlin.io.path.name
class JavaCompilerImpl(context: Context?) : ReusableJavaCompiler(context) {
override fun parse(filename: JavaFileObject?, content: CharSequence?): JCCompilationUnit {
if (VMUtils.isJvm()) {
return super.parse(filename, content)
}
val file = ClientCodeWrapper.instance(context).unwrap(filename)
val compilerConfig = JavaCompilerConfig.instance(context)
// Preconditions
if (
content == null ||
compilerConfig.files == null ||
filename?.kind != SOURCE ||
compilerConfig.files?.contains(file) == false
) {
return super.parse(filename, content)
}
// If the file is NOT being parsed for a completion request,
// we should not prune method bodies of active documents
if (compilerConfig.completionInfo == null && FileManager.isActive(filename.toUri())) {
return super.parse(filename, content)
}
val pruned = withStopWatch("${if(file is SourceFileObject) "[${file.path.name}] " else ""}Prune method bodies") { watch ->
val contentBuilder = StringBuilder(content)
return@withStopWatch TSJavaParser.parse(file).use { parseResult ->
prune(
contentBuilder,
parseResult.tree,
compilerConfig.completionInfo?.cursor?.index ?: -1
)
watch.log()
return@use contentBuilder
}
}
return super.parse(filename, pruned)
}
companion object {
@JvmStatic
fun preRegister(context: ReusableContext, replace: Boolean = false) {
if (replace) {
context.drop(compilerKey)
}
context.put(compilerKey, Context.Factory { JavaCompilerImpl(it) })
}
}
}
@@ -0,0 +1,576 @@
/*
* 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.java.compiler;
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;
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarations;
import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.FileManager;
import com.itsaky.androidide.projects.api.AndroidModule;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.util.BootClasspathProvider;
import com.itsaky.androidide.projects.util.StringSearch;
import com.itsaky.androidide.utils.Cache;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.SourceClassTrie;
import com.itsaky.androidide.utils.StopWatch;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jdkx.tools.Diagnostic;
import jdkx.tools.JavaFileObject;
import jdkx.tools.StandardLocation;
import openjdk.source.tree.CompilationUnitTree;
import openjdk.source.tree.MethodTree;
import openjdk.source.util.SourcePositions;
import openjdk.source.util.TreePath;
import openjdk.source.util.Trees;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JavaCompilerService implements CompilerProvider {
public static final JavaCompilerService NO_MODULE_COMPILER = new JavaCompilerService(null);
private static final Cache<String, Boolean> cacheContainsWord = new Cache<>();
private static final Cache<Void, List<String>> cacheContainsType = new Cache<>();
private static final Logger LOG = LoggerFactory.getLogger(JavaCompilerService.class);
protected final Set<String> classPathClasses;
protected final List<Diagnostic<? extends JavaFileObject>> diagnostics = new ArrayList<>();
protected final Map<JavaFileObject, Long> cachedModified = new HashMap<>();
protected final Cache<Void, List<String>> cacheFileImports = new Cache<>();
protected final SynchronizedTask synchronizedTask = new SynchronizedTask();
protected final SourceFileManager fileManager;
protected final ModuleProject module;
public ReusableCompiler compiler = new JCReusableCompiler();
protected Set<String> bootClasspathClasses =
BootClasspathProvider.getTopLevelClasses(
Collections.singleton(Environment.ANDROID_JAR.getAbsolutePath()));
private CompileBatch cachedCompile;
private int changeDelta = 0;
private Position lastReparsePosition = Position.NONE;
private Position newCursorPosition = Position.NONE;
// The module project must not be null
// It is marked as nullable just for some special cases like tests
public JavaCompilerService(@Nullable ModuleProject module) {
this.module = module;
if (module == null) {
this.fileManager = SourceFileManager.NO_MODULE;
this.classPathClasses = Collections.emptySet();
} else {
this.fileManager = SourceFileManager.forModule(module);
this.classPathClasses =
Collections.unmodifiableSet(module.compileClasspathClasses.allClassNames());
this.bootClasspathClasses = Collections.unmodifiableSet(getBootclasspathClasses());
}
}
private Set<String> getBootclasspathClasses() {
if (module != null && module instanceof AndroidModule) {
final List<String> classpaths =
((AndroidModule) module)
.getBootClassPaths().stream().map(File::getPath).collect(Collectors.toList());
BootClasspathProvider.update(classpaths);
this.bootClasspathClasses =
Collections.unmodifiableSet(BootClasspathProvider.getTopLevelClasses(classpaths));
}
return bootClasspathClasses;
}
private JavaCompilerService(
@Nullable ModuleProject module,
SourceFileManager fileManager,
Set<String> bootClasspathClasses,
Set<String> classPathClasses) {
this.module = module;
this.fileManager = fileManager;
this.bootClasspathClasses = bootClasspathClasses;
this.classPathClasses = classPathClasses;
}
public ModuleProject getModule() {
return module;
}
@Override
public TreeSet<String> publicTopLevelTypes() {
TreeSet<String> all = new TreeSet<>();
List<SourceClassTrie.SourceNode> sourceClasses =
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
for (SourceClassTrie.SourceNode node : sourceClasses) {
all.add(node.getQualifiedName());
}
all.addAll(classPathClasses);
all.addAll(bootClasspathClasses);
return all;
}
@Override
public TreeSet<String> packagePrivateTopLevelTypes(String packageName) {
return new TreeSet<>();
}
@Override
public Optional<JavaFileObject> findAnywhere(String className) {
Path fromSource = findTypeDeclaration(className);
if (fromSource != NOT_FOUND) {
return Optional.of(new SourceFileObject(fromSource));
}
return Optional.empty();
}
@Override
public Path findTypeDeclaration(String className) {
Path fastFind = findPublicTypeDeclaration(className);
if (fastFind != NOT_FOUND) {
return fastFind;
}
// In principle, the slow path can be skipped in many cases.
// If we're spending a lot of time in findTypeDeclaration, this would be a good
// optimization.
String packageName = Extractors.packageName(className);
String simpleName = Extractors.simpleName(className);
List<SourceClassTrie.SourceNode> classes =
module != null ? module.listClassesFromSourceDirs(packageName) : Collections.emptyList();
for (SourceClassTrie.SourceNode node : classes) {
final Path path = node.getFile();
if (containsWord(path, simpleName) && containsType(path, className)) {
return path;
}
}
return NOT_FOUND;
}
@Override
public Path[] findTypeReferences(String className) {
String packageName = Extractors.packageName(className);
String simpleName = Extractors.simpleName(className);
List<Path> candidates = new ArrayList<>();
List<SourceClassTrie.SourceNode> sourceNodes =
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
for (SourceClassTrie.SourceNode node : sourceNodes) {
final Path path = node.getFile();
if (containsWord(path, packageName)
&& containsImport(path, className)
&& containsWord(path, simpleName)) {
candidates.add(path);
}
}
return candidates.toArray(new Path[0]);
}
@Override
public Path[] findMemberReferences(String className, String memberName) {
List<Path> candidates = new ArrayList<>();
List<SourceClassTrie.SourceNode> sourceNodes =
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
for (SourceClassTrie.SourceNode node : sourceNodes) {
final Path path = node.getFile();
if (containsWord(path, memberName)) {
candidates.add(path);
}
}
return candidates.toArray(new Path[0]);
}
@Override
public List<String> findQualifiedNames(String simpleName, boolean onlyOne) {
final var names = new ArrayList<String>();
for (var name : publicTopLevelTypes()) {
// This will be true in a test environment
if (name.contains("/")) {
name = name.replace('/', '.');
}
if (name.endsWith("." + simpleName)) {
names.add(name);
if (onlyOne) {
break;
}
}
}
return names;
}
@Override
public ParseTask parse(Path file) {
Parser parser = Parser.parseFile(file);
return new ParseTask(parser.task, parser.root);
}
@Override
public ParseTask parse(JavaFileObject file) {
Parser parser = Parser.parseJavaFileObject(file);
return new ParseTask(parser.task, parser.root);
}
@Override
public SynchronizedTask compile(final CompilationRequest request) {
return compileBatch(request);
}
private SynchronizedTask compileBatch(CompilationRequest request) {
synchronizedTask.post(
() -> {
if (needsCompilation(request.sources)) {
reparseOrRecompile(request);
} else {
LOG.info("...using cached compile");
}
synchronizedTask.setTask(new CompileTask(cachedCompile, diagnostics));
});
return synchronizedTask;
}
private boolean needsCompilation(Collection<? extends JavaFileObject> sources) {
if (cachedModified.size() != sources.size()) {
return true;
}
for (JavaFileObject f : sources) {
if (!cachedModified.containsKey(f)) {
return true;
}
final Long modified = cachedModified.get(f);
if (modified != null && f.getLastModified() != modified) {
return true;
}
}
return false;
}
private synchronized void reparseOrRecompile(CompilationRequest request) {
// if (needsRecompilation(request)) {
// LOG.warn("Cannot reparse. Recompilation is required");
recompile(request);
// } else {
// LOG.debug("Trying to perform a reparse...");
// tryReparse(request);
// }
}
private boolean needsRecompilation(final CompilationRequest request) {
return this.cachedCompile == null
|| this.cachedCompile.closed;
// || request.partialRequest == null
// || request.partialRequest.cursor < 0
// || !isChangeValidForReparse()
// || request.sources.size() != 1; // Cannot perform a reparse if there are multiple files
}
private boolean isChangeValidForReparse() {
return this.lastReparsePosition == Position.NONE
|| (this.newCursorPosition != Position.NONE
&& this.lastReparsePosition.getLine() == this.newCursorPosition.getLine());
}
private void tryReparse(@NonNull final CompilationRequest request) {
// Satisfy lint
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());
final String path = file.getAbsolutePath();
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, TreePath> currentMethod =
binarySearchCurrentMethod(positions, 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");
}
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;
if (start < 0 || end < 0 || start > end || end >= partialRequest.contents.length()) {
LOG.warn(
"Cannot reparse. Invalid change delta. end: {} changeDelta: {} content.length: {}",
end,
this.changeDelta,
partialRequest.contents.length()
);
recompile(request);
return;
}
watch.lapFromLast("Found start and end positions of current method");
final PartialReparser reparser = new PartialReparserImpl();
final String newBody = partialRequest.contents.substring(start, end);
final boolean reparsed =
reparser.reparseMethod(info, currentMethod.second, newBody, partialRequest.contents);
if (!reparsed) {
LOG.error("Failed to reparse");
recompile(request);
return;
}
watch.log();
LOG.info("Successfully reparsed method: {}", methodTree.getName());
updateModificationCache(request);
cachedCompile.updatePositions(info.cu, true);
this.changeDelta = 0;
this.lastReparsePosition = this.newCursorPosition;
}
@Nullable
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, TreePath> method = positions.get(mid);
final Range range = method.first;
final int startIndex = range.getStart().requireIndex();
final int endIndex = range.getEnd().requireIndex();
if (cursor < startIndex) {
right = mid - 1;
} else if (cursor > endIndex) {
left = mid + 1;
} else {
return method;
}
}
return null;
}
private synchronized void recompile(CompilationRequest request) {
close();
this.cachedCompile = performCompilation(request);
this.changeDelta = 0;
updateModificationCache(request);
}
public synchronized void close() {
if (cachedCompile != null) {
cachedCompile.close();
cachedCompile.borrow.close();
}
}
private void updateModificationCache(final CompilationRequest request) {
cachedModified.clear();
for (JavaFileObject f : request.sources) {
cachedModified.put(f, f.getLastModified());
}
}
private CompileBatch performCompilation(CompilationRequest request) {
final Collection<? extends JavaFileObject> sources = request.sources;
if (sources.isEmpty()) {
throw new RuntimeException("empty sources");
}
CompileBatch firstAttempt = new CompileBatch(this, sources, request);
Set<Path> addFiles = firstAttempt.needsAdditionalSources();
if (addFiles.isEmpty()) {
return firstAttempt;
}
// If the compiler needs additional source files that contain package-private files
LOG.info("...need to recompile with {}", addFiles);
firstAttempt.close();
firstAttempt.borrow.close();
List<JavaFileObject> moreSources = new ArrayList<>(sources);
for (Path add : addFiles) {
moreSources.add(new SourceFileObject(add));
}
return new CompileBatch(this, moreSources, request);
}
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 List<String> readImports(Path file) {
if (cacheFileImports.needs(file, null)) {
loadImports(file);
}
return cacheFileImports.get(file, null);
}
private void loadImports(Path file) {
List<String> list = new ArrayList<>();
Pattern importClass = Pattern.compile("^import +([\\w.]+\\.\\w+);");
Pattern importStar = Pattern.compile("^import +([\\w.]+\\.\\*);");
try (BufferedReader lines = FileManager.INSTANCE.getReader(file)) {
for (String line = lines.readLine(); line != null; line = lines.readLine()) {
// If we reach a class declaration, stop looking for imports
// TODO This could be a little more specific
if (line.contains("class")) {
break;
}
// import foo.bar.Doh;
Matcher matchesClass = importClass.matcher(line);
if (matchesClass.matches()) {
list.add(matchesClass.group(1));
}
// import foo.bar.*
Matcher matchesStar = importStar.matcher(line);
if (matchesStar.matches()) {
list.add(matchesStar.group(1));
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
cacheFileImports.load(file, null, list);
}
private String packageNameOrEmpty(Path file) {
return module != null ? module.packageNameOrEmpty(file) : "";
}
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();
this.newCursorPosition = event.getChangeRange().getEnd();
}
public JavaCompilerService copy() {
final JavaCompilerService compiler =
new JavaCompilerService(
this.module, this.fileManager, this.bootClasspathClasses, this.classPathClasses);
compiler.cachedCompile = null;
compiler.newCursorPosition = Position.NONE;
compiler.lastReparsePosition = Position.NONE;
compiler.changeDelta = 0;
compiler.compiler = new ReusableCompiler();
compiler.diagnostics.clear();
compiler.cachedModified.clear();
return compiler;
}
private boolean containsType(Path file, String className) {
if (cacheContainsType.needs(file, null)) {
CompilationUnitTree root = parse(file).root;
List<String> types = new ArrayList<>();
new FindTypeDeclarations().scan(root, types);
cacheContainsType.load(file, null, types);
}
return cacheContainsType.get(file, null).contains(className);
}
private Path findPublicTypeDeclaration(String className) {
JavaFileObject source;
try {
source =
fileManager.getJavaFileForInput(
StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (source == null) {
return NOT_FOUND;
}
if (!source.toUri().getScheme().equals("file")) {
return NOT_FOUND;
}
Path file = Paths.get(source.toUri());
if (!containsType(file, className)) {
return NOT_FOUND;
}
return file;
}
}
@@ -0,0 +1,253 @@
/*
* 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.java.compiler;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.CloseUtils;
import com.itsaky.androidide.javac.config.JavacConfigProvider;
import com.itsaky.androidide.javac.services.fs.AndroidFsProviderImpl;
import com.itsaky.androidide.projects.api.AndroidModule;
import com.itsaky.androidide.projects.api.ModuleProject;
import com.itsaky.androidide.projects.util.StringSearch;
import com.itsaky.androidide.utils.ClassTrie;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.SourceClassTrie;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
import jdkx.tools.FileObject;
import jdkx.tools.ForwardingJavaFileManager;
import jdkx.tools.JavaFileObject;
import jdkx.tools.StandardLocation;
import openjdk.tools.javac.api.JavacTool;
import openjdk.tools.javac.file.JavacFileManager;
import openjdk.tools.javac.util.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SourceFileManager extends ForwardingJavaFileManager<JavacFileManager> {
public static final SourceFileManager NO_MODULE;
private static final Logger LOG;
private static final Map<ModuleProject, SourceFileManager> cachedFileManagers =
new ConcurrentHashMap<>();
static {
// Initialize LOG first to prevent NPE
LOG = LoggerFactory.getLogger(SourceFileManager.class);
NO_MODULE = new SourceFileManager(null);
}
private final ModuleProject module;
private SourceFileManager(final ModuleProject module) {
super(createDelegateFileManager());
this.module = module;
AndroidFsProviderImpl.INSTANCE.init();
if (module == null) {
return;
}
// Must be set before setting classpaths
System.setProperty(JavacConfigProvider.PROP_ANDROIDIDE_JAVA_HOME,
Environment.JAVA_HOME.getAbsolutePath());
setLocationLogError(StandardLocation.SOURCE_PATH, module.getCompileSourceDirectories());
setLocationLogError(StandardLocation.CLASS_PATH, configureClasspaths(module));
listLocations(EnumSet.of(StandardLocation.CLASS_PATH, StandardLocation.PLATFORM_CLASS_PATH));
}
@NonNull
private Set<File> configureClasspaths(final ModuleProject module) {
if (module == null) {
return emptySet();
}
if (module instanceof AndroidModule) {
final AndroidModule androidModule = (AndroidModule) module;
setLocationLogError(StandardLocation.PLATFORM_CLASS_PATH, androidModule.getBootClassPaths());
}
return module.getCompileClasspaths();
}
private static JavacFileManager createDelegateFileManager() {
return JavacTool.create()
.getStandardFileManager(msg -> LOG.debug(String.valueOf(msg)), Locale.getDefault(),
StandardCharsets.UTF_8);
}
public void setLocationLogError(Location location, Iterable<File> searchPath) {
try {
fileManager.setLocation(location, searchPath);
} catch (IOException e) {
LOG.error("Unable to set location", e);
}
}
private void listLocations(final EnumSet<StandardLocation> locations) {
for (StandardLocation location : locations) {
try {
fileManager.cacheLocation(location);
} catch (Exception e) {
// Ignored
LOG.debug("Failed to list location: {}", location, e);
}
}
}
@Override
public Iterable<JavaFileObject> list(
Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
if (this.module == null) {
return emptyList();
}
final ClassTrie.Node node = module.compileJavaSourceClasses.findNode(packageName);
if (node == null || node.isClass()) {
return emptyList();
}
final Stream<ClassTrie.Node> nodes;
if (recurse) {
nodes = node.allClassNodes().stream();
} else {
nodes = node.getChildren().values().stream();
}
final Stream<JavaFileObject> stream =
nodes
.filter(it -> it instanceof SourceClassTrie.SourceNode)
.map(it -> asJavaFileObject((SourceClassTrie.SourceNode) it));
return stream::iterator;
}
return super.list(location, packageName, kinds, recurse);
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (location == StandardLocation.SOURCE_PATH) {
SourceFileObject source = (SourceFileObject) file;
String packageName = packageNameOrEmpty(source.path);
String className = removeExtension(source.path.getFileName().toString());
if (!packageName.isEmpty()) {
className = packageName + "." + className;
}
return className;
}
return super.inferBinaryName(location, file);
}
@Override
public boolean hasLocation(Location location) {
return location == StandardLocation.SOURCE_PATH || super.hasLocation(location);
}
@Override
public JavaFileObject getJavaFileForInput(
Location location, String className, JavaFileObject.Kind kind) throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
String packageName = StringSearch.mostName(className);
String simpleClassName = StringSearch.lastName(className);
List<SourceClassTrie.SourceNode> classes =
module != null ? module.listClassesFromSourceDirs(packageName) : emptyList();
for (SourceClassTrie.SourceNode node : classes) {
final Path path = node.getFile();
if (path.getFileName().toString().equals(simpleClassName + kind.extension)) {
return new SourceFileObject(path);
}
}
// Fall through to disk in case we have .jar or .zip files on the source path
}
return super.getJavaFileForInput(location, className, kind);
}
@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName)
throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
return null;
}
return super.getFileForInput(location, packageName, relativeName);
}
@Override
public boolean contains(Location location, FileObject file) throws IOException {
if (location == StandardLocation.SOURCE_PATH) {
SourceFileObject source = (SourceFileObject) file;
return module.compileJavaSourceClasses.findSource(source.path) != null;
} else {
return super.contains(location, file);
}
}
private String packageNameOrEmpty(Path file) {
return this.module != null ? module.packageNameOrEmpty(file) : "";
}
private String removeExtension(String fileName) {
int lastDot = fileName.lastIndexOf(".");
return (lastDot == -1 ? fileName : fileName.substring(0, lastDot));
}
private JavaFileObject asJavaFileObject(SourceClassTrie.SourceNode node) {
// TODO erase method bodies of files that are not open
return new SourceFileObject(node.getFile());
}
public static SourceFileManager forModule(@NonNull ModuleProject project) {
Objects.requireNonNull(project);
return cachedFileManagers.computeIfAbsent(project, SourceFileManager::createForModule);
}
private static SourceFileManager createForModule(@NonNull ModuleProject project) {
LOG.info("Creating source file manager instance for module: {}", project);
return new SourceFileManager(project);
}
public static void clearCache() {
for (final SourceFileManager fileManager : cachedFileManagers.values()) {
CloseUtils.closeIO(fileManager);
}
cachedFileManagers.clear();
}
public void setContext(final Context context) {
fileManager.setContext(context);
}
}
@@ -0,0 +1,173 @@
/*
* 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.java.compiler;
import com.google.common.base.MoreObjects;
import com.itsaky.androidide.projects.FileManager;
import com.itsaky.androidide.utils.DocumentUtils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Objects;
import jdkx.lang.model.element.Modifier;
import jdkx.lang.model.element.NestingKind;
import jdkx.tools.JavaFileObject;
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 */
String contents;
/** if contents is set, the modified time of contents */
Instant modified;
public SourceFileObject(Path path) {
this(path, null, Instant.EPOCH);
}
public SourceFileObject(Path path, String contents, Instant modified) {
if (!DocumentUtils.isJavaFile(path)) throw new RuntimeException(path + " is not a java source");
this.path = path;
this.contents = contents;
this.modified = modified;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("path", this.path.toString()).toString();
}
@Override
public Kind getKind() {
String name = path.getFileName().toString();
return kindFromExtension(name);
}
private static Kind kindFromExtension(String name) {
for (Kind candidate : Kind.values()) {
if (name.endsWith(candidate.extension)) {
return candidate;
}
}
return null;
}
@Override
public boolean isNameCompatible(String simpleName, Kind kind) {
return path.getFileName().toString().equals(simpleName + kind.extension);
}
@Override
public NestingKind getNestingKind() {
return null;
}
@Override
public Modifier getAccessLevel() {
return null;
}
@Override
public URI toUri() {
return this.path.toAbsolutePath().toUri();
}
@Override
public String getName() {
return path.toString();
}
@Override
public InputStream openInputStream() {
if (contents != null) {
byte[] bytes = contents.getBytes();
return new ByteArrayInputStream(bytes);
}
return FileManager.INSTANCE.getInputStream(path);
}
@Override
public OutputStream openOutputStream() {
throw new UnsupportedOperationException();
}
@Override
public Reader openReader(boolean ignoreEncodingErrors) {
if (contents != null) {
return new StringReader(contents);
}
return FileManager.INSTANCE.getReader(path);
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
if (contents != null) {
return contents;
}
return FileManager.INSTANCE.getDocumentContents(this.path);
}
@Override
public Writer openWriter() {
throw new UnsupportedOperationException();
}
@Override
public long getLastModified() {
if (contents != null) {
return modified.toEpochMilli();
}
return FileManager.INSTANCE.getLastModified(this.path).toEpochMilli();
}
@Override
public boolean delete() {
throw new UnsupportedOperationException();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SourceFileObject)) {
return false;
}
final SourceFileObject that = (SourceFileObject) o;
try {
return this.path != null && that.path != null
&& Files.isSameFile(this.path, that.path)
&& Objects.equals(contents, that.contents)
&& Objects.equals(modified, that.modified);
} catch (Exception e) {
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(path, contents, modified);
}
}
@@ -0,0 +1,135 @@
/*
* 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/>.
*/
/*
* 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.java.compiler
import com.itsaky.androidide.lsp.java.CompilationCancellationException
import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled
import org.slf4j.LoggerFactory
import java.util.concurrent.Semaphore
class SynchronizedTask {
@Volatile
@PublishedApi
internal var isCompiling = false
@PublishedApi
internal val semaphore = Semaphore(1)
@PublishedApi
internal var task: CompileTask? = null
private set
companion object {
@PublishedApi
internal val log = LoggerFactory.getLogger(SynchronizedTask::class.java)
}
inline fun run(crossinline taskConsumer: (CompileTask) -> Unit) {
try {
semaphore.acquire()
} catch (e: InterruptedException) {
throw CompilationCancellationException(e)
}
try {
taskConsumer(task!!)
} catch (err: Throwable) {
if (!isCancelled(err)) {
log.error("An error occurred while working with compilation task", err)
}
throw err
} finally {
semaphore.release()
}
}
inline fun <T : Any?> get(crossinline action: (CompileTask) -> T): T {
try {
semaphore.acquire()
} catch (e: InterruptedException) {
throw CompilationCancellationException(e)
}
return try {
action(task!!)
} catch (err: Throwable) {
if (!isCancelled(err)) {
log.error("An error occurred while working with compilation task", err)
}
throw err
} finally {
semaphore.release()
}
}
fun post(action: Runnable) = post { action.run() }
inline fun post(action: () -> Unit) {
try {
semaphore.acquire()
} catch (e: InterruptedException) {
throw CompilationCancellationException(e)
}
isCompiling = true
try {
if (task != null) {
task!!.close()
}
action()
} catch (err: Throwable) {
if (!isCancelled(err)) {
log.error("An error occurred", err)
}
throw err
} finally {
semaphore.release()
isCompiling = false
}
}
fun setTask(task: CompileTask?) {
this.task = task
}
@get:Synchronized
val isBusy: Boolean
get() = isCompiling || semaphore.availablePermits() == 0
/**
* **FOR INTERNAL USE ONLY!**
*/
fun logStats() {
log.warn("[SynchronizedTask] isCompiling={} queuedLength={}", isCompiling,
semaphore.queueLength)
}
}
@@ -0,0 +1,63 @@
/*
* 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.java.edits
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.projects.IProjectManager
import io.github.rosemoe.sora.text.Content
import io.github.rosemoe.sora.widget.CodeEditor
import java.nio.file.Path
/**
* Handles edits for Java completion items.
*
* @author Akash Yadav
*/
abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditHandler() {
override fun performEdits(
item: CompletionItem,
editor: CodeEditor,
text: Content,
line: Int,
column: Int,
index: Int
) {
val compiler = JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(file, false) ?: return)
performEdits(compiler, editor, item)
executeCommand(editor, item.command)
}
/**
* Java edit handlers which require instance of the compiler should override this method instead
* of [performEdits].
*
* @param compiler The compiler service instance.
* @param editor The editor to perform edits on.
* @param completionItem The completion item which contains required data.
*/
abstract fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
completionItem: CompletionItem
)
}
@@ -0,0 +1,40 @@
/*
* 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.java.edits
import com.itsaky.androidide.editor.api.ILspEditor
import com.itsaky.androidide.lsp.edits.DefaultEditHandler
import com.itsaky.androidide.lsp.models.Command
import io.github.rosemoe.sora.widget.CodeEditor
/**
* Implementation of [DefaultEditHandler] which avoids reflection in
* [DefaultEditHandler.executeCommand].
*
* @author Akash Yadav
*/
open class BaseJavaEditHandler : DefaultEditHandler() {
override fun executeCommand(editor: CodeEditor, command: Command?) {
if (editor is ILspEditor) {
editor.executeCommand(command)
return
}
super.executeCommand(editor, command)
}
}
@@ -0,0 +1,50 @@
/*
* 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.java.edits
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.utils.EditHelper
import com.itsaky.androidide.lsp.models.ClassCompletionData
import com.itsaky.androidide.lsp.models.CompletionItem
import com.itsaky.androidide.lsp.util.RewriteHelper
import io.github.rosemoe.sora.widget.CodeEditor
import java.nio.file.Path
/**
* Imports the required class for a ClassCompletionItem.
*
* @param imports The current file imports.
* @param file The file in which this edit will be performed.
* @author Akash Yadav
*/
class ClassImportEditHandler(val imports: Set<String>, file: Path) : AdvancedJavaEditHandler(file) {
override fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
completionItem: CompletionItem
) {
val data = completionItem.data as? ClassCompletionData ?: return
val className = data.className
val edits = EditHelper.addImportIfNeeded(compiler, file, imports, className)
if (edits.isNotEmpty()) {
RewriteHelper.performEdits(edits, editor)
}
}
}
@@ -0,0 +1,59 @@
/*
* 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.java.edits
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
import com.itsaky.androidide.lsp.java.utils.EditHelper
import io.github.rosemoe.sora.widget.CodeEditor
import org.slf4j.LoggerFactory
import java.nio.file.Path
/**
* Imports multiple classes at once.
*
* @param classes The fully qualified classnames to import.
* @param imported The current imports of the given file.
* @author Akash Yadav
*/
class MultipleClassImportEditHandler(
private val classes: Set<String>,
private val imported: Set<String>,
file: Path
) : AdvancedJavaEditHandler(file) {
companion object {
private val log = LoggerFactory.getLogger(MultipleClassImportEditHandler::class.java)
}
override fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
completionItem: com.itsaky.androidide.lsp.models.CompletionItem
) {
val edits = mutableListOf<com.itsaky.androidide.lsp.models.TextEdit>()
for (className in classes) {
try {
edits.addAll(EditHelper.addImportIfNeeded(compiler, file, imported, className))
} catch (err: Throwable) {
log.error("Unable to compute edits to perform import for class: {}", className)
}
}
com.itsaky.androidide.lsp.util.RewriteHelper.performEdits(edits, editor)
}
}
@@ -0,0 +1,27 @@
/*
* 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.java.indexing
/**
* Anything that can be cloned.
*
* @author Akash Yadav
*/
interface ICloneable {
fun clone(): ICloneable
}

Some files were not shown because too many files have changed in this diff Show More