From 7aa04dfc56ded49107fd44eddc1f9b8f5a0b9477 Mon Sep 17 00:00:00 2001 From: Dave Holoway Date: Tue, 30 Jun 2020 18:32:15 +0100 Subject: [PATCH] update initial file loading to use URIs passed from the client changes to the appSourceRoot now require an extension restart --- extension.js | 30 ++++++-- langserver/document.js | 151 +--------------------------------------- langserver/package.json | 3 +- langserver/server.js | 54 ++++++++------ langserver/settings.js | 5 +- package.json | 2 +- 6 files changed, 63 insertions(+), 182 deletions(-) diff --git a/extension.js b/extension.js index b90e0c3..57d7f35 100644 --- a/extension.js +++ b/extension.js @@ -11,7 +11,7 @@ const { selectTargetDevice } = require('./src/utils/device'); /** * @param {vscode.ExtensionContext} context */ -function createLanguageClient(context) { +async function createLanguageClient(context) { // The server is implemented in node let serverModule = context.asAbsolutePath(path.join('langserver', 'server.js')); // The debug options for the server @@ -33,6 +33,15 @@ function createLanguageClient(context) { } }; + const appSourceRoot = vscode.workspace.getConfiguration('android-dev-ext').get('appSourceRoot', ''); + let globSearchRoot = appSourceRoot; + if (globSearchRoot) { + // for findFiles to work properly, the path cannot begin with slash or have any relative components + globSearchRoot = path.normalize(appSourceRoot.replace(/(^[\\/]+)|([\\/]+$)/,'')); + if (globSearchRoot) globSearchRoot += '/'; + } + const sourceFiles = (await vscode.workspace.findFiles(`${globSearchRoot}**/*.java`, null, 1000, null)).map(uri => uri.toString()); + // Options to control the language client /** @type {import('vscode-languageclient').LanguageClientOptions} */ let clientOptions = { @@ -43,6 +52,9 @@ function createLanguageClient(context) { initializationOptions: { // extensionPath points to the root of the extension (the folder where this file is) extensionPath: context.extensionPath, + appSourceRoot, + sourceFiles, + workspaceFolders: (vscode.workspace.workspaceFolders || []).map(z => z.uri.toString()), }, synchronize: { // Notify the server about file changes to '.java files contained in the workspace @@ -63,6 +75,9 @@ function createLanguageClient(context) { let languageClient; let languageSupportEnabled = false; function refreshLanguageServerEnabledState() { + if (!languageClient) { + return; + } let langSupport = vscode.workspace.getConfiguration('android-dev-ext').get('languageSupport', false); if (langSupport === languageSupportEnabled) { return; @@ -83,15 +98,18 @@ function refreshLanguageServerEnabledState() { } -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed +/** + * @param {vscode.ExtensionContext} context + */ function activate(context) { /* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */ AndroidContentProvider.register(context, vscode.workspace); - languageClient = createLanguageClient(context); - refreshLanguageServerEnabledState(); + createLanguageClient(context).then(client => { + languageClient = client; + refreshLanguageServerEnabledState(); + }); // The commandId parameter must match the command field in package.json const disposables = [ @@ -136,8 +154,6 @@ function activate(context) { // trying to shut down the language server in the middle of a change-configuration request process.nextTick(() => refreshLanguageServerEnabledState()); }), - - languageClient, ]; context.subscriptions.splice(context.subscriptions.length, 0, ...disposables); diff --git a/langserver/document.js b/langserver/document.js index dcda3a9..d595759 100644 --- a/langserver/document.js +++ b/langserver/document.js @@ -1,8 +1,4 @@ -const fs = require('fs'); -const path = require('path'); -const os = require('os'); const { CEIType } = require('java-mti'); -const { Settings } = require('./settings'); const ParseProblem = require('./java/parsetypes/parse-problem'); const { parse } = require('./java/body-parser'); const { SourceUnit } = require('./java/source-types'); @@ -211,8 +207,7 @@ class ParsedInfo { */ function reparse(uris, liveParsers, androidLibrary, opts) { trace(`reparse`); - // we must have at least one URI - if (!uris || !uris.length) { + if (!Array.isArray(uris)) { return; } if (first_parse_waiting) { @@ -239,9 +234,6 @@ function reparse(uris, liveParsers, androidLibrary, opts) { cached_units.push(docinfo.parsed.unit); } } - if (!parsers.length) { - return; - } // Each parse uses a unique typemap, initialised from the android library const typemap = new Map(androidLibrary); @@ -283,150 +275,9 @@ function reparse(uris, liveParsers, androidLibrary, opts) { } } -/** - * Called during initialization and whenever the App Source Root setting is changed to scan - * for source files - * - * @param {string} src_folder absolute path to the source root - * @param {Map} liveParsers - */ -async function rescanSourceFolders(src_folder, liveParsers) { - if (!src_folder) { - return; - } - - // when the appSourceRoot config value changes and we rescan the folder, we need - // to delete any parsers that were from the old appSourceRoot - const unused_keys = new Set(liveParsers.keys()); - - const files = await loadWorkingFileList(src_folder); - - // create live parsers for all the java files, but don't replace any existing ones which - // have been loaded (and may be edited) before we reach here - for (let file of files) { - if (!/\.java$/i.test(file.fpn)) { - continue; - } - const uri = `file://${file.fpn}`; // todo - handle case-differences on Windows - unused_keys.delete(uri); - - if (liveParsers.has(uri)) { - trace(`already loaded: ${uri}`); - continue; - } - - try { - // read the full file content - const file_content = await new Promise((res, rej) => fs.readFile(file.fpn, 'UTF8', (err,data) => err ? rej(err) : res(data))); - // construct a new JavaDoc instance for the source file - liveParsers.set(uri, new JavaDocInfo(uri, file_content, 0)); - } catch {} - } - - // remove any parsers that are no longer part of the working set - unused_keys.forEach(uri => liveParsers.delete(uri)); -} - -/** - * Attempts to locate the app root folder using workspace folders and the appSourceRoot setting - * @param {*} workspace - * @returns Absolute path to app root folder or null - */ -async function getAppSourceRootFolder(workspace) { - /** @type {string} */ - let src_folder = null; - - const folders = await workspace.getWorkspaceFolders(); - if (!folders || !folders.length) { - trace('No workspace folders'); - return src_folder; - } - - folders.find(folder => { - const main_folder = path.join(folder.uri.replace(/^\w+:\/\//, ''), Settings.appSourceRoot); - try { - if (fs.statSync(main_folder).isDirectory()) { - src_folder = main_folder; - return true; - } - } catch {} - }); - - if (!src_folder) { - console.log([ - `Failed to find source root from workspace folders:`, - ...folders.map(f => ` - ${f.uri}`), - 'Configure the Android App Source Root value in your workspace settings to point to your source folder containing AndroidManifest.xml', - ].join(os.EOL)); - } - - return src_folder; -} - -async function loadWorkingFileList(src_folder) { - if (!src_folder) { - return []; - } - - trace(`Using src root folder: ${src_folder}. Searching for Android project source files...`); - time('source file search') - const files = scanSourceFiles(src_folder); - timeEnd('source file search'); - - if (!files.find(file => /^androidmanifest.xml$/i.test(file.relfpn))) { - console.log(`Warning: No AndroidManifest.xml found in app root folder. Check the Android App Source Root value in your workspace settings.`) - } - - return files; - - /** - * @param {string} base_folder - * @returns {{fpn:string, relfpn: string, stat:fs.Stats}[]} - */ - function scanSourceFiles(base_folder) { - // strip any trailing slash - base_folder = base_folder.replace(/[\\/]+$/, ''); - const done = new Set(), folders = [base_folder], files = []; - const max_folders = 100; - while (folders.length) { - const folder = folders.shift(); - if (done.has(folder)) { - continue; - } - done.add(folder); - if (done.size > max_folders) { - console.log(`Max folder limit reached - cancelling file search`); - break; - } - try { - trace(`scan source folder ${folder}`) - fs.readdirSync(folder) - .forEach(name => { - const fpn = path.join(folder, name); - const stat = fs.statSync(fpn); - files.push({ - fpn, - // relative path (without leading slash) - relfpn: fpn.slice(base_folder.length + 1), - stat, - }); - if (stat.isDirectory()) { - folders.push(fpn) - } - }); - } catch (err) { - trace(`Failed to scan source folder ${folder}: ${err.message}`) - } - } - return files; - } -} - exports.indexAt = indexAt; exports.positionAt = positionAt; exports.FileURIMap = FileURIMap; exports.JavaDocInfo = JavaDocInfo; exports.ParsedInfo = ParsedInfo; exports.reparse = reparse; -exports.getAppSourceRootFolder = getAppSourceRootFolder; -exports.rescanSourceFolders = rescanSourceFolders; \ No newline at end of file diff --git a/langserver/package.json b/langserver/package.json index 1732615..985b4d3 100644 --- a/langserver/package.json +++ b/langserver/package.json @@ -11,7 +11,8 @@ "dependencies": { "java-mti": "adelphes/java-mti#c8e1d3c", "vscode-languageserver": "6.1.1", - "vscode-languageserver-textdocument": "1.0.1" + "vscode-languageserver-textdocument": "1.0.1", + "vscode-uri": "2.1.2" }, "devDependencies": { "@types/node": "^13.13.4" diff --git a/langserver/server.js b/langserver/server.js index d57224f..f68a500 100644 --- a/langserver/server.js +++ b/langserver/server.js @@ -8,6 +8,7 @@ const { const fs = require('fs'); const { TextDocument } = require('vscode-languageserver-textdocument'); +const { URI } = require('vscode-uri'); const { loadAndroidSystemLibrary } = require('./java/java-libraries'); const { CEIType } = require('java-mti'); @@ -16,7 +17,7 @@ const { Settings } = require('./settings'); const { trace } = require('./logging'); const { getCompletionItems, resolveCompletionItem } = require('./completions'); const { getSignatureHelp } = require('./method-signatures'); -const { getAppSourceRootFolder, FileURIMap, JavaDocInfo, indexAt, reparse, rescanSourceFolders } = require('./document'); +const { FileURIMap, JavaDocInfo, indexAt, reparse } = require('./document'); /** * The global map of Android system types @@ -118,6 +119,33 @@ 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}`); + } + } + reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true }); + } + return { capabilities: { // we support incremental updates @@ -155,18 +183,11 @@ connection.onInitialized(async () => { }); } - const src_folder = await getAppSourceRootFolder(connection.workspace); - if (src_folder) { - await rescanSourceFolders(src_folder, liveParsers); - reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true }); - } - trace('Initialization complete'); }); connection.onDidChangeConfiguration(async (change) => { trace(`onDidChangeConfiguration: ${JSON.stringify(change)}`); - const old_app_root = Settings.appSourceRoot; // fetch and update the settings const newSettings = await connection.workspace.getConfiguration({ @@ -174,15 +195,6 @@ connection.onDidChangeConfiguration(async (change) => { }); Settings.set(newSettings); - - if (old_app_root !== Settings.appSourceRoot) { - // if the app root has changed, rescan the source folder - const src_folder = await getAppSourceRootFolder(connection.workspace); - if (src_folder) { - rescanSourceFolders(src_folder, liveParsers); - reparse([...liveParsers.keys()], liveParsers, androidLibrary); - } - } }) documents.onDidClose((e) => { @@ -201,13 +213,15 @@ connection.onDidChangeWatchedFiles( case 1: // create // if the user creates the file directly in vscode, the file will automatically open (and we receive an open callback) // - but if the user creates or copies a file into the workspace, we need to manually add it to the set. - if (!liveParsers.has(change.uri) && /^file:\/\//.test(change.uri)) { + if (!liveParsers.has(change.uri)) { trace(`file added: ${change.uri}`) try { - const fname = change.uri.replace(/^file:\/\//, ''); + const fname = URI.parse(change.uri, true).fsPath; liveParsers.set(change.uri, new JavaDocInfo(change.uri, fs.readFileSync(fname, 'utf8'), 0)); files_changed = true; - } catch {} + } catch (err) { + console.log(`Failed to add new file '${change.uri}' to working set. ${err.message}`); + } } break; case 2: // change diff --git a/langserver/settings.js b/langserver/settings.js index 5aafc6c..bec7b34 100644 --- a/langserver/settings.js +++ b/langserver/settings.js @@ -25,12 +25,11 @@ const defaultSettings = { return; } this.updateCount += 1; - console.log(`settings set: ${JSON.stringify(values)}`); - for (let key in defaultSettings) { + for (let key in defaultSettings) { if (Object.prototype.hasOwnProperty.call(values, key)) { this[key] = values[key]; } - } + } } } diff --git a/package.json b/package.json index 56d5910..934bea0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "scope": "resource", "type": "string", "default": "app/src/main", - "description": "Workspace-relative path to the app source files. The specified folder should contain AndroidManifest.xml." + "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." }, "android-dev-ext.subscriptionKey": { "scope": "application",