eclipse.jdt.ls icon indicating copy to clipboard operation
eclipse.jdt.ls copied to clipboard

Android project has no code suggestions

Open zykowal opened this issue 1 year ago • 15 comments

In Android project how can I get android code suggestions?

OS: windows 10 Gradle: 6.5 JDK: installed 8, 11 ,17 IDE: Neovim 0.10.1

I set JAVA_HOME and ANDROID_HOME in the system variables, but after building the project and install dependences, no errors, I only get basic JDK code suggestions and no Android code suggestions. Do I need to make any special settings in my lua file, or is there any other configuration needed? I'm a newbie in this area and would appreciate any help! Thanks!

my config

config = {
        -- set jdtls server settings
        jdtls = {
          function()
            -- use this function notation to build some variables
            local root_markers = { ".git", "mvnw", "gradlew", "pom.xml", "build.gradle" }
            local root_dir = require("jdtls.setup").find_root(root_markers)
            -- calculate workspace dir
            local project_name = vim.fn.fnamemodify(vim.fn.getcwd(), ":p:h:t")
            local workspace_dir = vim.fn.stdpath "data" .. "/site/java/workspace-root/" .. project_name
            os.execute("mkdir " .. workspace_dir)
            -- get the mason install path
            local install_path = require("mason-registry").get_package("jdtls"):get_install_path()
            -- get the current OS
            local os
            if vim.fn.has "macunix" then
              os = "mac"
            elseif vim.fn.has "win32" then
              os = "win"
            else
              os = "linux"
            end
            -- return the server config
            return {
              cmd = {
                "java",
                "-Declipse.application=org.eclipse.jdt.ls.core.id1",
                "-Dosgi.bundles.defaultStartLevel=4",
                "-Declipse.product=org.eclipse.jdt.ls.core.product",
                "-Dlog.protocol=true",
                "-Dlog.level=ALL",
                "-javaagent:" .. install_path .. "/lombok.jar",
                "-Xms1g",
                "--add-modules=ALL-SYSTEM",
                "--add-opens",
                "java.base/java.util=ALL-UNNAMED",
                "--add-opens",
                "java.base/java.lang=ALL-UNNAMED",
                "-jar",
                vim.fn.glob(install_path .. "/plugins/org.eclipse.equinox.launcher_*.jar"),
                "-configuration",
                install_path .. "/config_" .. os,
                "-data",
                workspace_dir,
              },
              root_dir = root_dir,
            }
          end,
        },
      },

zykowal avatar Sep 25 '24 15:09 zykowal

If someone could help me, I would be extremely grateful. 😃

zykowal avatar Sep 25 '24 15:09 zykowal

Having exactly the same issue, seems that jdtls is not able to get the classpath from the gradle project if it is using the android gradle plugin or something. Manually adding jars from the android dependencies (extracting the .aar files to get the classes.jar file) to the .classpath file I managed to get some autocomplete, but not all. Also enabling the setting to enable android project support didn't have any effect either

amgdev9 avatar Oct 31 '24 21:10 amgdev9

@codewithtoucans I finally managed to get it to work, but as the gradle classpath resolver is not working properly we need to workaround it adding the necessary jars from the android sdk and dependencies to the .classpath file in the app module. Here is my raw setup, but beware that some tweaking is needed depending on the project (adjusting the sdk version, configured variants, sdk path...):

Add this task to the app build.gradle.kts, which is used to get the compile classpath for the declared dependencies:

tasks.register("printCompileClasspath") {
    doLast {
        println("---START---")
        configurations.getByName("debugCompileClasspath").files.forEach { file ->
            println(file.absolutePath)
        }
        println("---END---")
    }
}

And then run this script to fill the .classpath file:

import subprocess
import os

result = subprocess.run(['./gradlew', 'app:printCompileClasspath'], stdout=subprocess.PIPE).stdout.decode("utf-8")

# Split by lines
lines = result.split('\n')
# Get the lines between ---START--- and ---END--- lines
start = lines.index("---START---") + 1
end = lines.index("---END---")
lines = lines[start:end]
print(lines)

final_lines = []

for line in lines:
    if line.endswith('.aar'):
        # Get the folder path
        folder = os.path.dirname(line)
        # Extract the aar in that folder
        subprocess.run(['unzip', '-o', line, '-d', folder])
        # Add the classes.jar
        final_lines.append(os.path.join(folder, 'classes.jar'))
    else:
        final_lines.append(line)

with open('app/.classpath', 'w') as f:
    f.write("""
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
	<classpathentry kind="output" path="bin/default"/>
    <classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="lib" path="/home/amg/Projects/JavaApp/app/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/processDebugResources/R.jar"/>
    <classpathentry kind="lib" path="/home/amg/SDK/android/platforms/android-34/android.jar"/>
<classpathentry kind="lib" path="/home/amg/SDK/android/platforms/android-34/core-for-system-modules.jar"/>
""")
    for line in final_lines:
        f.write(f'  <classpathentry kind="lib" path="{line}"/>\n')

