add support for loading filtered androidx libraries for code completion

This commit is contained in:
Dave Holoway
2020-07-02 13:34:47 +01:00
parent 7aa04dfc56
commit f50aaa48aa
7 changed files with 264 additions and 55 deletions

View File

@@ -33,7 +33,8 @@ async function createLanguageClient(context) {
}
};
const appSourceRoot = vscode.workspace.getConfiguration('android-dev-ext').get('appSourceRoot', '');
const config = vscode.workspace.getConfiguration('android-dev-ext');
const appSourceRoot = config.get('appSourceRoot', '');
let globSearchRoot = appSourceRoot;
if (globSearchRoot) {
// for findFiles to work properly, the path cannot begin with slash or have any relative components
@@ -52,7 +53,7 @@ async function createLanguageClient(context) {
initializationOptions: {
// extensionPath points to the root of the extension (the folder where this file is)
extensionPath: context.extensionPath,
appSourceRoot,
initialSettings: config,
sourceFiles,
workspaceFolders: (vscode.workspace.workspaceFolders || []).map(z => z.uri.toString()),
},

View File

@@ -317,6 +317,10 @@ function initDefaultCompletionTypes(lib) {
}
}
function clearDefaultCompletionEntries() {
defaultCompletionTypes = null;
}
/**
* Called from the VSCode completion item request.
*
@@ -331,8 +335,10 @@ async function getCompletionItems(params, liveParsers, androidLibrary) {
return [];
}
let dct = defaultCompletionTypes;
if (!defaultCompletionTypes) {
initDefaultCompletionTypes(androidLibrary);
dct = defaultCompletionTypes || {};
}
// wait for the Android library to load (in case we receive an early request)
@@ -366,7 +372,7 @@ async function getCompletionItems(params, liveParsers, androidLibrary) {
lastCompletionTypeMap = (parsed && parsed.typemap) || androidLibrary;
let locals = [],
modifiers = defaultCompletionTypes.modifiers,
modifiers = dct.modifiers,
sourceTypes = [];
if (parsed.unit) {
@@ -404,7 +410,7 @@ async function getCompletionItems(params, liveParsers, androidLibrary) {
// if this is not a static method, include this/super
if (!options.method.modifiers.includes('static')) {
locals.push(...defaultCompletionTypes.instances);
locals.push(...dct.instances);
}
// if we're inside a method, don't show the modifiers
@@ -430,18 +436,18 @@ async function getCompletionItems(params, liveParsers, androidLibrary) {
// exclude dotted (inner) types because they result in useless
// matches in the intellisense filter when . is pressed
const types = [
...defaultCompletionTypes.types,
...dct.types,
...sourceTypes,
].filter(x => !x.label.includes('.'))
.sort(sortBy.label)
return [
...locals,
...defaultCompletionTypes.primitiveTypes,
...defaultCompletionTypes.literals,
...dct.primitiveTypes,
...dct.literals,
...modifiers,
...types,
...defaultCompletionTypes.packageNames,
...dct.packageNames,
].map((x,idx) => {
// to force the order above, reset sortText for each item based upon a fixed-length number
x.sortText = `${1000+idx}`;
@@ -489,3 +495,4 @@ function resolveCompletionItem(item) {
exports.getCompletionItems = getCompletionItems;
exports.resolveCompletionItem = resolveCompletionItem;
exports.clearDefaultCompletionEntries = clearDefaultCompletionEntries;

View File

@@ -1,22 +1,30 @@
const fs = require('fs');
const path = require('path');
const { CEIType, loadAndroidLibrary } = require('java-mti');
const { CEIType, loadJavaLibraryCacheFile } = require('java-mti');
const { trace, time, timeEnd } = require('../logging');
/**
* @param {string} extensionPath install path of extension
* @param {string[]} additional_libs set of androidx library names to include eg: ["androidx.activity:activity"]
* @returns {Promise<Map<string,CEIType>>}
*/
async function loadAndroidSystemLibrary(extensionPath) {
console.time('android-library-load');
async function loadAndroidSystemLibrary(extensionPath, additional_libs) {
time('android-library-load');
let library;
try {
if (!extensionPath) {
throw new Error('Missing extension path')
}
const cache_folder = path.join(extensionPath, 'langserver', '.library-cache');
library = await loadHighestAPIPlatform(cache_folder);
trace(`loading android library from ${cache_folder} with androidx libs: ${JSON.stringify(additional_libs)}`)
const typemap = await loadJavaLibraryCacheFile(path.join(cache_folder, 'android-29.zip'));
if (Array.isArray(additional_libs) && additional_libs.length) {
await loadJavaLibraryCacheFile(path.join(cache_folder, 'androidx-20200701.zip'), additional_libs, typemap);
}
trace(`loaded ${typemap.size} types into android library`);
library = typemap;
} finally {
console.timeEnd('android-library-load');
timeEnd('android-library-load');
}
return library;
}
@@ -53,7 +61,7 @@ async function loadHighestAPIPlatform(cache_folder) {
console.log(`loading android platform cache: ${best_match.file.name}`);
const cache_file = path.join(cache_folder, best_match.file.name);
const typemap = loadAndroidLibrary(cache_file);
const typemap = loadJavaLibraryCacheFile(cache_file);
return typemap;
}

View File

@@ -282,6 +282,11 @@
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"vscode-uri": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz",
"integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -15,7 +15,7 @@ const { CEIType } = require('java-mti');
const { Settings } = require('./settings');
const { trace } = require('./logging');
const { getCompletionItems, resolveCompletionItem } = require('./completions');
const { clearDefaultCompletionEntries, getCompletionItems, resolveCompletionItem } = require('./completions');
const { getSignatureHelp } = require('./method-signatures');
const { FileURIMap, JavaDocInfo, indexAt, reparse } = require('./document');
@@ -32,9 +32,27 @@ let androidLibrary = null;
*/
const liveParsers = new FileURIMap();
let startupOpts = null;
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
function loadCodeCompletionLibrary(extensionPath, codeCompletionLibraries) {
// the android library is loaded asynchronously, with the global `androidLibrary` variable
// set to the promise while it is loading.
androidLibrary = (androidLibrary instanceof Promise
? androidLibrary // if we're currently loading, wait for it to complete
: Promise.resolve(new Map())
)
.then(() => loadAndroidSystemLibrary(extensionPath, codeCompletionLibraries))
.then(
library => androidLibrary = library,
err => {
console.log(`Android library load failed: ${err.message}\n Code completion may not be available.`);
return new Map();
}
);
}
// Text document manager monitoring file opens and edits
let documents = new TextDocuments({
/**
@@ -100,16 +118,21 @@ const connection = createConnection(ProposedFeatures.all);
connection.onInitialize((params) => {
// the android library is loaded asynchronously, with the global `androidLibrary` variable
// set to the promise while it is loading.
androidLibrary = loadAndroidSystemLibrary((params.initializationOptions || {}).extensionPath)
.then(
library => androidLibrary = library,
err => {
console.log(`Android library load failed: ${err.message}\n Code completion may not be available.`);
return new Map();
}
);
startupOpts = {
extensionPath: '',
initialSettings: {
appSourceRoot: '',
/** @type {string[]} */
codeCompletionLibraries: [],
trace: false,
},
sourceFiles: [],
...params.initializationOptions,
}
Settings.set(startupOpts.initialSettings);
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
let capabilities = params.capabilities;
@@ -119,32 +142,30 @@ connection.onInitialize((params) => {
hasWorkspaceFolderCapability = capabilities.workspace && !!capabilities.workspace.workspaceFolders;
if (params.initializationOptions) {
/** @type {string[]} */
const file_uris = params.initializationOptions.sourceFiles || [];
for (let file_uri of file_uris) {
const file = URI.parse(file_uri, true);
const filePath = file.fsPath;
if (!/.java/i.test(filePath)) {
trace(`ignoring non-java file: ${filePath}`);
continue;
}
if (liveParsers.has(file_uri)) {
trace(`File already loaded: ${file_uri}`);
continue;
}
try {
// it's fine to load the initial file set synchronously - the language server runs in a
// separate process and nothing (useful) can happen until the first parse is complete.
const content = fs.readFileSync(file.fsPath, 'utf8');
liveParsers.set(file_uri, new JavaDocInfo(file_uri, content, 0));
trace(`Added initial file: ${file_uri}`);
} catch (err) {
trace(`Failed to load initial source file: ${filePath}. ${err.message}`);
}
/** @type {string[]} */
const file_uris = Array.isArray(startupOpts.sourceFiles) ? startupOpts.sourceFiles : [];
for (let file_uri of file_uris) {
const file = URI.parse(file_uri, true);
const filePath = file.fsPath;
if (!/.java/i.test(filePath)) {
trace(`ignoring non-java file: ${filePath}`);
continue;
}
if (liveParsers.has(file_uri)) {
trace(`File already loaded: ${file_uri}`);
continue;
}
try {
// it's fine to load the initial file set synchronously - the language server runs in a
// separate process and nothing (useful) can happen until the first parse is complete.
const content = fs.readFileSync(file.fsPath, 'utf8');
liveParsers.set(file_uri, new JavaDocInfo(file_uri, content, 0));
trace(`Added initial file: ${file_uri}`);
} catch (err) {
trace(`Failed to load initial source file: ${filePath}. ${err.message}`);
}
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true });
}
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true });
return {
capabilities: {
@@ -171,10 +192,6 @@ connection.onInitialized(async () => {
DidChangeConfigurationNotification.type, {
section: 'android-dev-ext',
});
const initialSettings = await connection.workspace.getConfiguration({
section: "android-dev-ext"
});
Settings.set(initialSettings);
}
if (hasWorkspaceFolderCapability) {
@@ -189,12 +206,23 @@ connection.onInitialized(async () => {
connection.onDidChangeConfiguration(async (change) => {
trace(`onDidChangeConfiguration: ${JSON.stringify(change)}`);
const prev_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
// fetch and update the settings
const newSettings = await connection.workspace.getConfiguration({
section: "android-dev-ext"
});
Settings.set(newSettings);
const new_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
if (new_ccl.length !== prev_ccl.length || new_ccl.find((lib,idx) => lib !== prev_ccl[idx])) {
// code-completion libraries have changed - reload the android library
trace("code completion libraries changed - reloading android library and reparsing")
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false });
clearDefaultCompletionEntries();
}
})
documents.onDidClose((e) => {

View File

@@ -1,6 +1,7 @@
const defaultSettings = {
appSourceRoot: 'app/src/main',
codeCompletionLibraries: [],
trace: false,
}
@@ -11,6 +12,11 @@ const defaultSettings = {
*/
appSourceRoot = defaultSettings.appSourceRoot;
/**
* The set of androidx libraries to include in code completion
*/
codeCompletionLibraries = defaultSettings.codeCompletionLibraries;
/**
* True if we log details
*/

View File

@@ -2,7 +2,7 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "1.1.0",
"version": "1.2.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
@@ -43,7 +43,161 @@
"scope": "resource",
"type": "string",
"default": "app/src/main",
"description": "Workspace-relative path to the app source files. The specified folder should contain AndroidManifest.xml.\r\nChanges to this field require the extension to be restarted."
"description": "Workspace-relative path to the app source files. The specified folder should contain AndroidManifest.xml.\r\nChanges to this field require the extension or workspace to be reloaded."
},
"android-dev-ext.codeCompletionLibraries": {
"scope": "resource",
"type": "array",
"description": "Select which Android Jetpack Libraries (androidx.*) to include in code-completion results.\nNote: Switch to the JSON Settings editor for simpler editing of this list.",
"examples": [
["androidx.activity:activity"]
],
"items" : {
"type":"string",
"enum": [
"androidx.activity:activity",
"androidx.annotation:annotation",
"androidx.annotation:annotation-experimental",
"androidx.annotation:annotation-experimental-lint",
"androidx.appcompat:appcompat",
"androidx.appcompat:appcompat-resources",
"androidx.arch.core:core-common",
"androidx.arch.core:core-runtime",
"androidx.arch.core:core-testing",
"androidx.asynclayoutinflater:asynclayoutinflater",
"androidx.autofill:autofill",
"androidx.benchmark:benchmark-common",
"androidx.benchmark:benchmark-gradle-plugin",
"androidx.benchmark:benchmark-junit4",
"androidx.biometric:biometric",
"androidx.browser:browser",
"androidx.cardview:cardview",
"androidx.collection:collection",
"androidx.concurrent:concurrent-futures",
"androidx.constraintlayout:constraintlayout",
"androidx.constraintlayout:constraintlayout-solver",
"androidx.contentpager:contentpager",
"androidx.coordinatorlayout:coordinatorlayout",
"androidx.core:core",
"androidx.cursoradapter:cursoradapter",
"androidx.customview:customview",
"androidx.databinding:databinding-adapters",
"androidx.databinding:databinding-common",
"androidx.databinding:databinding-compiler",
"androidx.databinding:databinding-compiler-common",
"androidx.databinding:databinding-runtime",
"androidx.databinding:viewbinding",
"androidx.documentfile:documentfile",
"androidx.drawerlayout:drawerlayout",
"androidx.dynamicanimation:dynamicanimation",
"androidx.emoji:emoji",
"androidx.emoji:emoji-appcompat",
"androidx.emoji:emoji-bundled",
"androidx.enterprise:enterprise-feedback",
"androidx.enterprise:enterprise-feedback-testing",
"androidx.exifinterface:exifinterface",
"androidx.fragment:fragment",
"androidx.fragment:fragment-testing",
"androidx.gridlayout:gridlayout",
"androidx.heifwriter:heifwriter",
"androidx.interpolator:interpolator",
"androidx.leanback:leanback",
"androidx.leanback:leanback-preference",
"androidx.legacy:legacy-preference-v14",
"androidx.legacy:legacy-support-core-ui",
"androidx.legacy:legacy-support-core-utils",
"androidx.legacy:legacy-support-v13",
"androidx.legacy:legacy-support-v4",
"androidx.lifecycle:lifecycle-common",
"androidx.lifecycle:lifecycle-common-java8",
"androidx.lifecycle:lifecycle-compiler",
"androidx.lifecycle:lifecycle-extensions",
"androidx.lifecycle:lifecycle-livedata",
"androidx.lifecycle:lifecycle-livedata-core",
"androidx.lifecycle:lifecycle-process",
"androidx.lifecycle:lifecycle-reactivestreams",
"androidx.lifecycle:lifecycle-runtime",
"androidx.lifecycle:lifecycle-service",
"androidx.lifecycle:lifecycle-viewmodel",
"androidx.lifecycle:lifecycle-viewmodel-savedstate",
"androidx.loader:loader",
"androidx.localbroadcastmanager:localbroadcastmanager",
"androidx.media2:media2-common",
"androidx.media2:media2-exoplayer",
"androidx.media2:media2-player",
"androidx.media2:media2-session",
"androidx.media2:media2-widget",
"androidx.media:media",
"androidx.mediarouter:mediarouter",
"androidx.multidex:multidex",
"androidx.multidex:multidex-instrumentation",
"androidx.navigation:navigation-common",
"androidx.navigation:navigation-dynamic-features-fragment",
"androidx.navigation:navigation-dynamic-features-runtime",
"androidx.navigation:navigation-fragment",
"androidx.navigation:navigation-runtime",
"androidx.navigation:navigation-safe-args-generator",
"androidx.navigation:navigation-safe-args-gradle-plugin",
"androidx.navigation:navigation-testing",
"androidx.navigation:navigation-ui",
"androidx.paging:paging-common",
"androidx.paging:paging-runtime",
"androidx.paging:paging-rxjava2",
"androidx.palette:palette",
"androidx.percentlayout:percentlayout",
"androidx.preference:preference",
"androidx.print:print",
"androidx.recommendation:recommendation",
"androidx.recyclerview:recyclerview",
"androidx.recyclerview:recyclerview-selection",
"androidx.room:room-common",
"androidx.room:room-compiler",
"androidx.room:room-guava",
"androidx.room:room-migration",
"androidx.room:room-runtime",
"androidx.room:room-rxjava2",
"androidx.room:room-testing",
"androidx.savedstate:savedstate",
"androidx.sharetarget:sharetarget",
"androidx.slice:slice-builders",
"androidx.slice:slice-core",
"androidx.slice:slice-view",
"androidx.slidingpanelayout:slidingpanelayout",
"androidx.sqlite:sqlite",
"androidx.sqlite:sqlite-framework",
"androidx.swiperefreshlayout:swiperefreshlayout",
"androidx.test:core",
"androidx.test.espresso:espresso-accessibility",
"androidx.test.espresso:espresso-contrib",
"androidx.test.espresso:espresso-core",
"androidx.test.espresso:espresso-idling-resource",
"androidx.test.espresso:espresso-intents",
"androidx.test.espresso:espresso-remote",
"androidx.test.espresso:espresso-web",
"androidx.test.espresso.idling:idling-concurrent",
"androidx.test.espresso.idling:idling-net",
"androidx.test.ext:junit",
"androidx.test.ext:truth",
"androidx.test.janktesthelper:janktesthelper",
"androidx.test:monitor",
"androidx.test:rules",
"androidx.test:runner",
"androidx.test.uiautomator:uiautomator",
"androidx.transition:transition",
"androidx.tvprovider:tvprovider",
"androidx.vectordrawable:vectordrawable",
"androidx.vectordrawable:vectordrawable-animated",
"androidx.versionedparcelable:versionedparcelable",
"androidx.viewpager2:viewpager2",
"androidx.viewpager:viewpager",
"androidx.wear:wear",
"androidx.webkit:webkit",
"androidx.work:work-gcm",
"androidx.work:work-runtime",
"androidx.work:work-rxjava2",
"androidx.work:work-testing"
]
}
},
"android-dev-ext.subscriptionKey": {
"scope": "application",