diff --git a/extension.js b/extension.js index 57d7f35..4266882 100644 --- a/extension.js +++ b/extension.js @@ -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()), }, diff --git a/langserver/completions.js b/langserver/completions.js index 5cc8d7a..c0b3482 100644 --- a/langserver/completions.js +++ b/langserver/completions.js @@ -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; diff --git a/langserver/java/java-libraries.js b/langserver/java/java-libraries.js index dad1155..0d9fa17 100644 --- a/langserver/java/java-libraries.js +++ b/langserver/java/java-libraries.js @@ -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>} */ -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; } diff --git a/langserver/package-lock.json b/langserver/package-lock.json index 5893c1e..575d958 100644 --- a/langserver/package-lock.json +++ b/langserver/package-lock.json @@ -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", diff --git a/langserver/server.js b/langserver/server.js index f68a500..da65392 100644 --- a/langserver/server.js +++ b/langserver/server.js @@ -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) => { diff --git a/langserver/settings.js b/langserver/settings.js index bec7b34..bfe4172 100644 --- a/langserver/settings.js +++ b/langserver/settings.js @@ -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 */ diff --git a/package.json b/package.json index 934bea0..94186a7 100644 --- a/package.json +++ b/package.json @@ -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",