I haven't configured it yet to support some features (such as viewbinding or buildconfig), but it should be straightforward to add the support for these with this base

amgdev9 avatar Nov 01 '24 14:11 amgdev9

JDT-LS relies on Eclipse BuildShip to compute a proper Gradle classpath. Maybe the issue should also be reported at https://github.com/eclipse/buildship/issues ?

mickaelistria avatar Nov 04 '24 07:11 mickaelistria

JDT-LS relies on Eclipse BuildShip to compute a proper Gradle classpath. Maybe the issue should also be reported at https://github.com/eclipse/buildship/issues ?

This seems like a jdt-ls issue, as Android support was implemented directly in jdt-ls here: https://github.com/eclipse-jdtls/eclipse.jdt.ls/pull/2197

I also tried running this in the demo Android application that was added in that PR and experienced the same issue, so presumably something has broken the support.

nolanpollack avatar Nov 30 '24 04:11 nolanpollack

@amgdev9 It doesn't work for me. I have tried many settings, but none of them work. However, if I just run a gradle project, like a Spring Boot project, it works perfectly fine. I don't have a good idea about Android project.

zykowal avatar Dec 01 '24 16:12 zykowal

@nolanpollack @mickaelistria I looked through the #2197 PR and didn't find any configuration they mentioned, so I just followed the normal configuration, but it didn't work. The eclipse.jdt.ls wiki.

zykowal avatar Dec 01 '24 17:12 zykowal

@amgdev9 In Android Gradle plugin 8.1.0-alpha09, they made significant changes to the plugin loading process, causing jdt-ls to not be able to find the Android SDK path, although it can recognize Android projects. I found a solution, which involves updating the last 5 lines of the android/init.gradle file in org.eclipse.jdt.ls.core_*.jar as follows:

allprojects {
  afterEvaluate {
    afterEvaluate {
        it.getPlugins().apply(JavaLanguageServerAndroidPlugin)
    }
  }
}

Here, two calls to afterEvaluate are required.

dev-mz avatar Dec 31 '24 11:12 dev-mz

@amgdev9 In a certain version of Gradle 8.0, possibly 8.0.2, they made significant changes to the plugin loading process, causing jdt-ls to not be able to find the Android SDK path, although it can recognize Android projects. I found a solution, which involves updating the last 5 lines of the android/init.gradle file in org.eclipse.jdt.ls.core_*.jar as follows:

allprojects {
  afterEvaluate {
    afterEvaluate {
        it.getPlugins().apply(JavaLanguageServerAndroidPlugin)
    }
  }
}

Here, two calls to afterEvaluate are required.

Nice!! Would you submit a PR so we have this fixed?

amgdev9 avatar Dec 31 '24 14:12 amgdev9

@amgdev9 In a certain version of Gradle 8.0, possibly 8.0.2, they made significant changes to the plugin loading process, causing jdt-ls to not be able to find the Android SDK path, although it can recognize Android projects. I found a solution, which involves updating the last 5 lines of the android/init.gradle file in org.eclipse.jdt.ls.core_*.jar as follows:

allprojects {

afterEvaluate {

afterEvaluate {
    it.getPlugins().apply(JavaLanguageServerAndroidPlugin)
}

}

}

Here, two calls to afterEvaluate are required.

Nice!! Would you submit a PR so we have this fixed?

I'm running Gradle 8.7 and tried testing modifying the .jar directly to add the second closure and this works!

Additionally I needed to modify the nvim-jdtls configuration as follows to add this setting:

