mirror of
https://github.com/adelphes/android-dev-ext.git
synced 2025-12-23 01:48:18 +00:00
initial working language server
This commit is contained in:
337
langserver/server.js
Normal file
337
langserver/server.js
Normal file
@@ -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<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 => {
|
||||
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(/(<p ?.*?>)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>)|(\{@link.+?\}|\{@code.+?\})|(<li>)|(<a href="\{@docRoot\}.*?">.+?<\/a>)|(<h\d>)|<\/?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();
|
||||
|
||||
Reference in New Issue
Block a user