muggle-string
我写麻瓜串的 vuejs /语言工具,将Vue的单文件组件转换为语言服务器的TypeScript代码。它一次从几个地方重写源文本,并将结果映射回源,既作为语言服务器的跨度,也作为v3源映射。它没有依赖关系。
1.0取代了0.4的段数组帮助器(
replace,replaceSourceRange,toString,……)。它们已经消失了,没有被弃用。
背景
在vuejs/language-tools中,props、emit、models、slots和refs的插件都是一样的 <script setup> 块。生成器移动其中的部分,比如默认导出的表达式,并在模板代码周围将块分成几部分呈现。输出的每个部分都保留了它所来自的代码的编辑器特性,比如悬停、完成和重命名。麻瓜串是它背后的模型,没有Vue在里面。我想从中得到三个东西:
- 编辑保持正确。 插件不知道彼此的存在。删除加起来,所以没有编辑撤消另一个。在一个位置的代码是按其作者的等级排序的,所以插件的顺序决定它,而不是调用的顺序。这些测试
tests/用随机编辑检查两者。 - 地图在工具读取的地方是准确的。 相同的部分给出了语言服务器的范围和用于调试器的v3源代码映射。在源映射中,生成的代码映射为空,因此调试器永远不会将其显示为您的代码,即使当它像V8那样跨行查看时也是如此。在JavaScript、TypeScript、HTML和CSS中,保持代码映射恰好在令牌开始处,这是断点、堆栈跟踪和覆盖读取的地方。测试检查映射的每个生成的字符。
- 性能。 语言服务器在每次更改文件后都会重新生成代码,因此模型会跳过它不需要的工作。插入或替换操作只被追加到其编写器的列表中。没有文本搜索,分割或复制时,您编辑。渲染将编辑排序一次,或者当它们按顺序到达时根本不排序,之后的渲染将重用它,直到下一次编辑。渲染传递保留的文本作为源的范围,从不复制它。是否要把这些碎片拼凑成一根弦取决于你。 在Apple M2上使用Node 24,对于1 MB的文本,从4个写入器随机插入和替换100,000个,每次编辑在新过程中花费约115 ns,在加热过程中花费约37 ns,无论之前有多少,将文本渲染为字符串大约需要40 ms冷和29 ms热。
采用
- vuejs /语言工具:生成Vue单文件组件的TypeScript代码。它的版本使用0.4;向1.0的迁移正在进行中,尚未发布。
- muggle-pug:将Pug编译为HTML格式
Piece范围。
安装
npm install muggle-string. 它只作为ES模块发布。CommonJS代码可以 require 在Node 20.19及更高版本,或22.12及更高版本。 renderWithSourceMap 还需要全球 TextDecoder WHATWG编码标准,如Node和Bun所定义的; renderWithDecodedSourceMap 没有。
用法
编辑和渲染
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'log(name);\nconst name = "Ada";\n' };
const rewrite = createSourceRewrite(source);
for (const { index: start } of source.content.matchAll(/\bname\b/g)) {
rewrite.replace(start, start + 'name'.length, 'user');
}
rewrite.insertBefore(0, rewrite.cut(source.content.indexOf('const'), source.content.length));
let text = '';
rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece));
console.log(text);
// const user = "Ada";
// log(user);
源是具有的任何对象 content. 内容更改后的渲染会抛出。
作家和职级
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'f(x)' };
const rewrite = createSourceRewrite(source);
const inner = rewrite.writer(0);
const outer = rewrite.writer(1);
inner.insertBefore(2, '(');
outer.insertBefore(2, '[');
rewrite.insertBefore(2, '{');
inner.insertAfter(3, ')');
outer.insertAfter(3, ']');
rewrite.insertAfter(3, '}');
inner.insertAfter(3, '!');
let text = '';
rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece));
console.log(text);
// f({[(x)!]})
在一个位置,无论调用顺序如何,高阶代码都排在低阶代码之外。重写本身是最外层的写入器。一个人在一个位置编写代码,保持呼叫秩序。
带着伤口移动
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'export default { a: 1 };\n' };
const rewrite = createSourceRewrite(source);
const object = rewrite.cut(15, 23, { placeholder: '__options' });
rewrite.replace(20, 21, '2');
console.log(rewrite.unrendered().length);
// 1
rewrite.insertBefore(0, ['const __options = ', object, ';\n']);
let text = '';
rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece));
console.log(text);
// const __options = { a: 2 };
// export default __options;
console.log(rewrite.unrendered().length);
// 0
切片渲染范围,其中包含您放置它的每个编辑。占位符呈现范围所在的位置。
切片的保存文本携带其切片的数据,也与其他数据一起在视图中:
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'f(a, b)' };
const rewrite = createSourceRewrite<typeof source, { from: string }>(source);
rewrite.insertBefore(2, rewrite.cut(5, 6, { from: 'cut' }));
const parts: string[] = [];
rewrite.render(0, 7, piece => {
parts.push(typeof piece === 'string' ? piece : `${rewritePieceText(piece)}@${piece.from}`);
}, { from: 'view' });
console.log(parts.join(' '));
// f(@view b@cut a, @view )@view
若要在其所在位置包装范围,请在其周围插入而不是切割它。
的观点
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'let a = 1;\nlet b = 2;\n' };
const rewrite = createSourceRewrite<typeof source, { view: string }>(source);
rewrite.insertAfter(0, '// start\n');
rewrite.replace(4, 5, 'x');
function view(start: number, end: number, name: string) {
const kept: string[] = [];
let text = '';
rewrite.render(start, end, piece => {
text += rewritePieceText(piece);
if (typeof piece !== 'string' && piece.text === undefined) {
kept.push(piece.view);
}
}, { view: name });
console.log(JSON.stringify(text), kept.join());
}
view(0, 10, 'first');
// "// start\nlet x = 1;" first,first
view(4, 22, 'second');
// "x = 1;\nlet b = 2;\n" second
视图没有文本,因此视图可以重叠并再次呈现。保存的文本将数据传递给 render. 在所有文本之前插入的代码(insertAfter(0, …))以0开始的视图呈现,并在所有文本之后使用以末尾结束的视图进行编码。
在视图的边缘插入
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'a+b' };
const rewrite = createSourceRewrite(source);
rewrite.insertAfter(1, '>');
rewrite.insertBefore(1, '<');
for (const [start, end] of [[0, 1], [1, 3], [1, 1]] as const) {
let text = '';
rewrite.render(start, end, piece => text += rewritePieceText(piece));
console.log(start, end, JSON.stringify(text));
}
// 0 1 "a>"
// 1 3 "<+b"
// 1 1 ""
const empty = { content: '' };
const emptyRewrite = createSourceRewrite(empty);
emptyRewrite.insertBefore(0, 'B');
emptyRewrite.insertAfter(0, 'A');
let text = '';
emptyRewrite.render(0, 0, piece => text += rewritePieceText(piece));
console.log(JSON.stringify(text));
// "AB"
插入语句附在char的一侧: insertBefore(offset, …) 对炭 offset, insertAfter(offset, …) 到它前面的字符。视图呈现它所呈现的字符的插入,因此视图 a..b 呈现 insertBefore(a, …) 和 insertAfter(b, …),但不是 insertAfter(a, …) 或 insertBefore(b, …). 文本内的空视图,比如 1..1,不呈现插入。位于0或末尾的空视图在所有文本之前或之后呈现代码,因此在空源代码中 render(0, 0) 呈现。按文本选择代码所属的那一边:表达式末尾的代码是 insertAfter(end, …),所以当表达式变成视图时,它仍然会呈现。
部分保存文本的数据
import { createSourceRewrite, rewritePieceText, type Piece, type RewriteOrigin } from 'muggle-string';
const source = { content: 'a + b' };
const rewrite = createSourceRewrite<typeof source, { complete: boolean }>(source);
rewrite.replace(2, 3, '&&');
const parts: string[] = [];
const sink = (piece: Piece<RewriteOrigin<typeof source, { complete: boolean }>>) => {
parts.push(typeof piece === 'string' ? piece : `${rewritePieceText(piece)}:${piece.complete}`);
};
rewrite.render(0, 4, sink, { complete: true });
rewrite.render(4, 5, sink, { complete: false });
console.log(parts.join('|'));
// a :true|&&| :true|b:false
保留文本从视图中获取数据,因此呈现连续视图以提供不同范围的部分数据。
嵌套
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'xy' };
function wrap(order: 'pre-order' | 'enter and leave' | 'pre-order with ranks') {
const rewrite = createSourceRewrite(source);
const outer = order === 'pre-order with ranks' ? rewrite.writer(1) : rewrite;
const inner = order === 'pre-order with ranks' ? rewrite.writer(0) : rewrite;
if (order === 'enter and leave') {
rewrite.insertBefore(0, '(');
rewrite.insertBefore(1, '[');
rewrite.insertAfter(2, ']');
rewrite.insertAfter(2, ')');
}
else {
outer.insertBefore(0, '(');
outer.insertAfter(2, ')');
inner.insertBefore(1, '[');
inner.insertAfter(2, ']');
}
let text = '';
rewrite.render(0, 2, piece => text += rewritePieceText(piece));
console.log(text, order);
}
wrap('pre-order');
// (x[y)] pre-order
wrap('enter and leave');
// (x[y]) enter and leave
wrap('pre-order with ranks');
// (x[y]) pre-order with ranks
要包装嵌套节点,请在输入节点时编写打开代码,在离开节点时编写关闭代码:节点的关闭代码在一个位置结束,然后按调用顺序首先进入内部。如果你把“open”和“close”写在一起,在预定顺序中,“closed”会先写在外面;每个深度都有自己的作者,等级越高越好。
任意位置
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'a +' };
const rewrite = createSourceRewrite(source);
const at = source.content.length;
rewrite.insertBefore(at, { source, start: at, end: at, text: ' missing' });
rewrite.render(0, at, piece => {
if (typeof piece !== 'string') {
console.log(piece.start, piece.end, JSON.stringify(rewritePieceText(piece)));
}
});
// 0 3 "a +"
// 3 3 " missing"
要将代码映射到一个位置而不是一个范围,例如解析错误,请插入一个没有宽度的范围: insertBefore(at, { source, start: at, end: at, text })。一个 replace 的空范围,例如没有char的引用,不删除任何内容,并将其代码放在char的位置 at,因此替换范围的代码不需要检查是否为空。
与源代码不同的代码
import { createSourceRewrite, rewritePieceText, type Piece } from 'muggle-string';
const block = { content: '{{ a && b }}' };
type BlockPiece = Exclude<Piece<{ source: typeof block }>, string>;
const decoded: BlockPiece[] = [
{ source: block, start: 3, end: 5 },
{ source: block, start: 5, end: 15, text: '&&' },
{ source: block, start: 15, end: 17 },
];
function toBlock(start: number, end: number, sink: (piece: BlockPiece) => void) {
let at = 0;
for (const piece of decoded) {
const length = rewritePieceText(piece).length;
const from = Math.max(start, at);
const to = Math.min(end, at + length);
if (from < to) {
sink(piece.text === undefined
? { source: block, start: piece.start + from - at, end: piece.start + to - at }
: piece);
}
at += length;
}
}
const code = { content: decoded.map(rewritePieceText).join('') };
const rewrite = createSourceRewrite(code);
rewrite.insertBefore(0, '__ctx.');
rewrite.insertBefore(5, '__ctx.');
let text = '';
const ranges: string[] = [];
rewrite.render(0, code.content.length, piece => {
if (typeof piece === 'string') {
text += piece;
return;
}
toBlock(piece.start, piece.end, blockPiece => {
text += rewritePieceText(blockPiece);
ranges.push(`${blockPiece.start}..${blockPiece.end}`);
});
});
console.log(text);
// __ctx.a && __ctx.b
console.log(ranges.join(' '));
// 3..5 5..15 15..16 16..17
如果代码是块的文本,则重写块并将代码的范围呈现为视图。在不同的地方,例如在解码实体之后,将代码重写为自己的源代码,并让生成代码的层将其作为块偏移量的片段提供,通过块偏移量保留范围映射回来。
几个来源
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const script = { lang: 'ts', content: 'f();\n' };
const template = { tag: 'template', content: '{{ a }}' };
const rewrite = createSourceRewrite<typeof script | typeof template>(script);
rewrite.insertBefore(2, { source: template, start: 3, end: 4 });
rewrite.render(0, script.content.length, piece => {
if (typeof piece !== 'string') {
console.log(JSON.stringify(rewritePieceText(piece)), 'lang' in piece.source ? 'script' : 'template');
}
});
// "f(" script
// "a" template
// ");\n" script
S 的 createSourceRewrite<S, Data> 是代码中出现的每个源,而不仅仅是它所编辑的源。传递它们的联合或公共类型。
错误
import { createSourceRewrite } from 'muggle-string';
function attempt(edit: () => void) {
try {
edit();
}
catch (error) {
console.log((error as Error).message);
}
}
const source = { content: 'a = 1;\nb = 2;\n' };
const rewrite = createSourceRewrite(source);
attempt(() => rewrite.insertBefore(20, 'x'));
// The offset 20 is out of 0..14.
attempt(() => rewrite.writer(-1));
// The rank -1 is not a non-negative integer below the rank of the rewrite.
const first = rewrite.cut(0, 6);
attempt(() => rewrite.cut(4, 9));
// The cut 4..9 crosses the cut 0..6.
attempt(() => rewrite.render(0, 14, () => rewrite.insertBefore(0, 'x')));
// The rewrite is rendering.
source.content = 'a = 2;\nb = 2;\n';
attempt(() => rewrite.render(0, 14, () => {}));
// The content of the source changed after the rewrite was made.
import { createSourceRewrite } from 'muggle-string';
const source = { content: 'a = 1;\nb = 2;\n' };
const rewrite = createSourceRewrite(source);
const slice = rewrite.cut(2, 6);
rewrite.insertBefore(2, slice);
try {
rewrite.render(0, 14, () => {});
}
catch (error) {
console.log((error as Error).message);
}
// The cut 2..6 is placed inside itself, so it never renders.
insertBefore(2, …) 连接到切片的第一个字符,因此放置在其起始位置的切片在其内部。
抛出的编辑不会改变任何东西。渲染抛出的错误来自于停留的编辑,所以以后的渲染也会抛出错误。
源地图
import { createSourceRewrite, renderWithSourceMap } from 'muggle-string';
const source = { name: 'main.js', content: 'log(name);\nconst name = "Ada";\n' };
const rewrite = createSourceRewrite<typeof source, { name?: string }>(source);
for (const { index: start } of source.content.matchAll(/\bname\b/g)) {
const end = start + 'name'.length;
rewrite.replace(start, end, { source, start, end, text: 'user', name: 'name' });
}
rewrite.insertBefore(0, rewrite.cut(source.content.indexOf('const'), source.content.length));
const { text, map } = renderWithSourceMap(
sink => rewrite.render(0, source.content.length, sink),
(from: typeof source) => from.name,
);
console.log(text);
// const user = "Ada";
// log(user);
console.log(JSON.stringify(map));
// {"version":3,"sources":["main.js"],"sourcesContent":["log(name);\nconst name = \"Ada\";\n"],"names":["name"],"mappings":"AACA,KAAK,CAACA,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AADlB,GAAG,CAACA,IAAI,CAAC;"}
renderWithSourceMap(render, sourceName) 将接收器传递给 render, sourceName 中每个源的条目 sources. 全球在哪里 TextDecoder 则第一个调用在呈现之前抛出。
renderWithDecodedSourceMap(render, sourceName) 给出相同的文本和地图 mappings 解码,用于在片段上工作的代码,而不是写出映射。生成文本的每一行都有一个片段数组, [column, source, line, column, name] 在以零为基础的数字中,没有源字段,其中段没有源并且没有 name 那里没有。它不需要 TextDecoder。
import { createSourceRewrite, renderWithDecodedSourceMap } from 'muggle-string';
const source = { name: 'main.js', content: 'let a = 1;\n' };
const rewrite = createSourceRewrite<typeof source, { name?: string }>(source);
rewrite.replace(4, 5, { source, start: 4, end: 5, text: 'b', name: 'a' });
const { map } = renderWithDecodedSourceMap(
sink => rewrite.render(0, source.content.length, sink),
(from: typeof source) => from.name,
);
console.log(JSON.stringify(map.mappings));
// [[[0,0,0,0],[3,0,0,3],[4,0,0,4,0],[5,0,0,5],[6,0,0,6],[7,0,0,7],[8,0,0,8],[9,0,0,9]],[]]
encodeMappings(mappings) 将解码后的映射写入v3 mappings 字符串, renderWithSourceMap 写入它们,用于在写入映射之前更改段的代码。如果字段不是0到2 ** 30 - 1之间的整数,则会抛出抛出 TextDecoder 没有定义。
import { encodeMappings } from 'muggle-string';
console.log(encodeMappings([[[0, 0, 0, 0], [4]], [], [[2, 0, 1, 3, 0]]]));
// AAAA,I;;EACGA
如果范围没有源,或者其源内容中的偏移量不是整数,则在到达接收器时抛出:
import { renderWithSourceMap } from 'muggle-string';
const source = { name: 'main.js', content: 'a = 1;\n' };
try {
renderWithSourceMap(sink => sink({ source, start: 0, end: 9, text: 'b' }), (from: typeof source) => from.name);
}
catch (error) {
console.log((error as Error).message);
}
// The range 0..9 of main.js is out of 0..7.
来自可变字符串编辑器
偏移总是指向原始文本,在其他编辑之后也是如此。
| 来 | 写 |
|---|---|
| 覆盖范围 | replace(start, end, text),或 replace(start, end, { source, start, end, text }) 把它映射到值域 |
| 移除范围 | replace(start, end, '') |
| 在文本前加或追加 | insertAfter(0, code) 或 insertBefore(source.content.length, code) |
| 在偏移位置插入 | insertBefore(offset, code) 或 insertAfter(offset, code) |
| 移动范围 | insertBefore(to, rewrite.cut(start, end)) |
| 读取结果 | rewrite.render(0, source.content.length, piece => text += rewritePieceText(piece)) |
| 获取源映射 | renderWithSourceMap上面, |
| 获取源地图的片段 | renderWithDecodedSourceMap上面, |
这些技术节省了什么?
表中的每种技术都添加了代码。 npm run bench 构建源代码,并为每种技术构建仅取出该技术的变体(bench/variants.mjs),检查它们是否都通过散列给出相同的输出,并在固定输入的新进程中计算每个输入的时间:大约1 MB的代码样文本,并以4个编写者的不同偏移量进行编辑。 bench/table.mjs 制作下面的表格。每个数字都是一个回合,所以几个百分点的差异接近噪音。节点24.15,Apple M2,单位ms;冷是第一次运行一个过程,暖中值为20次运行后20次以上。
| 技术 | 工作负载 | 温暖,有→没有 | 冷,有→没有 |
|---|---|---|---|
| 编辑和删除添加,排序一次渲染 | 10万次随机编辑 | 36.6→183 | 54.1→193 |
| 当编辑按顺序到达时,没有排序 | 按源顺序进行100,000次编辑 | 14.6→16.9 | 32.7→37.2 |
| 作家们一次合并 | 按源顺序进行100,000次编辑 | 14.6→17.6 | 32.7→43.1 |
| 视图通过二分搜索找到它开始的位置 | 10万次编辑后,点击量达到5000次 | 15.0→318 | 33.2→262 |
| 映射写成字节,由块解码 | 按源顺序编辑10万次后的源地图 | 34.7→97.5 | 55.3→127 |
| 不带正则表达式的ASCII字符 | 相同的源图 | 34.7→51.4 | 55.3→72.8 |
| 在所有其他同等水平的切割之后,又进行了切割 | 货源订单10000次削减 | 1.45→1.58 | 15.2→15.6 |
| 保留文本作为范围传递,不复制 | 按长度计算,有10万次编辑 | 21.0→22.1 | 38.9→42.3 |
键和移除 Int32Array | 10万次随机编辑 | 36.6→39.8 | 54.1→57.9 |
在不使用技术的地方,它没有什么区别:随机编辑的顺序快捷键,文本渲染的地图技术,整体渲染的视图搜索,没有剪切的剪切快捷键。最后三行节省9%或更少。
基线是0.4段数组,它的每次编辑扫描和分割段,以及一个排序数组,将每个编辑拼接到适当的位置:
| 文本渲染 | muggle-string | 排序的数组,拼接 | 分段数组 |
|---|---|---|---|
| 一万次随机编辑,暖 | 3.11 | 4.90 | 155 |
| 十万次随机编辑,暖 | 36.6 | 408 | 21,739(冷,一次运行) |
| 10万编辑源顺序,温暖 | 14.6 | 18.1 | 35232(冷,一次运行) |
参考
编辑
insertBefore(offset, code)将代码附加到字符atoffset,insertAfter(offset, code)到它前面的字符。代码跟着该字符移动。replace(start, end, code)去除start..end并将code在它的位置上。空范围不删除任何内容并放置code在char的位置start,在插页内,或在文本末尾;要将代码映射到某个位置,请写入{ source, start: at, end: at, text }。- 在0..中不是整数的编辑、剪切和视图的偏移量。内容抛出的长度。
rewrite.writer(rank)给了一个作家自己的等级。在一个位置上,高阶代码位于低阶代码之外。重写本身是最外层的写入器。一个人在一个位置编写代码,保持呼叫秩序。rewrite.cut(start, end, options)取出一个范围并返回一个切片。切片渲染范围,其中包含您放置它的每个编辑。options.placeholder渲染范围所在的位置。- 切割可以按任何顺序进行。一个人在另一个人的体内移动。一个切口穿过另一个切口,命名两个范围。
- 切片的保留文本携带其切片的数据,无论切片呈现在何处。
- 切片在它被放置和渲染的任何地方和频率呈现,就像视图一样。在自己的范围内放置的切片,也由
insertBefore在开始时,在渲染时抛出。rewrite.unrendered()列出尚未呈现的切片。 - 渲染渲染之前所做的编辑,所以渲染后的编辑将在下一个渲染中显示。渲染运行时的编辑(例如从其接收器)会引发。源内容更改后的呈现将抛出。
呈现
rewrite.render(start, end, sink, data) 传递的碎片 start..end 来 sink,按顺序。您可以呈现多个范围,它们可能会重叠。一个范围会将代码附加到它的字符上: insertBefore(start, …) 和 insertAfter(end, …),而不是 insertAfter(start, …) 或 insertBefore(end, …). 文本内的空区域不呈现任何代码。在所有文本之前插入的代码(insertAfter(0, …))呈现的范围从0开始,所有文本后面的代码的范围以末尾结束,所以 render(0, 0) 的空源同时呈现。
块
type Piece<Origin = unknown> = string | (Origin & { start: number; end: number; text?: string });
- 字符串是新的文本。
- 没有的范围
text将源文本保存在start..end。 - 的范围
text生成的文本表示start..end。 start和end是整数,0≤start≤end≤的长度content来源的。renderWithSourceMap和renderWithDecodedSourceMap当范围达到它们的sink时,抛出一个打破这个或没有源的范围。rewritePieceText扔在这样的范围没有text,因为它只读取保存的文本的源。当你调用它们时,重写检查它自己的编辑、剪切和视图的偏移量,并将你写的范围作为代码传递给它们。
重写的范围也携带它们 source 和田野 Data. 保留的文本得到 data 传递给 render 或 cut. 你写的范围和你写的一样,比如 name 在源映射示例中。 rewritePieceText(piece) 给出任意片段的文本。单一来源的生产者,如 muggle-pug,能发出平淡 Piece 不带的范围 source。
收集碎片:
import { createSourceRewrite, rewritePieceText, type Piece, type RewriteOrigin } from 'muggle-string';
const source = { content: 'log(name);\n' };
const rewrite = createSourceRewrite<typeof source, { data: string }>(source);
rewrite.replace(4, 8, { source, start: 4, end: 8, text: 'user', data: 'rename' });
const pieces: Piece<RewriteOrigin<typeof source, { data: string }>>[] = [];
rewrite.render(0, source.content.length, piece => pieces.push(piece), { data: 'file' });
for (const piece of pieces) {
console.log(JSON.stringify(rewritePieceText(piece)), typeof piece === 'string' ? 'new' : piece.data);
}
// "log(" file
// "user" rename
// ");\n" file
源地图
- 在每行、单词、空格和其他字符的开头保留文本映射。单词是Unicode字母、标记和数字的集合。识字课等
_,$和-自己绘制地图,因为语言在它们是否属于一个名称上存在分歧。单词内的位置映射到单词的开头。 - 这个
text的值映射到范围的开始。 - 新的文本映射为空。
renderWithSourceMap需要全球TextDecoderWHATWG编码标准,如Node和Bun所定义的。如果没有定义,则第一次调用在呈现之前抛出。renderWithDecodedSourceMap给出与其相同的地图mappings已解码:作为v3映射逐行编码,它们是mappings的renderWithSourceMap,包括空的最后一行。它不需要TextDecoder。- 这个
name的范围text进入names. 仅当text重命名范围内的标识符。保留文本映射到自己,所以name没有的范围text,例如name田野data一种观点,无处可去。文本上以另一个标识符开头的名称,例如__props.x为x,显示调试器__props作为x. 麻瓜麻绳本身没有名字。
跨度的映射
语言工具按范围映射生成的代码。逐字跨距将每个偏移量映射到原始范围中的相同偏移量。Atom span将其末端映射到原始范围的末端,并将其中的每个偏移量映射到原始起点。一个保持的范围是一个逐字跨度,一个与 text 是一个Atom跨度:
import { createSourceRewrite, rewritePieceText } from 'muggle-string';
const source = { content: 'log(name);\nconst name = "Ada";\n' };
const rewrite = createSourceRewrite(source);
for (const { index: start } of source.content.matchAll(/\bname\b/g)) {
const end = start + 'name'.length;
rewrite.replace(start, end, { source, start, end, text: 'user' });
}
rewrite.insertBefore(0, rewrite.cut(source.content.indexOf('const'), source.content.length));
type Span = { virtualStart: number; virtualEnd: number; originalStart: number; originalEnd: number; kind: 'Verbatim' | 'Atom' };
const spans: Span[] = [];
let virtualStart = 0;
rewrite.render(0, source.content.length, piece => {
const { length } = rewritePieceText(piece);
if (typeof piece !== 'string') {
const kind = piece.text === undefined ? 'Verbatim' : 'Atom';
spans.push({ virtualStart, virtualEnd: virtualStart + length, originalStart: piece.start, originalEnd: piece.end, kind });
}
virtualStart += length;
});
for (const span of spans) {
console.log(JSON.stringify(span));
}
// {"virtualStart":0,"virtualEnd":6,"originalStart":11,"originalEnd":17,"kind":"Verbatim"}
// {"virtualStart":6,"virtualEnd":10,"originalStart":17,"originalEnd":21,"kind":"Atom"}
// {"virtualStart":10,"virtualEnd":20,"originalStart":21,"originalEnd":31,"kind":"Verbatim"}
// {"virtualStart":20,"virtualEnd":24,"originalStart":0,"originalEnd":4,"kind":"Verbatim"}
// {"virtualStart":24,"virtualEnd":28,"originalStart":4,"originalEnd":8,"kind":"Atom"}
// {"virtualStart":28,"virtualEnd":31,"originalStart":8,"originalEnd":11,"kind":"Verbatim"}
一个 text 只要它的范围(其偏移量对齐,例如将其重命名为相同长度的名称)可以是一个veratim span。Vuejs /language-tools做到了这一点,所以名称内的编辑映射回来。