settings = {
    java = {
      jdt = {
        ls = {
          androidSupport = {
            enabled = true, -- Enable Android support
          },
        },
      },
    },

NoahIrzinger avatar Jan 08 '25 09:01 NoahIrzinger

Following this as I've spent all day trying to get android depedencies to be recognised with jdtls in neovim. Have put androidSupport, enabled = true. But anytime JDTLS fires up in my java file, the inline diagnostics just can't find the android depedencies.

mikaelwills avatar Jan 23 '25 17:01 mikaelwills

Since @dev-mz 's already found a solution, and @NoahIrzinger 's tested it, can someone make a PR based on it? I can't do it myself because I don't do any Java outside of Android (am new to even Android!) and have no idea how to build/test this project.

Edit: am running gradle 8.10 and so far haven't got this solution to work

Edit2: https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3181 seems to be related. I got the same error message.

suguruwataru avatar Feb 23 '25 15:02 suguruwataru

So I made this very impromptu dive into this rabbit hole. I haven't found a solution but I'll write down here what I found, so hopefully someone who's more familiar with Java/Gradle can make use of it and come up with a solution.

The place where I suppose is the center of this problem is here: https://github.com/eclipse-jdtls/eclipse.jdt.ls/blob/eb8536db8bd212720c8e82faaa67a9b2dc235875/org.eclipse.jdt.ls.core/gradle/android/init.gradle#L106

For me, the call of this function here always returns null, regardless of whether the ending block of this file has 1 or 2 usage of afterEvaluate. The result is that JDTLS thinks it's not in an Android project.

This function tries to find the android.jar file used when building the project, by using a bootClasspath property of the android property of the Gradle Project. The android property has interface ApplicationExtension, but I haven't found any documentation on the existence of such a bootClasspath property on this interface. So I suppose what happened is that, Gradle and AGP teams made an incompatible change to a property that they want to keep internal but the original author of JDTLS' Android support somehow had found and made use of.

There's somewhere else where we can find a field called "bootClasspath". In recent (8.7+) versions of Gradle, the sdkComponents property of the androidComponent property of the Gradle Project has a property called bootClasspath. I wanted to make a change so that this field is used instead of the field in android, but it didn't work. The problem is, this bootClasspath is a "Provider", and its value can only be fetched when "ready". So far, in my tests, I have never witnessed its value becoming ready. According to its documentation, its value only becomes ready "at execution time", and I have no idea what this means. I only know this "execution time" is probably later than when the plugin is being applied currently. Perhaps this delaying of getting the path of android.jar is @dev-mz meant by "significant changes to the plugin loading process", and that's why they suggested putting the code applying the plugin into an extra level of afterEvaluate, delaying when it's run. This worked for them and @NoahIrzinger , but sadly somehow doesn't work for me.

suguruwataru avatar Feb 24 '25 09:02 suguruwataru

@dev-mz Thanks for the new pointers. Yeah I know I need to package the file back into the jar. Not knowing better about building java projects, that's how I've done all the testing with it and got my findings. With my findings I've got some ideas what your original solution was doing and therefore have already tried your new suggestion. It still didn't work, with bootClasspath being an empty list. A possibility is that I'm using Gradle 8.10. It's not super surprising to me that this solution doesn't always work, as it just delays applying the plugin, but doesn't directly address that applying this plugin needs to happen "at execution time".

suguruwataru avatar Feb 24 '25 11:02 suguruwataru

@dev-mz It turns out your solution does work for me! Thanks a lot! What got me is actually a very silly misuse of bash.

So in my .bashrc, I put

ANDROID_HOME=/path/to/android/sdk

when I should have put

export ANDRIOD_HOME=/path/to/android/sdk

Then Gradle can't find my android SDK, and naturally cannot provide bootClasspath whenever I ask for it.

suguruwataru avatar Feb 24 '25 13:02 suguruwataru

I finally managed to get it to work, but as the gradle classpath resolver is not working properly we need to workaround it adding the necessary jars from the android sdk and dependencies to the .classpath file in the app module. Here is my raw setup, but beware that some tweaking is needed depending on the project (adjusting the sdk version, configured variants, sdk path...):

Add this task to the app build.gradle.kts, which is used to get the compile classpath for the declared dependencies:

tasks.register("printCompileClasspath") {
    doLast {
        println("---START---")
        configurations.getByName("debugCompileClasspath").files.forEach { file ->
            println(file.absolutePath)
        }
        println("---END---")
    }
}
...

@amgdev9 It worked very well for independent project. If the project depends on subprojects(eg. by api project(':lib')), the printCompileClasspath will failed with error message: Could not resolve all files for configuration ':app:debugCompileClasspath'.

elementdavv avatar Jul 02 '25 08:07 elementdavv

For someone who have trouble of generate classpath for android project(like me), actually we only need one line tweak to make init.gradle work standalone (generate classpath do not really need jdtls).

android-classpath-generator

PEMessage avatar Jul 21 '25 16:07 PEMessage

I believe the following change might be better.

diff --git a/android/init.gradle b/android/init.gradle
index 7565ab7..e2e3ed5 100644
--- a/android/init.gradle
+++ b/android/init.gradle
@@ -45,10 +45,6 @@ class JavaLanguageServerAndroidPlugin implements Plugin<Project> {
         if (!project.hasProperty(ECLIPSE_PROPERTY)) {
             return
         }
-        File androidSDKFile = getAndroidSDKFile(project)
-        if (androidSDKFile == null) {
-            return
-        }
         project.afterEvaluate {
             List<Object> variants = getAndroidDebuggableVariants(project)
             EclipseModel eclipseModel = (EclipseModel) project.property(ECLIPSE_PROPERTY)
@@ -87,6 +83,8 @@ class JavaLanguageServerAndroidPlugin implements Plugin<Project> {
             // Add buildconfig files to source folders of eclipse model
             eclipseModel.classpath.file.whenMerged(new AddBuildConfigFilesAction(project, variants))
             // Add android.jar to project classpath of eclipse model
+        File androidSDKFile = getAndroidSDKFile(project)
+        if (androidSDKFile != null)
             eclipseModel.classpath.file.whenMerged(new AddAndroidSDKAction(androidSDKFile))
             // Add project dependencies to project classpath of eclipse model
             // for aar dependencies, extract classes.jar and add them to project classpath

dev-mz avatar Jul 22 '25 15:07 dev-mz