Skip to content

Commit

Permalink
Generate outline for .mo files.
Browse files Browse the repository at this point in the history
  - There is still some error for types.
  • Loading branch information
AnHeuermann committed Dec 18, 2023
1 parent 3765522 commit 64e34d3
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 214 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"lint": "eslint ./client/src ./server/src --ext .ts,.tsx",
"postinstall": "cd client && npm install && cd ../server && npm install && cd ..",
"test": "sh ./scripts/e2e.sh",
"test:server": "cd server && npx mocha -r ts-node/register src/test/**/*.test.ts"
"test:server": "cd server && npx mocha -r ts-node/register src/test/**/*.test.ts src/util/test/**/*.test.ts"
},
"devDependencies": {
"@types/mocha": "^10.0.6",
Expand Down
6 changes: 3 additions & 3 deletions server/src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default class Analyzer {
logger.debug(tree.rootNode.toString());

// Get declarations
const declarations = getAllDeclarationsInTree({ tree, uri });
const declarations = getAllDeclarationsInTree(tree, uri);

// Update saved analysis for document uri
this.uriToAnalyzedDocument[uri] = {
Expand All @@ -59,13 +59,13 @@ export default class Analyzer {
*
* TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document.
*/
public getDeclarationsForUri({ uri }: { uri: string }): LSP.SymbolInformation[] {
public getDeclarationsForUri(uri: string): LSP.SymbolInformation[] {
const tree = this.uriToAnalyzedDocument[uri]?.tree;

if (!tree?.rootNode) {
return [];
}

return getAllDeclarationsInTree({ uri, tree });
return getAllDeclarationsInTree(tree, uri);
}
}
8 changes: 7 additions & 1 deletion server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,18 @@ export class ModelicaServer {
const diagnostics = this.analyzer.analyze(document);
}

/**
* Provide symbols defined in document.
*
* @param params Unused.
* @returns Symbol information.
*/
private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] {
// TODO: ideally this should return LSP.DocumentSymbol[] instead of LSP.SymbolInformation[]
// which is a hierarchy of symbols.
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol
logger.debug(`onDocumentSymbol`);
return this.analyzer.getDeclarationsForUri({ uri: params.textDocument.uri });
return this.analyzer.getDeclarationsForUri(params.textDocument.uri);
}

}
Expand Down
256 changes: 61 additions & 195 deletions server/src/util/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ import * as Parser from 'web-tree-sitter';
import * as TreeSitterUtil from './tree-sitter';
import { logger } from './logger';

const TREE_SITTER_TYPE_TO_LSP_KIND: { [type: string]: LSP.SymbolKind | undefined } = {
// These keys are using underscores as that's the naming convention in tree-sitter.
environment_variable_assignment: LSP.SymbolKind.Variable,
function_definition: LSP.SymbolKind.Function,
variable_assignment: LSP.SymbolKind.Variable,
};
const isEmpty = (data: string): boolean => typeof data === "string" && data.trim().length == 0;

export type GlobalDeclarations = { [word: string]: LSP.SymbolInformation }
export type Declarations = { [word: string]: LSP.SymbolInformation[] }
Expand All @@ -27,58 +22,18 @@ const GLOBAL_DECLARATION_LEAF_NODE_TYPES = new Set([
'function_definition',
]);

/**
* Returns declarations (functions or variables) from a given root node
* that would be available after sourcing the file. This currently does
* not include global variables defined inside if statements or functions
* as we do not do any flow tracing.
*
* Will only return one declaration per symbol name – the latest definition.
* This behavior is consistent with how Bash behaves, but differs between
* LSP servers.
*
* Used when finding declarations for sourced files and to get declarations
* for the entire workspace.
*/
export function getGlobalDeclarations({
tree,
uri,
}: {
tree: Parser.Tree
uri: string
}): GlobalDeclarations {
const globalDeclarations: GlobalDeclarations = {};

TreeSitterUtil.forEach(tree.rootNode, (node) => {
const followChildren = !GLOBAL_DECLARATION_LEAF_NODE_TYPES.has(node.type);

const symbol = getDeclarationSymbolFromNode({ node, uri });
if (symbol) {
const word = symbol.name;
globalDeclarations[word] = symbol;
}

return followChildren;
});

return globalDeclarations;
}

/**
* Returns all declarations (functions or variables) from a given tree.
* This includes local variables.
*
* @param tree Tree-sitter tree.
* @param uri The document's uri.
* @returns Symbol information for all declarations.
*/
export function getAllDeclarationsInTree({
tree,
uri,
}: {
tree: Parser.Tree
uri: string
}): LSP.SymbolInformation[] {
export function getAllDeclarationsInTree(tree: Parser.Tree, uri: string): LSP.SymbolInformation[] {
const symbols: LSP.SymbolInformation[] = [];

TreeSitterUtil.forEach(tree.rootNode, (node) => {
const symbol = getDeclarationSymbolFromNode({ node, uri });
const symbol = getDeclarationSymbolFromNode(node, uri);
if (symbol) {
symbols.push(symbol);
}
Expand All @@ -88,171 +43,82 @@ export function getAllDeclarationsInTree({
}

/**
* Returns declarations available for the given file and location.
* The heuristics used is a simplification compared to bash behaviour,
* but deemed good enough, compared to the complexity of flow tracing.
* Converts node to symbol information.
*
* Used when getting declarations for the current scope.
* @param tree Tree-sitter tree.
* @param uri The document's uri.
* @returns Symbol information from node.
*/
export function getLocalDeclarations({
node,
rootNode,
uri,
}: {
node: Parser.SyntaxNode | null
rootNode: Parser.SyntaxNode
uri: string
}): Declarations {
const declarations: Declarations = {};

// Bottom up traversal to capture all local and scoped declarations
const walk = (node: Parser.SyntaxNode | null) => {
// NOTE: there is also node.walk
if (node) {
for (const childNode of node.children) {
let symbol: LSP.SymbolInformation | null = null;

// local variables
if (childNode.type === 'declaration_command') {
const variableAssignmentNode = childNode.children.filter(
(child) => child.type === 'variable_assignment',
)[0];

if (variableAssignmentNode) {
symbol = nodeToSymbolInformation({
node: variableAssignmentNode,
uri,
});
}
} else if (childNode.type === 'for_statement') {
const variableNode = childNode.child(1);
if (variableNode && variableNode.type === 'variable_name') {
symbol = LSP.SymbolInformation.create(
variableNode.text,
LSP.SymbolKind.Variable,
TreeSitterUtil.range(variableNode),
uri,
);
}
} else {
symbol = getDeclarationSymbolFromNode({ node: childNode, uri });
}

if (symbol) {
if (!declarations[symbol.name]) {
declarations[symbol.name] = [];
}
declarations[symbol.name].push(symbol);
}
}

walk(node.parent);
}
};

walk(node);

// Top down traversal to add missing global variables from within functions
Object.entries(
getAllGlobalVariableDeclarations({
rootNode,
uri,
}),
).map(([name, symbols]) => {
if (!declarations[name]) {
declarations[name] = symbols;
}
});

return declarations;
}

function getAllGlobalVariableDeclarations({
uri,
rootNode,
}: {
uri: string
rootNode: Parser.SyntaxNode
}) {
const declarations: Declarations = {};

TreeSitterUtil.forEach(rootNode, (node) => {
if (
node.type === 'variable_assignment' &&
// exclude local variables
node.parent?.type !== 'declaration_command'
) {
const symbol = nodeToSymbolInformation({ node, uri });
if (symbol) {
if (!declarations[symbol.name]) {
declarations[symbol.name] = [];
}
declarations[symbol.name].push(symbol);
}
}

return;
});

return declarations;
}

function nodeToSymbolInformation({
node,
uri,
}: {
node: Parser.SyntaxNode
uri: string
}): LSP.SymbolInformation | null {
export function nodeToSymbolInformation(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null {
const named = node.firstNamedChild;

if (named === null) {
return null;
}

const name = TreeSitterUtil.getIdentifier(node);
if (name === null || isEmpty(name)) {
return null;
}

const kind = getKind(node);

const containerName =
TreeSitterUtil.findParent(node, (p) => p.type === 'function_definition')
?.firstNamedChild?.text || '';

const kind = TREE_SITTER_TYPE_TO_LSP_KIND[node.type];

return LSP.SymbolInformation.create(
named.text,
name,
kind || LSP.SymbolKind.Variable,
TreeSitterUtil.range(node),
uri,
containerName,
);
}

function getDeclarationSymbolFromNode({
node,
uri,
}: {
node: Parser.SyntaxNode
uri: string
}): LSP.SymbolInformation | null {
/**
* Get declaration from node and convert to symbol information.
*
* @param node Root node of tree.
* @param uri The associated URI for this document.
* @returns LSP symbol information for definition.
*/
function getDeclarationSymbolFromNode(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null {
if (TreeSitterUtil.isDefinition(node)) {
//logger.debug('Found definition:');
//logger.debug(node.toString());
return nodeToSymbolInformation({ node, uri });
} else if (node.type === 'command' && node.text.startsWith(': ')) {
// : does argument expansion and retains the side effects.
// A common usage is to define default values of environment variables, e.g. : "${VARIABLE:="default"}".
const variableNode = node.namedChildren
.find((c) => c.type === 'string')
?.namedChildren.find((c) => c.type === 'expansion')
?.namedChildren.find((c) => c.type === 'variable_name');

if (variableNode) {
return LSP.SymbolInformation.create(
variableNode.text,
LSP.SymbolKind.Variable,
TreeSitterUtil.range(variableNode),
uri,
);
}
return nodeToSymbolInformation(node, uri);
}

return null;
}

/**
* Returns symbol kind from class definition node.
*
* @param node Node containing class_definition
* @returns Symbol kind or `undefined`.
*/
function getKind(node: Parser.SyntaxNode): LSP.SymbolKind | undefined {

const classPrefixes = TreeSitterUtil.getClassPrefixes(node)?.split(/\s+/);
if (classPrefixes === undefined) {
return undefined;
}

switch (classPrefixes[classPrefixes.length - 1]) {
case 'block':
case 'class':
case 'connector':
case 'model':
return LSP.SymbolKind.Class;
case 'function':
case 'operator':
return LSP.SymbolKind.Function;
case 'package':
case 'record':
return LSP.SymbolKind.Package;
case 'type':
return LSP.SymbolKind.TypeParameter;
default:
return undefined;
}
}
Loading

0 comments on commit 64e34d3

Please sign in to comment.