UNPKG

langium

Version:

A language engineering tool for the Language Server Protocol

304 lines (258 loc) 10.9 kB
/****************************************************************************** * Copyright 2021 TypeFox GmbH * This program and the accompanying materials are made available under the * terms of the MIT License, which is available in the project root. ******************************************************************************/ import type { Position, Range } from 'vscode-languageserver-textdocument'; import type { GeneratorNode } from './generator-node.js'; import type { SourceRegion, TextRegion, TraceRegion } from './generator-tracing.js'; import { CompositeGeneratorNode, IndentNode, NewLineNode } from './generator-node.js'; import { getSourceRegion } from './generator-tracing.js'; type OffsetAndPosition = { offset: number } & Position class Context { defaultIndentation = ' '; pendingIndent = true; readonly currentIndents: IndentNode[] = []; readonly recentNonImmediateIndents: IndentNode[] = []; private traceData: InternalTraceRegion[] = []; private lines: string[][] = [[]]; private length: number = 0; constructor(defaultIndent?: string | number) { if (typeof defaultIndent === 'string') { this.defaultIndentation = defaultIndent; } else if (typeof defaultIndent === 'number') { this.defaultIndentation = ''.padStart(defaultIndent); } } get content(): string { return this.lines.map(e => e.join('')).join(''); } get contentLength(): number { return this.length; } get currentLineNumber(): number { return this.lines.length - 1; } get currentLineContent(): string { return this.lines[this.currentLineNumber].join(''); } get currentPosition(): OffsetAndPosition { return { offset: this.contentLength, line: this.currentLineNumber, character: this.currentLineContent.length }; } append(value: string, isIndent?: boolean) { if (value.length > 0) { const beforePos = isIndent && this.currentPosition; this.lines[this.currentLineNumber].push(value); this.length += value.length; if (beforePos) { this.indentPendingTraceRegions(beforePos); } } } private indentPendingTraceRegions(before: OffsetAndPosition) { for (let i = this.traceData.length - 1; i >= 0; i--) { const tr = this.traceData[i]; if (tr.targetStart && tr.targetStart.offset === before.offset /* tr.targetStart.line == before.line && tr.targetStart.character === before.character*/) tr.targetStart = this.currentPosition; } } increaseIndent(node: IndentNode) { this.currentIndents.push(node); if (!node.indentImmediately) { this.recentNonImmediateIndents.push(node); } } decreaseIndent() { this.currentIndents.pop(); } get relevantIndents() { return this.currentIndents.filter(i => !this.recentNonImmediateIndents.includes(i)); } resetCurrentLine() { this.length -= this.lines[this.currentLineNumber].join('').length; this.lines[this.currentLineNumber] = []; this.pendingIndent = true; this.recentNonImmediateIndents.length = 0; } addNewLine() { this.lines.push([]); this.pendingIndent = true; this.recentNonImmediateIndents.length = 0; } pushTraceRegion(sourceRegion: SourceRegion | undefined): InternalTraceRegion { const region = createTraceRegion( sourceRegion, this.currentPosition, it => this.traceData[this.traceData.length - 1]?.children?.push(it)); this.traceData.push(region); return region; } popTraceRegion(expected: TraceRegion): InternalTraceRegion { const traceRegion = this.traceData.pop()!; // the following assertion can be dropped once the tracing is considered stable this.assertTrue(traceRegion === expected, 'Trace region mismatch!'); return traceRegion; } getParentTraceSourceFileURI() { for (let i = this.traceData.length - 1; i > -1; i--) { const fileUri = this.traceData[i].sourceRegion?.fileURI; if (fileUri) return fileUri; } return undefined; } private assertTrue(condition: boolean, msg: string): asserts condition is true { if (!condition) { throw new Error(msg); } } } interface InternalTraceRegion extends TraceRegion { targetStart?: OffsetAndPosition; complete?: (targetEnd: OffsetAndPosition) => TraceRegion; } function createTraceRegion(sourceRegion: SourceRegion | undefined, targetStart: OffsetAndPosition, accept: (it: TraceRegion) => void): TraceRegion { const result = <InternalTraceRegion>{ sourceRegion, targetRegion: undefined!, children: [], targetStart, complete: (targetEnd: OffsetAndPosition) => { result.targetRegion = <TextRegion>{ offset: result.targetStart!.offset, end: targetEnd.offset, length: targetEnd.offset - result.targetStart!.offset, range: <Range>{ start: { line: result.targetStart!.line, character: result.targetStart!.character }, end: { line: targetEnd.line, character: targetEnd.character }, } }; delete result.targetStart; if (result.children?.length === 0) { delete result.children; } if (result.targetRegion?.length) { accept(result); } delete result.complete; return result; } }; return result; } export function processGeneratorNode(node: GeneratorNode, defaultIndentation?: string | number): { text: string, trace: TraceRegion } { const context = new Context(defaultIndentation); const trace = context.pushTraceRegion(undefined); processNodeInternal(node, context); context.popTraceRegion(trace); trace.complete && trace.complete(context.currentPosition); const singleChild = trace.children && trace.children.length === 1 ? trace.children[0] : undefined; const singleChildTargetRegion = singleChild?.targetRegion; const rootTargetRegion = trace.targetRegion; if (singleChildTargetRegion && singleChild.sourceRegion && singleChildTargetRegion.offset === rootTargetRegion.offset && singleChildTargetRegion.length === rootTargetRegion.length) { // some optimization: // if (the root) `node` is traced (`singleChild.sourceRegion` !== undefined) and spans the entire `context.content` // we skip the wrapping root trace object created above at the beginning of this method return { text: context.content, trace: singleChild }; } else { return { text: context.content, trace }; } } function processNodeInternal(node: GeneratorNode | string, context: Context) { if (typeof(node) === 'string') { processStringNode(node, context); } else if (node instanceof IndentNode) { processIndentNode(node, context); } else if (node instanceof CompositeGeneratorNode) { processCompositeNode(node, context); } else if (node instanceof NewLineNode) { processNewLineNode(node, context); } } function hasContent(node: GeneratorNode | string, ctx: Context): boolean { if (typeof(node) === 'string') { return node.length !== 0; // cs: do not ignore ws only content here, enclosed within other nodes it will matter! } else if (node instanceof CompositeGeneratorNode) { return node.contents.some(e => hasContent(e, ctx)); } else if (node instanceof NewLineNode) { return !(node.ifNotEmpty && ctx.currentLineContent.length === 0); } else { return false; } } function processStringNode(node: string, context: Context) { if (node) { handlePendingIndent(context, false); context.append(node); } } function handlePendingIndent(ctx: Context, endOfLine: boolean) { if (ctx.pendingIndent) { let indent = ''; for (const indentNode of ctx.relevantIndents.filter(e => e.indentEmptyLines || !endOfLine)) { indent += indentNode.indentation ?? ctx.defaultIndentation; } ctx.append(indent, true); ctx.pendingIndent = false; } } function processCompositeNode(node: CompositeGeneratorNode, context: Context) { let traceRegion: InternalTraceRegion | undefined = undefined; const sourceRegion: SourceRegion | undefined = getSourceRegion(node.tracedSource); if (sourceRegion) { traceRegion = context.pushTraceRegion(sourceRegion); } for (const child of node.contents) { processNodeInternal(child, context); } if (traceRegion) { context.popTraceRegion(traceRegion); const parentsFileURI = context.getParentTraceSourceFileURI(); if (parentsFileURI && sourceRegion?.fileURI === parentsFileURI) { // if some parent's sourceRegion refers to the same source file uri (and no other source file was referenced inbetween) // we can drop the file uri in order to reduce repeated strings delete sourceRegion.fileURI; } traceRegion.complete && traceRegion.complete(context.currentPosition); } } function processIndentNode(node: IndentNode, context: Context) { if (hasContent(node, context)) { if (node.indentImmediately && !context.pendingIndent) { context.append(node.indentation ?? context.defaultIndentation, true); } try { context.increaseIndent(node); processCompositeNode(node, context); } finally { context.decreaseIndent(); } } } function processNewLineNode(node: NewLineNode, context: Context) { if (node.ifNotEmpty && !hasNonWhitespace(context.currentLineContent)) { context.resetCurrentLine(); } else { handlePendingIndent(context, true); let count = node.count; while (count-- > 0) { context.append(node.lineDelimiter); context.addNewLine(); } } } function hasNonWhitespace(text: string) { return text.trimStart() !== ''; }