diff --git a/extension.js b/extension.js index 617c3a2..bba2bcf 100644 --- a/extension.js +++ b/extension.js @@ -1,11 +1,62 @@ // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below +const path = require('path'); const vscode = require('vscode'); +const { LanguageClient, TransportKind, } = require('vscode-languageclient'); const { AndroidContentProvider } = require('./src/contentprovider'); const { openLogcatWindow } = require('./src/logcat'); const { selectAndroidProcessID } = require('./src/process-attach'); const { selectTargetDevice } = require('./src/utils/device'); +/** @type {LanguageClient} */ +let client; + +function activateLanguageClient(context) { + // The server is implemented in node + let serverModule = context.asAbsolutePath(path.join('langserver', 'server.js')); + // The debug options for the server + // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging + let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] }; + + // If the extension is launched in debug mode then the debug server options are used + // Otherwise the run options are used + /** @type {import('vscode-languageclient').ServerOptions} */ + let serverOptions = { + run: { + module: serverModule, + transport: TransportKind.ipc, + }, + debug: { + module: serverModule, + transport: TransportKind.ipc, + options: debugOptions + } + }; + + // Options to control the language client + /** @type {import('vscode-languageclient').LanguageClientOptions} */ + let clientOptions = { + // Register the server for plain text documents + documentSelector: [{ scheme: 'file', language: 'java' }], + synchronize: { + // Notify the server about file changes to '.java files contained in the workspace + fileEvents: vscode.workspace.createFileSystemWatcher('**/.java') + } + }; + + // Create the language client and start the client. + client = new LanguageClient( + 'androidJavaLanguageServer', + 'Java (Android)', + serverOptions, + clientOptions + ); + + // Start the client. This will also launch the server + return client.start(); +} + + // this method is called when your extension is activated // your extension is activated the very first time the command is executed function activate(context) { @@ -50,6 +101,8 @@ function activate(context) { // the debugger requires a string value to be returned return JSON.stringify(o); }), + + activateLanguageClient(context), ]; context.subscriptions.splice(context.subscriptions.length, 0, ...disposables); diff --git a/langserver/java/mti.js b/langserver/java/mti.js new file mode 100644 index 0000000..1869763 --- /dev/null +++ b/langserver/java/mti.js @@ -0,0 +1,646 @@ + +/** + * @param {number} ref + * @param {MTI} mti + * @returns {string} + */ +function packageNameFromRef(ref, mti) { + if (typeof ref !== 'number') { + return null; + } + if (ref < 16) { + return KnownPackages[ref]; + } + return mti.minified.rp[ref - 16]; +} + +/** + * @param {number} ref + * @param {MTI} mti + */ +function typeFromRef(ref, mti) { + if (typeof ref !== 'number') { + return null; + } + if (ref < 16) { + return KnownTypes[ref]; + } + return mti.referenced.types[ref - 16]; +} + +function indent(s) { + return '\n' + s.split('\n').map(s => ` ${s}`).join('\n'); +} + +class MinifiableInfo { + + constructor(minified) { + this.minified = minified; + } + + /** + * Format a commented form of docs with a newline at the end. + */ + fmtdocs() { + // the docs field is always d in the minified objects + const d = this.minified.d; + return d ? `/**\n * ${d.replace(/\n/g,'\n *')}\n */\n` : ''; + } +} + +/** + * Minified Type Information + * + * Each MTI instance represents a Java unit (a single source file or a compiled class file). + * The mti JSON format is minimalistic to keep the size small - the Android framework has over 8000 classes in + * it, so keeping the information as small as possible is beneficial. + * ``` + mti: { + rp:[], // referenced packages + rt:[], // referenced types + it:[{ // implemented types + m:0, // type modifiers + n:'', // type name (in X$Y format) + p:null, // owner package + v:[], // type vars + e:null, // extends 0(class)/[0,...](interface)/null(unknown) + i:[], // implements [0,...] + c:[], // constructors [{m:0,p:[]},...] + f:[], // fields {m:0,n:'',t:0}, + g:[], // methods {n:'',s:[{m:0,t:0,p:[{m:0,t:0,n:''},...]}]} + u:[], // subtypes [0,...] + d:'', // type docs + }] + }, + ``` + */ +class MTI extends MinifiableInfo { + + /** + * @param {{rp:[], rt:[], it:[]}} mti + */ + constructor(mti) { + super(mti); + // initialise the lists of referenced packages and types + this.referenced = { + /** @type {string[]} */ + packages: mti.rp, + + /** @type {ReferencedType[]} */ + types: [], + } + // because ReferencedType can make use of earlier reference types, we must add them sequentially + // instead of using mti.rt.map() + for (let t of mti.rt) { + this.referenced.types.push(new ReferencedType(this, t)) + } + + // add the types implemented by this unit + this.types = mti.it.map(it => new MTIType(this, it)); + } + + /** + * Unpack all the classes from the given JSON + * @param {string} filename + */ + static unpackJSON(filename) { + const o = JSON.parse(require('fs').readFileSync(filename, 'utf8')); + delete o.NOTICES; + const types = []; + for (let pkg in o) { + for (let cls in o[pkg]) { + const unit = new MTI(o[pkg][cls]); + types.push(...unit.types); + } + } + return { + packages: Object.keys(o).sort(), + types: types.sort((a,b) => a.minified.n.localeCompare(b.minified.n)), + } + } +} + +/** + * A ReferencedType encodes a type used by a class, interface or enum. + * ``` + * { + * n: string | typeref - name or base typeref (for arrays and generic types) + * p?: pkgref - package the type is declared in (undefined for primitives) + * g?: typeref[] - generic type parameters + * a?: number - array dimensions + * } + * ``` + * + * A typeref value < 16 is a lookup into the KnownTypes array. + * + * All other types have a typeref >= 16 and an associated package reference. + * + * The packageref is a lookup into the MTIs pt array which lists package names. + */ +class ReferencedType extends MinifiableInfo { + + /** + * @param {MTI} unit + * @param {*} mti + * @param {string|false} [pkg_or_prim] predefined package name, an empty string for default packages or false for primitives + * @param {*} [default_value] + */ + constructor(unit, mti, pkg_or_prim, default_value = null) { + super(mti); + let baseType; + if (typeof mti.n === 'number') { + baseType = typeFromRef(mti.n, unit); + } + this.parsed = { + package: pkg_or_prim + || ((pkg_or_prim === false) + ? undefined + : packageNameFromRef(mti.p, unit) + ), + + /** @type {ReferencedType} */ + baseType, + + /** @type {ReferencedType[]} */ + typeParams: mti.g && mti.g.map(t => typeFromRef(t, unit)), + + /** @type {string} */ + arr: '[]'.repeat(mti.a | 0), + } + this.defaultValue = default_value; + } + + get isPrimitive() { return this.parsed.package === undefined } + + get package() { return this.parsed.package } + + get name() { + // note: names in enclosed types are in x$y format + const n = this.parsed.baseType ? this.parsed.baseType.name : this.minified.n; + const type_params = this.parsed.typeParams + ? `<${this.parsed.typeParams.map(tp => tp.name).join(',')}>` + : '' + return `${n}${type_params}${this.parsed.arr}`; + } + + get dottedName() { + return this.name.replace(/[$]/g, '.'); + } +} + + +/** + * MTIType encodes a complete type (class, interface or enum) + * ``` + * { + * d: string - type docs + * p: pkgref - the package this type belongs to + * n: string - type name (in x$y format for enclosed types) + * v: typeref[] - generic type variables + * e: typeref | typeref[] - super/extends type (single value for classes, array for interfaces) + * i: typeref[] - interface types + * f: mtifield[] - fields + * c: mtictrs[] - constructors + * g: mtimethod[] - methods + * } + * ``` + */ +class MTIType extends MinifiableInfo { + + /** + * @param {MTI} unit + * @param {*} mti + */ + constructor(unit, mti) { + super(mti); + this.parsed = { + package: packageNameFromRef(mti.p, unit), + + /** @type {ReferencedType[]} */ + typevars: mti.v.map(v => typeFromRef(v, unit)), + + /** @type {ReferencedType|ReferencedType[]} */ + extends: Array.isArray(mti.e) + ? mti.e.map(e => typeFromRef(e, unit)) + : typeFromRef(mti.e, unit), + + /** @type {ReferencedType[]} */ + implements: mti.i.map(i => typeFromRef(i, unit)), + + /** @type {MTIField[]} */ + fields: mti.f.map(f => new MTIField(unit, f)), + + /** @type {MTIConstructor[]} */ + constructors: mti.c.map(c => new MTIConstructor(unit, c)), + + /** + * MTI method are grouped by name - we split them here + * @type {MTIMethod[]} + */ + methods: mti.g.reduce((arr, m) => [...arr, ...MTIMethod.split(unit, this, m)], []), + } + } + + /** + * type docs + * @type {string} + */ + get docs() { return this.minified.d } + + /** + * type modifiers + * @type {number} + */ + get modifiers() { return this.minified.m } + + /** + * type name (in x$y format for enclosed types) + * @type {string} + */ + get name() { return this.minified.n } + + get dottedRawName() { return this.minified.n.replace(/[$]/g, '.') }; + + get dottedName() { + const t = this.typevars.map(t => t.name).join(','); + return t ? `${this.dottedRawName}<${t}>` : this.dottedRawName; + }; + + /** + * type name with no qualifiers + * @type {string} + */ + get simpleRawName() { return this.minified.n.match(/[^$]+$/)[0] } + + /** + * package this type belongs to + */ + get package() { return this.parsed.package } + + get typeKind() { + const m = this.minified.m; + return (m & TypeModifiers.enum) + ? 'enum' : (m & TypeModifiers.interface) + ? 'interface' : (m & TypeModifiers['@interface']) + ? '@interface' : 'class'; + } + + /** + * generic type variables + */ + get typevars() { return this.parsed.typevars } + + /** + * class or interface extends. + * Note that classes have a single extend type, but interfaces have an array. + */ + get extends() { return this.parsed.extends } + + /** + * class implements + */ + get implements() { return this.parsed.implements } + + /** + * @type {MTIConstructor[]} + */ + get constructors() { return this.parsed.constructors } + + /** + * @type {MTIField[]} + */ + get fields() { return this.parsed.fields } + + /** + * @type {MTIMethod[]} + */ + get methods() { return this.parsed.methods } + + toSource() { + let constructors = [], typevars = '', ex = '', imp = ''; + + // only add constructors if there's more than just the default constructor + if (!((this.constructors.length === 1) && (this.constructors[0].parameters.length === 0))) { + constructors = this.constructors; + } + + if (this.typevars.length) { + typevars = `<${this.typevars.map(tv => tv.name).join(',')}>`; + } + + if (this.extends) { + // only add extends if it's not derived from java.lang.Object + if (this.extends !== KnownTypes[3]) { + const x = Array.isArray(this.extends) ? this.extends : [this.extends]; + ex = `extends ${x.map(type => type.dottedName).join(', ')} `; + } + } + + if (this.implements.length) { + imp = `implements ${this.implements.map(type => type.dottedName).join(', ')} `; + } + + return [ + `${this.fmtdocs()}${typemods(this.modifiers)} ${this.simpleRawName}${typevars} ${ex}${imp}{`, + ...this.fields.map(f => indent(f.toSource())), + ...constructors.map(c => indent(c.toSource())), + ...this.methods.map(m => indent(m.toSource())), + `}` + ].join('\n'); + } +} + +/** + * MTIField encodes a single type field. + * ``` + * { + * d: string - docs + * m: number - access modifiers + * n: string - field name + * t: typeref - field type + * } + * ``` + */ +class MTIField extends MinifiableInfo { + + /** + * @param {MTI} owner + * @param {*} mti + */ + constructor(owner, mti) { + super(mti); + this.parsed = { + type: typeFromRef(mti.t, owner), + }; + } + + /** + * @type {number} + */ + get modifiers() { return this.minified.m } + + /** + * @type {string} + */ + get docs() { return this.minified.d } + + /** + * @type {string} + */ + get name() { return this.minified.n } + + /** + * @type {ReferencedType} + */ + get type() { return this.parsed.type } + + toSource() { + return `${this.fmtdocs()}${access(this.modifiers)}${this.type.dottedName} ${this.name} = ${this.type.defaultValue};` + } +} + +class MTIMethodBase extends MinifiableInfo {} + +/** + * MTIContructor encodes a single type constructor. + * ``` + * { + * d: string - docs + * m: number - access modifiers + * p: mtiparam[] - constructor parameters + * } + * ``` + */ +class MTIConstructor extends MTIMethodBase { + + /** + * @param {MTI} owner + * @param {*} mti + */ + constructor(owner, mti) { + super(mti); + this.parsed = { + typename: owner.minified.it[0].n, + /** @type {MTIParameter[]} */ + parameters: mti.p.map(p => new MTIParameter(owner, p)), + } + } + + /** + * @type {number} + */ + get modifiers() { return this.minified.m } + + get docs() { return this.minified.d } + + /** + * @type {MTIParameter[]} + */ + get parameters() { return this.parsed.parameters } + + toSource() { + const typename = this.parsed.typename.split('$').pop(); + return `${this.fmtdocs()}${access(this.modifiers)}${typename}(${this.parameters.map(p => p.toSource()).join(', ')}) {}` + } +} + +/** + * MTIMethod encodes a single type method. + * + * In minified form, methods are encoded as overloads - each entry + * has a single name with one or more method signatures. + * ``` + * { + * d: string - docs + * n: string - method name + * s: [{ + * m: number - access modifiers + * t: typeref - return type + * p: mtiparam[] - method parameters + * }, + * ... + * ] + * + * } + * ``` + */ + class MTIMethod extends MTIMethodBase { + + /** + * @param {MTI} unit + * @param {MTIType} type + * @param {string} name + * @param {*} mti + */ + constructor(unit, type, name, mti) { + super(mti); + this.interfaceMethod = type.modifiers & 0x200; + this.parsed = { + name, + /** @type {MTIParameter[]} */ + parameters: mti.p.map(p => new MTIParameter(unit, p)), + /** @type {ReferencedType} */ + return_type: typeFromRef(mti.t, unit), + } + } + + /** + * @param {MTI} unit + * @param {MTIType} type + * @param {*} mti + */ + static split(unit, type, mti) { + return mti.s.map(s => new MTIMethod(unit, type, mti.n, s)); + } + + /** + * @type {string} + */ + get docs() { return this.minified.d } + + /** + * @type {number} + */ + get modifiers() { return this.minified.m } + + /** + * @type {ReferencedType} + */ + get return_type() { return this.parsed.return_type } + + /** + * @type {string} + */ + get name() { return this.parsed.name } + + /** + * @type {MTIParameter[]} + */ + get parameters() { return this.parsed.parameters } + + toSource() { + let m = this.modifiers, body = ' {}'; + if (m & 0x400) { + body = ';'; // abstract method - no body + } else if (this.return_type.name !== 'void') { + body = ` { return ${this.return_type.defaultValue}; }`; + } + if (this.interfaceMethod) { + m &= ~0x400; // exclude abstract modifier as it's redundant + } + return `${this.fmtdocs()}${access(m)}${this.return_type.dottedName} ${this.name}(${this.parameters.map(p => p.toSource()).join(', ')})${body}` + } +} + +/** + * MTIParameter encodes a single method or constructor paramter + * ``` + * { + * m?: number - access modifiers (only 'final' is allowed) + * t: typeref - parameter type + * n: string - parameter name + * } + * ``` + */ +class MTIParameter extends MinifiableInfo { + + /** + * @param {MTI} owner + * @param {*} mti + */ + constructor(owner, mti) { + super(mti); + this.parsed = { + type: typeFromRef(mti.t, owner) + } + } + + /** + * @type {number} + */ + get modifiers() { return this.minified.m | 0 } + + /** + * @type {string} + */ + get name() { return this.minified.n } + + /** + * @type {ReferencedType} + */ + get type() { return this.parsed.type } + + toSource() { + return `${access(this.modifiers)}${this.type.dottedName} ${this.name}` + } +} + +const access_keywords = 'public private protected static final synchronized volatile transient native interface abstract strict'.split(' '); + +/** + * @param {number} modifier_bits + */ +function access(modifier_bits) { + // convert the modifier bits into keywords + const decls = access_keywords.filter((_,i) => modifier_bits & (1 << i)); + if (decls.length) { + decls.push(''); // make sure we end with a space + } + return decls.join(' '); +} + +const TypeModifiers = { + public: 0b0000_0000_0000_0001, // 0x1 + final: 0b0000_0000_0001_0000, // 0x10 + interface: 0b0000_0010_0000_0000, // 0x200 + abstract: 0b0000_0100_0000_0000, // 0x400 + '@interface': 0b0010_0000_0000_0000, // 0x2000 + enum: 0b0100_0000_0000_0000, // 0x4000 +} + +/** + * @param {number} modifier_bits + */ +function typemods(modifier_bits) { + const modifiers = []; + let type = 'class'; + if (modifier_bits & TypeModifiers.interface) { + type = 'interface'; + modifier_bits &= ~TypeModifiers.abstract; // ignore abstract keyword for interfaces + } else if (modifier_bits & TypeModifiers['@interface']) { + type = '@interface'; + } else if (modifier_bits & TypeModifiers.enum) { + type = 'enum'; + modifier_bits &= ~TypeModifiers.final; // ignore final keyword for enums + } + if (modifier_bits & TypeModifiers.public) modifiers.push('public'); + if (modifier_bits & TypeModifiers.final) modifiers.push('final'); + if (modifier_bits & TypeModifiers.abstract) modifiers.push('abstract'); + modifiers.push(type); + return modifiers.join(' '); +} + +/** + * List of known/common packages. + * These are used/encoded as pkgrefs between 0 and 15. + */ +const KnownPackages = ["java.lang","java.io","java.util",""]; + +/** + * Literals corresponding to the KnownTypes. + * These are used for method return values and field expressions when constructing source. + */ +const KnownTypeValues = ['','0','""','null','false',"'\\0'",'0','0l','0','0.0f','0.0d','null']; + +/** + * List of known/common types. + * These are used/encoded as typerefs between 0 and 15. + */ +const KnownTypes = [ + "void","int","String","Object","boolean","char","byte","long","short","float","double","Class" +].map((n,i) => { + const pkg_or_prim = /^[SOC]/.test(n) ? KnownPackages[0] : false; + return new ReferencedType(null, {n}, pkg_or_prim, KnownTypeValues[i]); +}); + +module.exports = MTI; diff --git a/langserver/package-lock.json b/langserver/package-lock.json new file mode 100644 index 0000000..dfc0e92 --- /dev/null +++ b/langserver/package-lock.json @@ -0,0 +1,56 @@ +{ + "name": "langserver", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/node": { + "version": "13.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz", + "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==", + "dev": true + }, + "jarscanner": { + "version": "file:../../../nodejs/jarscanner", + "dependencies": { + "@types/node": { + "version": "13.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz", + "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==" + } + } + }, + "vscode-jsonrpc": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz", + "integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==" + }, + "vscode-languageserver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz", + "integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==", + "requires": { + "vscode-languageserver-protocol": "^3.15.3" + } + }, + "vscode-languageserver-protocol": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz", + "integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==", + "requires": { + "vscode-jsonrpc": "^5.0.1", + "vscode-languageserver-types": "3.15.1" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz", + "integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==" + }, + "vscode-languageserver-types": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz", + "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==" + } + } +} diff --git a/langserver/package.json b/langserver/package.json new file mode 100644 index 0000000..7a90121 --- /dev/null +++ b/langserver/package.json @@ -0,0 +1,19 @@ +{ + "name": "langserver", + "version": "1.0.0", + "description": "Java language server for Android development", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "jarscanner": "file://~/dev/nodejs/jarscanner", + "vscode-languageserver": "^6.1.1", + "vscode-languageserver-textdocument": "^1.0.1" + }, + "devDependencies": { + "@types/node": "^13.13.4" + } +} diff --git a/langserver/server.js b/langserver/server.js new file mode 100644 index 0000000..af54419 --- /dev/null +++ b/langserver/server.js @@ -0,0 +1,337 @@ +const fs = require('fs'); +const { + createConnection, + TextDocuments, + //TextDocument, + Diagnostic, + DiagnosticSeverity, + ProposedFeatures, + //InitializeParams, + DidChangeConfigurationNotification, + CompletionItem, + CompletionItemKind, + TextDocumentSyncKind, + //TextDocumentPositionParams + } = require('vscode-languageserver'); + + const { TextDocument } = require('vscode-languageserver-textdocument'); + + const MTI = require('./java/mti'); + let androidLibrary = null; + function loadAndroidLibrary(retry) { + try { + androidLibrary = MTI.unpackJSON('/tmp/jarscanner/android-25/android-25.json'); + connection.console.log(`Android type cache loaded: ${androidLibrary.types.length} types from ${androidLibrary.packages.length} packages.`); + } catch (e) { + connection.console.log(`Failed to load android type cache`); + if (retry) { + return; + } + connection.console.log(`Rebuilding type cache...`); + const jarscanner = require(`jarscanner/jarscanner`); + fs.mkdir('/tmp/jarscanner', err => { + if (err) { + connection.console.log(`Cannot create type cache folder. ${err.message}.`); + return + } + jarscanner.process_android_sdk_source({ + destpath: '/tmp/jarscanner', + sdkpath: process.env['ANDROID_SDK'], + api: 25, + cleandest: true, + }, (err) => { + if (err) { + connection.console.log(`Android cache build failed. ${err.message}.`); + return + } + loadAndroidLibrary(true); + }) + }) + } + } + + // Create a connection for the server. The connection uses Node's IPC as a transport. + // Also include all preview / proposed LSP features. + let connection = createConnection(ProposedFeatures.all); + + // Create a simple text document manager. The text document manager + // supports full document sync only + let documents = new TextDocuments({ + /** + * + * @param {string} uri + * @param {string} languageId + * @param {number} version + * @param {string} content + */ + create(uri, languageId, version, content) { + connection.console.log(JSON.stringify({what:'create',uri,languageId,version,content})); + }, + /** + * + * @param {*} document + * @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes + * @param {number} version + */ + update(document, changes, version) { + connection.console.log(JSON.stringify({what:'update',changes,version})); + } + + }); + + let hasConfigurationCapability = false; + let hasWorkspaceFolderCapability = false; + let hasDiagnosticRelatedInformationCapability = false; + + connection.onInitialize((params) => { + process.nextTick(loadAndroidLibrary); + let capabilities = params.capabilities; + + // Does the client support the `workspace/configuration` request? + // If not, we will fall back using global settings + hasConfigurationCapability = + capabilities.workspace && !!capabilities.workspace.configuration; + + hasWorkspaceFolderCapability = + capabilities.workspace && !!capabilities.workspace.workspaceFolders; + + hasDiagnosticRelatedInformationCapability = + capabilities.textDocument && + capabilities.textDocument.publishDiagnostics && + capabilities.textDocument.publishDiagnostics.relatedInformation; + + return { + capabilities: { + textDocumentSync: TextDocumentSyncKind.Incremental, + // Tell the client that the server supports code completion + completionProvider: { + resolveProvider: true + } + } + }; + }); + + connection.onInitialized(() => { + if (hasConfigurationCapability) { + // Register for all configuration changes. + connection.client.register(DidChangeConfigurationNotification.type, undefined); + } + if (hasWorkspaceFolderCapability) { + connection.workspace.onDidChangeWorkspaceFolders(_event => { + connection.console.log('Workspace folder change event received.'); + }); + } + }); + + // The example settings + /** + * @typedef ExampleSettings + * @property {number} maxNumberOfProblems + */ + + // The global settings, used when the `workspace/configuration` request is not supported by the client. + // Please note that this is not the case when using this server with the client provided in this example + // but could happen with other clients. + const defaultSettings = { maxNumberOfProblems: 1000 }; + let globalSettings = defaultSettings; + + // Cache the settings of all open documents + /** @type {Map>} */ + 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 => { + documentSettings.delete(e.document.uri); + }); + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + // documents.onDidChangeContent(change => { + // connection.console.log(JSON.stringify(change)); + //validateTextDocument(change.document); + // }); + + /** + * @param {TextDocument} textDocument + */ + async function validateTextDocument(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 => { + // Monitored files have change in VS Code + connection.console.log('We received a file change event'); + }); + + // This handler provides the initial list of the completion items. + let allCompletionTypes = null; + connection.onCompletion( + /** + * @param {*} _textDocumentPosition TextDocumentPositionParams + */ + (_textDocumentPosition) => { + // The pass parameter contains the position of the text document in + // which code complete got requested. For the example we ignore this + // info and always provide the same completion items. + const lib = androidLibrary; + if (!lib) return []; + const typeKindMap = { + 'class':CompletionItemKind.Class, + 'interface': CompletionItemKind.Interface, + '@interface': CompletionItemKind.Interface, + 'enum': CompletionItemKind.Enum, + }; + return allCompletionTypes || (allCompletionTypes = lib.types.map((t,idx) => + /** @type {CompletionItem} */ + ({ + label: t.dottedRawName, + kind: typeKindMap[t.typeKind], + data: idx + }) + )); + return [ + { + label: 'TypeScript', + kind: CompletionItemKind.Text, + data: 1 + }, + { + label: 'JavaScript', + kind: CompletionItemKind.Text, + data: 2 + } + ]; + } + ); + + // This handler resolves additional information for the item selected in + // the completion list. + connection.onCompletionResolve( + /** + * @param {CompletionItem} item + */ + (item) => { + const t = androidLibrary.types[item.data]; + item.detail = `${t.package}.${t.dottedRawName}`; + item.documentation = t.docs && { + kind: "markdown", + value: `${t.typeKind} **${t.dottedName}**\n\n${ + t.docs + .replace(/(

)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>)|(\{@link.+?\}|\{@code.+?\})|(

  • )|(.+?<\/a>)|()|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,p,i,b,tt,c,lc,li,a,h) => { + return p ? '\n\n' + : i ? '*' + : b ? '**' + : tt ? '`' + : c ? '\n```' + : lc ? lc.replace(/\{@\w+\s*(.+)\}/, (_,x) => `\`${x.trim()}\``) + : li ? '\n- ' + : a ? a.replace(/.+?\{@docRoot\}(.*?)">(.+?)<\/a>/m, (_,p,t) => `[${t}](https://developer.android.com/${p})`) + : h ? `\n${'#'.repeat(1 + parseInt(h.slice(2,-1),10))} ` + : ''; + }) + }`, + } + return item; + } + ); + + /* + connection.onDidOpenTextDocument((params) => { + // A text document got opened in VS Code. + // params.uri uniquely identifies the document. For documents store on disk this is a file URI. + // params.text the initial full content of the document. + connection.console.log(`${params.textDocument.uri} opened.`); + }); + connection.onDidChangeTextDocument((params) => { + // The content of a text document did change in VS Code. + // params.uri uniquely identifies the document. + // params.contentChanges describe the content changes to the document. + connection.console.log(`${params.textDocument.uri} changed: ${JSON.stringify(params.contentChanges)}`); + }); + connection.onDidCloseTextDocument((params) => { + // A text document got closed in VS Code. + // params.uri uniquely identifies the document. + connection.console.log(`${params.textDocument.uri} closed.`); + }); + */ + + // Make the text document manager listen on the connection + // for open, change and close text document events + documents.listen(connection); + + // Listen on the connection + connection.listen(); + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2303692..fefe200 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1232,6 +1232,41 @@ "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.40.0.tgz", "integrity": "sha512-Fwze+9qbLDPuQUhtITJSu/Vk6zIuakNM1iR2ZiZRgRaMEgBpMs2JSKaT0chrhJHCOy6/UbpsUbUBIseF6msV+g==" }, + "vscode-jsonrpc": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz", + "integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==" + }, + "vscode-languageclient": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-6.1.3.tgz", + "integrity": "sha512-YciJxk08iU5LmWu7j5dUt9/1OLjokKET6rME3cI4BRpiF6HZlusm2ZwPt0MYJ0lV5y43sZsQHhyon2xBg4ZJVA==", + "requires": { + "semver": "^6.3.0", + "vscode-languageserver-protocol": "^3.15.3" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "vscode-languageserver-protocol": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz", + "integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==", + "requires": { + "vscode-jsonrpc": "^5.0.1", + "vscode-languageserver-types": "3.15.1" + } + }, + "vscode-languageserver-types": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz", + "integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==" + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index 09b0109..44e67a0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "activationEvents": [ "onCommand:android-dev-ext.view_logcat", "onCommand:PickAndroidDevice", - "onCommand:PickAndroidProcess" + "onCommand:PickAndroidProcess", + "onLanguage:java" ], "repository": { "type": "git", @@ -28,6 +29,18 @@ }, "main": "./extension", "contributes": { + "configuration": { + "type": "object", + "title": "Java (Android)", + "properties": { + "androidJavaLanguageServer.maxNumberOfProblems": { + "scope": "resource", + "type": "number", + "default": 100, + "description": "Controls the maximum number of problems produced by the server." + } + } + }, "commands": [ { "command": "android-dev-ext.view_logcat", @@ -228,6 +241,7 @@ "uuid": "^3.3.2", "vscode-debugadapter": "^1.40.0", "vscode-debugprotocol": "^1.40.0", + "vscode-languageclient": "6.1.3", "ws": "^7.1.2", "xmldom": "^0.1.27", "xpath": "^0.0.27"