allow configurable app root setting

This commit is contained in:
Dave Holoway
2020-06-28 19:23:39 +01:00
parent 113a7379ed
commit 18049ea08c
2 changed files with 156 additions and 108 deletions

View File

@@ -1,5 +1,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const os = require('os');
const { const {
createConnection, createConnection,
TextDocuments, TextDocuments,
@@ -18,6 +19,7 @@ const {
const { TextDocument } = require('vscode-languageserver-textdocument'); const { TextDocument } = require('vscode-languageserver-textdocument');
const { Settings } = require('./settings');
const { loadAndroidSystemLibrary } = require('./java/java-libraries'); const { loadAndroidSystemLibrary } = require('./java/java-libraries');
const { JavaType, CEIType, ArrayType, PrimitiveType, Method } = require('java-mti'); const { JavaType, CEIType, ArrayType, PrimitiveType, Method } = require('java-mti');
@@ -334,14 +336,43 @@ connection.onInitialized(async () => {
if (hasConfigurationCapability) { if (hasConfigurationCapability) {
// Register for all configuration changes. // Register for all configuration changes.
connection.client.register(DidChangeConfigurationNotification.type, undefined); connection.client.register(DidChangeConfigurationNotification.type, undefined);
const initialSettings = await connection.workspace.getConfiguration({
section: Settings.ID,
});
Settings.set(initialSettings);
} }
if (hasWorkspaceFolderCapability) { if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders((_event) => { connection.workspace.onDidChangeWorkspaceFolders((_event) => {
trace('Workspace folder change event received.'); trace('Workspace folder change event received.');
}); });
} }
const files = await loadWorkingFileList(); const src_folder = await getAppRootFolder();
if (src_folder) {
await rescanSourceFolders(src_folder);
reparse([...liveParsers.keys()], { includeMethods: false, first_parse: true });
}
trace('Initialization complete');
});
/**
* Called during initialization and whenver the App Root setting is changed to scan
* for source files
* @param {string} src_folder absolute path to the source root
*/
async function rescanSourceFolders(src_folder) {
if (!src_folder) {
return;
}
// when the appRoot config value changes and we rescan the folder, we need
// to delete any parsers that were from the old appRoot
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 // 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 // have been loaded (and may be edited) before we reach here
for (let file of files) { for (let file of files) {
@@ -349,27 +380,39 @@ connection.onInitialized(async () => {
continue; continue;
} }
const uri = `file://${file.fpn}`; // todo - handle case-differences on Windows const uri = `file://${file.fpn}`; // todo - handle case-differences on Windows
unused_keys.delete(uri);
if (liveParsers.has(uri)) { if (liveParsers.has(uri)) {
trace(`already loaded: ${uri}`); trace(`already loaded: ${uri}`);
continue; continue;
} }
try { try {
const file_content = await new Promise((res, rej) => fs.readFile(file.fpn, 'UTF8', (err,data) => err ? rej(err) : res(data))); const file_content = await new Promise((res, rej) => fs.readFile(file.fpn, 'UTF8', (err,data) => err ? rej(err) : res(data)));
liveParsers.set(uri, new JavaDocInfo(uri, file_content, 0)); liveParsers.set(uri, new JavaDocInfo(uri, file_content, 0));
} catch {} } catch {}
} }
reparse([...liveParsers.keys()], { includeMethods: false, first_parse: true }); // remove any parsers that are no longer part of the working set
unused_keys.forEach(uri => liveParsers.delete(uri));
}
trace('Initialization complete'); /**
}); * Attempts to locate the app root folder using workspace folders and the appRoot setting
* @returns Absolute path to app root folder or null
*/
async function getAppRootFolder() {
/** @type {string} */
let src_folder = null;
async function loadWorkingFileList() {
const folders = await connection.workspace.getWorkspaceFolders(); const folders = await connection.workspace.getWorkspaceFolders();
let src_folder = ''; if (!folders || !folders.length) {
trace('No workspace folders');
return src_folder;
}
folders.find(folder => { folders.find(folder => {
const main_folder = path.join(folder.uri.replace(/^\w+:\/\//, ''), 'app', 'src', 'main'); const main_folder = path.join(folder.uri.replace(/^\w+:\/\//, ''), Settings.appRoot);
try { try {
if (fs.statSync(main_folder).isDirectory()) { if (fs.statSync(main_folder).isDirectory()) {
src_folder = main_folder; src_folder = main_folder;
@@ -377,36 +420,65 @@ async function loadWorkingFileList() {
} }
} catch {} } catch {}
}); });
if (!src_folder) { if (!src_folder) {
trace(`Failed to find src root from workspace folders:\n - ${folders.map(f => f.uri).join('\n - ')}`); console.log([
return; `Failed to find source root from workspace folders:`,
...folders.map(f => ` - ${f.uri}`),
'Configure the Android App Root value in your workspace settings to point to your source folder containing AndroidManifest.xml',
].join(os.EOL));
} }
trace(`Found src root: ${src_folder}. Beginning search for source files...`); 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...`);
console.time('source file search') console.time('source file search')
const files = scanSourceFiles(src_folder); const files = scanSourceFiles(src_folder);
console.timeEnd('source file search') console.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 Root value in your workspace settings.`)
}
return files; return files;
/** /**
* @param {string} folder * @param {string} base_folder
* @returns {{fpn:string, stat:fs.Stats}[]} * @returns {{fpn:string, relfpn: string, stat:fs.Stats}[]}
*/ */
function scanSourceFiles(folder) { function scanSourceFiles(base_folder) {
const done = new Set(), folders = [folder], files = []; // strip any trailing slash
base_folder = base_folder.replace(/[\\/]+$/, '');
const done = new Set(), folders = [base_folder], files = [];
const max_folders = 100;
while (folders.length) { while (folders.length) {
const folder = folders.shift(); const folder = folders.shift();
if (done.has(folder)) { if (done.has(folder)) {
continue; continue;
} }
done.add(folder); done.add(folder);
if (done.size > max_folders) {
console.log(`Max folder limit reached - cancelling file search`);
break;
}
try { try {
trace(`scan source folder ${folder}`) trace(`scan source folder ${folder}`)
fs.readdirSync(folder) fs.readdirSync(folder)
.forEach(name => { .forEach(name => {
const fpn = path.join(folder, name); const fpn = path.join(folder, name);
const stat = fs.statSync(fpn); const stat = fs.statSync(fpn);
files.push({fpn,stat}); files.push({
fpn,
// relative path (without leading slash)
relfpn: fpn.slice(base_folder.length + 1),
stat,
});
if (stat.isDirectory()) { if (stat.isDirectory()) {
folders.push(fpn) folders.push(fpn)
} }
@@ -419,53 +491,23 @@ async function loadWorkingFileList() {
} }
} }
// The example settings connection.onDidChangeConfiguration(async (change) => {
/** trace(`onDidChangeConfiguration`);
* @typedef ExampleSettings if (change && change.settings && change.settings[Settings.ID]) {
* @property {number} maxNumberOfProblems const old_app_root = Settings.appRoot;
*/ Settings.onChange(change.settings[Settings.ID]);
if (old_app_root !== Settings.appRoot) {
// The global settings, used when the `workspace/configuration` request is not supported by the client. const src_folder = await getAppRootFolder();
// Please note that this is not the case when using this server with the client provided in this example if (src_folder) {
// but could happen with other clients. rescanSourceFolders(src_folder);
const defaultSettings = { maxNumberOfProblems: 1000 }; reparse([...liveParsers.keys()]);
let globalSettings = defaultSettings; }
}
// Cache the settings of all open documents
/** @type {Map<string, Thenable<ExampleSettings>>} */
let documentSettings = new Map();
connection.onDidChangeConfiguration((change) => {
if (hasConfigurationCapability) {
// Reset all cached document settings
documentSettings.clear();
} else {
globalSettings = change.settings.androidJavaLanguageServer || defaultSettings;
} }
})
// Revalidate all open text documents
documents.all().forEach(validateTextDocument);
});
function getDocumentSettings(resource) {
if (!hasConfigurationCapability) {
return Promise.resolve(globalSettings);
}
let result = documentSettings.get(resource);
if (!result) {
result = connection.workspace.getConfiguration({
scopeUri: resource,
section: 'androidJavaLanguageServer',
});
documentSettings.set(resource, result);
}
return result;
}
// Only keep settings for open documents
documents.onDidClose((e) => { documents.onDidClose((e) => {
trace(`doc closed ${e.document.uri}`); trace(`doc closed ${e.document.uri}`);
documentSettings.delete(e.document.uri);
connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] }); connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] });
}); });
@@ -517,54 +559,6 @@ async function validateTextDocument(textDocument) {
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
} }
async function validateTextDocument2(textDocument) {
// In this simple example we get the settings for every validate run.
//let settings = await getDocumentSettings(textDocument.uri);
// The validator creates diagnostics for all uppercase words length 2 and more
let text = textDocument.getText();
let pattern = /\b[A-Z]{2,}\b/g;
let m;
let problems = 0;
let diagnostics = [];
while ((m = pattern.exec(text)) /* && problems < settings.maxNumberOfProblems */) {
problems++;
/** @type {Diagnostic} */
let diagnostic = {
severity: DiagnosticSeverity.Warning,
range: {
start: textDocument.positionAt(m.index),
end: textDocument.positionAt(m.index + m[0].length),
},
message: `${m[0]} is all uppercase.`,
source: 'ex',
};
if (hasDiagnosticRelatedInformationCapability) {
diagnostic.relatedInformation = [
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range),
},
message: 'Spelling matters',
},
{
location: {
uri: textDocument.uri,
range: Object.assign({}, diagnostic.range),
},
message: 'Particularly for names',
},
];
}
diagnostics.push(diagnostic);
}
// Send the computed diagnostics to VS Code.
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
connection.onDidChangeWatchedFiles((_change) => { connection.onDidChangeWatchedFiles((_change) => {
// Monitored files have change in VS Code // Monitored files have change in VS Code
trace('We received a file change event'); trace('We received a file change event');

54
langserver/settings.js Normal file
View File

@@ -0,0 +1,54 @@
const defaultSettings = {
appRoot: 'app/src/main'
}
class AndroidProjectSettings {
/**
* The root of the app source folder.
* This folder should contain AndroidManifest.xml as well as the asets, res, etc folders
*/
appRoot = defaultSettings.appRoot;
/**
* The identifier for the language server settings
*/
ID = 'androidJavaLanguageServer';
static Instance = new AndroidProjectSettings();
/**
* Called when the user edits the settings
* @param {*} values
*/
onChange(values) {
this.set(values);
}
set(values) {
console.log(`settings set: ${JSON.stringify(values)}`);
for (let key in defaultSettings) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
this[key] = values[key];
}
}
}
}
// function getDocumentSettings(resource) {
// if (!hasConfigurationCapability) {
// return Promise.resolve(projectSettings);
// }
// let result = documentSettings.get(resource);
// if (!result) {
// result = connection.workspace.getConfiguration({
// scopeUri: resource,
// section: 'androidJavaLanguageServer',
// });
// documentSettings.set(resource, result);
// }
// return result;
// }
exports.Settings = AndroidProjectSettings.Instance;