@neo4j-cypher/language-support
Version:
Cypher language support
217 lines (182 loc) • 5.59 kB
text/typescript
import { SemanticTokenTypes } from 'vscode-languageserver-types';
import { Token } from 'antlr4';
import CypherLexer from '../generated-parser/CypherCmdLexer';
import { isCommentOpener } from '../helpers';
import { CypherTokenType, lexerSymbols } from '../lexerSymbols';
export interface TokenPosition {
line: number;
startCharacter: number;
startOffset: number;
}
export function tokenPositionToString(tokenPosition: TokenPosition): string {
return `${tokenPosition.line},${tokenPosition.startCharacter}`;
}
export interface ParsedCypherToken {
position: TokenPosition;
length: number;
tokenType: CypherTokenType;
token: string;
bracketInfo?: BracketInfo;
}
export interface BracketInfo {
bracketType: BracketType;
bracketLevel: number;
}
export enum BracketType {
bracket = 'bracket',
parenthesis = 'parenthesis',
curly = 'curly',
}
export interface ColouredToken {
position: TokenPosition;
length: number;
tokenColour: SemanticTokenTypes;
token: string;
}
export function getTokenPosition(token: Token): TokenPosition {
return {
line: token.line - 1,
startCharacter: token.column,
startOffset: token.start,
};
}
function getBracketType(token: Token): BracketType | undefined {
const bracketType: Record<number, BracketType> = {
[CypherLexer.LPAREN]: BracketType.parenthesis,
[CypherLexer.RPAREN]: BracketType.parenthesis,
[CypherLexer.LBRACKET]: BracketType.bracket,
[CypherLexer.RBRACKET]: BracketType.bracket,
[CypherLexer.LCURLY]: BracketType.curly,
[CypherLexer.RCURLY]: BracketType.curly,
};
return bracketType[token.type];
}
function isClosingBracket(token: Token): boolean {
return [
CypherLexer.RPAREN,
CypherLexer.RBRACKET,
CypherLexer.RCURLY,
].includes(token.type);
}
function isOpeningBracket(token: Token): boolean {
return [
CypherLexer.LPAREN,
CypherLexer.LBRACKET,
CypherLexer.LCURLY,
].includes(token.type);
}
function computeBracketInfo(
token: Token,
bracketsLevel?: Map<BracketType, number>,
): BracketInfo | undefined {
let bracketInfo: BracketInfo | undefined = undefined;
if (bracketsLevel) {
const bracketType = getBracketType(token);
if (bracketType !== undefined) {
let bracketLevel = bracketsLevel.get(bracketType) ?? 0;
if (isOpeningBracket(token)) {
bracketsLevel.set(bracketType, ++bracketLevel);
}
bracketInfo = {
bracketLevel: bracketLevel,
bracketType: bracketType,
};
if (isClosingBracket(token)) {
bracketsLevel.set(bracketType, --bracketLevel);
}
}
}
return bracketInfo;
}
export function toParsedTokens(
tokenPosition: TokenPosition,
tokenType: CypherTokenType,
tokenStr: string,
token: Token,
bracketsLevel?: Map<BracketType, number>,
): ParsedCypherToken[] {
let prevLen = 0;
return tokenStr.split('\n').flatMap((tokenChunk, i) => {
const position =
i == 0
? tokenPosition
: {
line: tokenPosition.line + i,
startCharacter: 0,
startOffset: token.start + prevLen,
};
prevLen += tokenChunk.length + '\n'.length;
// If the token is empty no need to create a token
if (tokenChunk.length === 0) {
return [];
}
const bracketInfo: BracketInfo | undefined = computeBracketInfo(
token,
bracketsLevel,
);
return [
{
position: position,
length: tokenChunk.length,
tokenType: tokenType,
token: tokenChunk,
bracketInfo: bracketInfo,
},
];
});
}
export function getCypherTokenType(
token: Token,
nextToken: Token | undefined,
): {
tokenType: CypherTokenType;
finished: boolean;
} {
const tokenText = token.text;
if (token.type === CypherLexer.ErrorChar) {
if (tokenText === '"' || tokenText === "'") {
return { tokenType: CypherTokenType.stringLiteral, finished: false };
} else if (tokenText.startsWith('`')) {
return { tokenType: CypherTokenType.symbolicName, finished: false };
}
}
if (isCommentOpener(token, nextToken)) {
return { tokenType: CypherTokenType.comment, finished: false };
}
return {
tokenType: lexerSymbols[token.type] ?? CypherTokenType.none,
finished: true,
};
}
export function shouldAssignTokenType(token: Token): boolean {
const nonEOF = token.type !== Token.EOF || token.text !== '<EOF>';
const inMainChannel = token.channel == 0;
const isComment =
token.type === CypherLexer.SINGLE_LINE_COMMENT ||
token.type === CypherLexer.MULTI_LINE_COMMENT;
return nonEOF && (inMainChannel || isComment);
}
export function sortTokens(tokens: ParsedCypherToken[]) {
return tokens.sort((a, b) => {
const lineDiff = a.position.line - b.position.line;
if (lineDiff !== 0) return lineDiff;
return a.position.startCharacter - b.position.startCharacter;
});
}
// Assumes the tokens are already sorted
export function removeOverlappingTokens(tokens: ParsedCypherToken[]) {
const result: ParsedCypherToken[] = [];
let prev: TokenPosition = { line: -1, startCharacter: -1, startOffset: -1 };
let prevEndCharacter = 0;
tokens.forEach((token) => {
const current = token.position;
if (current.line > prev.line || current.startCharacter > prevEndCharacter) {
// Add current token to the list and in further iterations
// remove tokens overlapping with it
result.push(token);
prev = current;
prevEndCharacter = prev.startCharacter + (token.token?.length ?? 0) - 1;
}
});
return result;
}