nadesiko3
Version:
Japanese Programming Language
263 lines (231 loc) • 8.2 kB
text/typescript
interface PreprocessItem {
text: string;
sourcePosition: number
}
/** prepareとtokenizeのソースマッピング */
export class SourceMappingOfTokenization {
private readonly sourceCodeLength: number
private readonly preprocessed: PreprocessItem[]
private readonly cumulativeSum: number[]
private lastIndex: number
private lastPreprocessedCodePosition: number
/**
* @param {number} sourceCodeLength
* @param {PreprocessItem[]} preprocessed
*/
constructor (sourceCodeLength: number, preprocessed: PreprocessItem[]) {
/** @private @readonly */
this.sourceCodeLength = sourceCodeLength
/** @private @readonly */
this.preprocessed = preprocessed
let i = 0
/** @private @readonly @type {number[]} */
this.cumulativeSum = []
for (const el of preprocessed) {
this.cumulativeSum.push(i)
i += el.text.length
}
/** @private */
this.lastIndex = 0
/** @private */
this.lastPreprocessedCodePosition = 0
}
/**
* preprocess後の文字列上のoffsetからソースコード上のoffsetへ変換
* @param {number} preprocessedCodePosition
* @returns {number}
*/
map (preprocessedCodePosition: number): number {
const i = this.findIndex(preprocessedCodePosition)
return Math.min(
this.preprocessed[i].sourcePosition + (preprocessedCodePosition - this.cumulativeSum[i]),
i === this.preprocessed.length - 1 ? this.sourceCodeLength : this.preprocessed[i + 1].sourcePosition - 1
)
}
/**
* @param {number} preprocessedCodePosition
* @returns {number}
*/
findIndex (preprocessedCodePosition: number): number {
// 連続アクセスに対する高速化
if (preprocessedCodePosition < this.lastPreprocessedCodePosition) {
this.lastIndex = 0
}
this.lastPreprocessedCodePosition = preprocessedCodePosition
for (let i = this.lastIndex; i < this.preprocessed.length - 1; i++) {
if (preprocessedCodePosition < this.cumulativeSum[i + 1]) {
this.lastIndex = i
return i
}
}
this.lastIndex = this.preprocessed.length - 1
return this.preprocessed.length - 1
}
}
export class SourceMappingOfIndentSyntax {
private lines: { offset: number, len: number }[]
private readonly linesInsertedByIndentationSyntax: number[]
private readonly linesDeletedByIndentationSyntax: { lineNumber: number, len: number }[]
lastLineNumber: number
lastOffset: number
/**
* @param {string} codeAfterProcessingIndentationSyntax
* @param {readonly number[]} linesInsertedByIndentationSyntax
* @param {readonly { lineNumber: number, len: number }[]} linesDeletedByIndentationSyntax
*/
constructor (
codeAfterProcessingIndentationSyntax: string,
linesInsertedByIndentationSyntax: number[],
linesDeletedByIndentationSyntax: { lineNumber: number, len: number }[]
) {
/** @private @type {{ offset: number, len: number }[]} */
this.lines = []
/** @private @readonly */
this.linesInsertedByIndentationSyntax = linesInsertedByIndentationSyntax
/** @private @readonly */
this.linesDeletedByIndentationSyntax = linesDeletedByIndentationSyntax
let offset = 0
for (const line of codeAfterProcessingIndentationSyntax.split('\n')) {
this.lines.push({ offset, len: line.length })
offset += line.length + 1
}
/** @private */
this.lastLineNumber = 0
/** @private */
this.lastOffset = 0
}
/**
* @param {number | null} startOffset
* @param {number | null} endOffset
* @returns {{ startOffset: number | null, endOffset: number | null }}
*/
map (startOffset: number | null, endOffset:number | null): { startOffset: number | null, endOffset: number | null } {
if (startOffset === null) {
return { startOffset, endOffset }
}
// 何行目かを判定
const tokenLine = this.getLineNumber(startOffset)
for (const insertedLine of this.linesInsertedByIndentationSyntax) {
// インデント構文の処理後のソースコードの `insertedLine` 行目にあるトークンのソースマップ情報を削除する。
if (tokenLine === insertedLine) {
startOffset = null
endOffset = null
break
}
// インデント構文の処理後のソースコードの `insertedLine` 行目以降にあるトークンのoffsetから
// `linesInsertedByIndentationSyntax[i]` 行目の文字数(\rを含む) を引く。
if (tokenLine > insertedLine) {
// "\n"の分1足す
startOffset -= this.lines[insertedLine].len + 1
if (endOffset !== null) {
endOffset -= this.lines[insertedLine].len + 1
}
}
}
for (const deletedLine of this.linesDeletedByIndentationSyntax) {
if (tokenLine >= deletedLine.lineNumber) {
// "\n"の分1足す
if (startOffset !== null) {
startOffset += deletedLine.len + 1
}
if (endOffset !== null) {
endOffset += deletedLine.len + 1
}
}
}
return { startOffset, endOffset }
}
/**
* @param {number} offset
* @returns {number}
* @private
*/
getLineNumber (offset: number): number {
// 連続アクセスに対する高速化
if (offset < this.lastOffset) {
this.lastLineNumber = 0
}
this.lastOffset = offset
for (let i = this.lastLineNumber; i < this.lines.length - 1; i++) {
if (offset < this.lines[i + 1].offset) {
this.lastLineNumber = i
return i
}
}
this.lastLineNumber = this.lines.length - 1
return this.lines.length - 1
}
}
/** offsetから (line, column) へ変換する。 */
export class OffsetToLineColumn {
private lineOffsets: number[]
private lastLineNumber: number
private lastOffset: number
/**
* @param {string} code
*/
constructor (code: string) {
/** @private @type {number[]} */
this.lineOffsets = []
// 各行の先頭位置を先に計算しておく
let offset = 0
for (const line of code.split('\n')) {
this.lineOffsets.push(offset)
offset += line.length + 1
}
/** @private */
this.lastLineNumber = 0
/** @private */
this.lastOffset = 0
}
/**
* @param {number} offset
* @param {boolean} oneBasedLineNumber trueのときlineを1から始める
* @returns {{ line: number, column: number }}
*/
map (offset: number, oneBasedLineNumber: boolean):{ line: number, column: number } {
// 連続アクセスに対する高速化
if (offset < this.lastOffset) {
this.lastLineNumber = 0
}
this.lastOffset = offset
for (let i = this.lastLineNumber; i < this.lineOffsets.length - 1; i++) {
if (offset < this.lineOffsets[i + 1]) {
this.lastLineNumber = i
return {
line: i + (oneBasedLineNumber ? 1 : 0),
column: offset - this.lineOffsets[i]
}
}
}
this.lastLineNumber = this.lineOffsets.length - 1
return {
line: this.lineOffsets.length - 1 + (oneBasedLineNumber ? 1 : 0),
column: offset - this.lineOffsets[this.lineOffsets.length - 1]
}
}
}
/**
* preCodeの分、ソースマップのoffset、行数、列数を減らす。
* @type {<T extends {line?: number, column?: number, startOffset: number | null, endOffset: number | null }>(sourceMap: T, preCode: string) => T}
*/
export function subtractSourceMapByPreCodeLength (sourceMap: any, preCode: string) {
// offsetは単純に引くだけでよい
if (typeof sourceMap.startOffset === 'number') {
sourceMap.startOffset -= preCode.length
}
if (typeof sourceMap.endOffset === 'number') {
sourceMap.endOffset -= preCode.length
}
// たとえば preCode = 'abc\ndef\nghi' のとき、line -= 2 して、先頭行なら column -= 3 もする。
if (preCode !== '') {
const lines = preCode.split('\n')
if (typeof sourceMap.line === 'number') {
sourceMap.line -= lines.length - 1
}
if (sourceMap.line === 0 && typeof sourceMap.column === 'number') {
sourceMap.column -= lines[lines.length - 1].length
}
}
return sourceMap
}