const fs = require('fs'); const { createConnection, TextDocuments, //TextDocument, Diagnostic, DiagnosticSeverity, ProposedFeatures, //InitializeParams, DidChangeConfigurationNotification, CompletionItem, CompletionItemKind, TextDocumentSyncKind, Position, //TextDocumentPositionParams } = require('vscode-languageserver'); const { TextDocument } = require('vscode-languageserver-textdocument'); const { loadAndroidLibrary, JavaType } = require('java-mti'); const { ParseProblem } = require('./java/parser'); const { parse, ModuleBlock } = require('./java/parser9'); const { validate } = require('./java/validater'); /** * @typedef {Map} AndroidLibrary * @type {AndroidLibrary|Promise} */ let androidLibrary = null; // 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); /** * @typedef LiveParseInfo * @property {string} uri * @property {JavaTokenizer.LineInfo[]} lines * @property {{startState: string, states: string[], endState: string}[]} states */ ///** @type {LiveParseInfo[]} */ //const liveParsers = []; /** @type {{content: string, uri: string, result: ModuleBlock, positionAt:(n) => Position, indexAt:(p:Position) => number}} */ let parsed = null; function reparse(uri, content) { const result = parse(content); parsed = { content, uri, result, positionAt(n) { let line = 0, last_nl_idx = 0, character = 0; if (n <= 0) return { line, character }; for (let idx = 0; ;) { idx = this.content.indexOf('\n', idx) + 1; if (idx === 0 || idx > n) { if (idx === 0) n = content.length; character = n - last_nl_idx; return { line, character }; } last_nl_idx = idx; line++; } }, indexAt(pos) { let idx = 0; for (let i = 0; i < pos.line; i++) { idx = this.content.indexOf('\n', idx) + 1; if (idx === 0) { return this.content.length; } } return Math.min(idx + pos.character, this.content.length); }, }; } // 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})); // tokenize the file content and build the initial parse state connection.console.log(`create parse ${version}`); reparse(uri, content); //connection.console.log(res.imports.length.toString()); // const lines = JavaTokenizer.get().tokenizeSource(content); // const initialParse = new JavaParser().parseLines(lines); // liveParsers.push({ // uri, // lines, // states: initialParse, // }) // console.log(initialParse.map(x => x.decls).filter(x => x.length).map(x => JSON.stringify(x, null, ' '))); return { uri }; }, /** * * @param {TextDocument} document * @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes * @param {number} version */ update(document, changes, version) { connection.console.log(JSON.stringify({ what: 'update', /* changes, */ version })); //connection.console.log(`update ${version}`); //return document; if (parsed && document && parsed.uri === document.uri) { changes.forEach((change) => { /** @type {import('vscode-languageserver').Range} */ const r = change['range']; if (r) { const start_index = parsed.indexAt(r.start); let end_index = start_index + (r.end.character - r.start.character); if (r.end.line !== r.start.line) end_index = parsed.indexAt(r.end); parsed.content = `${parsed.content.slice(0, start_index)}${change.text}${parsed.content.slice(end_index)}`; } }); //connection.console.log(JSON.stringify(parsed.content)); reparse(document.uri, parsed.content); } return document; }, }); let hasConfigurationCapability = false; let hasWorkspaceFolderCapability = false; let hasDiagnosticRelatedInformationCapability = false; connection.onInitialize((params) => { console.time('android-library-load') androidLibrary = loadAndroidLibrary('android-25').then(lib => { console.timeEnd('android-library-load') return androidLibrary = lib; }, err => { console.log(`android library load failed: ${err.message}`); return androidLibrary = new Map(); }); 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) => { connection.console.log('doc closed'); parsed = null; 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 {{uri}} textDocument */ async function validateTextDocument(textDocument) { if (androidLibrary instanceof Promise) { connection.console.log('Waiting for Android Library load'); androidLibrary = await androidLibrary; } /** @type {ParseProblem[]} */ let problems = []; connection.console.log('validateTextDocument'); if (parsed && parsed.result) { try { problems = validate(parsed.result, androidLibrary); } catch(err) { console.error(err); } } const diagnostics = problems .filter((p) => p) .map((p) => { const start = parsed.positionAt(p.startIdx); const end = parsed.positionAt(p.endIdx); /** @type {Diagnostic} */ let diagnostic = { severity: p.severity, range: { start, end, }, message: p.message, source: 'java-android', }; return diagnostic; }); 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) => { // 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 */ async (_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. if (androidLibrary instanceof Promise) { androidLibrary = await androidLibrary; } const lib = androidLibrary; if (!lib) return []; const typeKindMap = { class: CompletionItemKind.Class, interface: CompletionItemKind.Interface, '@interface': CompletionItemKind.Interface, enum: CompletionItemKind.Enum, }; return ( allCompletionTypes || (allCompletionTypes = [ ...'boolean byte char double float int long short void'.split(' ').map((t) => ({ label: t, kind: CompletionItemKind.Keyword, data: -1, })), ...'public private protected static final abstract volatile native'.split(' ').map((t) => ({ label: t, kind: CompletionItemKind.Keyword, data: -1, })), ...'false true null'.split(' ').map((t) => ({ label: t, kind: CompletionItemKind.Value, data: -1, })), ...[...lib.values()].map( (t, idx) => /** @type {CompletionItem} */ ({ label: t.dottedTypeName, kind: typeKindMap[t.typeKind], data: t.shortSignature, }) ), ]) ); } ); // This handler resolves additional information for the item selected in // the completion list. connection.onCompletionResolve( /** * @param {CompletionItem} item */ (item) => { if (androidLibrary instanceof Promise) { return item; } const t = androidLibrary.get(item.data); if (!t) { return item; } item.detail = t.fullyDottedRawName; item.documentation = t.docs && { kind: 'markdown', value: `${t.typeKind} **${t.dottedTypeName}**\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();