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

@@ -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
*/