-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathtwoslash.ts
160 lines (136 loc) · 4.65 KB
/
twoslash.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import { command, Module, listener } from 'cookiecord';
import { Message, TextChannel } from 'discord.js';
import { twoslasher } from '../util/twoslasher';
import { findCodeFromChannel } from '../util/findCodeblockFromChannel';
import { sendWithMessageOwnership } from '../util/send';
const CODEBLOCK = '```';
// Custom escape function instead of using discord.js Util.escapeCodeBlock because this
// produces better results with template literal types. Discord's markdown handling is pretty
// bad. It doesn't properly handle escaping back ticks, so we instead insert zero width spaces
// so that users cannot escape our code block.
function escapeCode(code: string) {
return code.replace(/`(?=`)/g, '`\u200B');
}
// Remove `@noErrorTruncation` from the source; this can cause lag/crashes for large errors
function redactNoErrorTruncation(code: string) {
return code.replace(/@noErrorTruncation/g, '');
}
export class TwoslashModule extends Module {
@command({
single: true,
description:
'Twoslash: Run twoslash on the latest codeblock, optionally returning the quickinfos of specified symbols',
aliases: ['ts'],
})
async twoslash(msg: Message, content: string) {
const code = await findCodeFromChannel(msg.channel as TextChannel);
if (!code)
return await sendWithMessageOwnership(
msg,
`:warning: could not find any TypeScript codeblocks in the past 10 messages`,
);
if (!content) return await this.twoslashBlock(msg, code);
if (!/^\s*([_$a-zA-Z][_$0-9a-zA-Z]*\b\s*)+/.test(content)) {
return sendWithMessageOwnership(
msg,
'You need to give me a valid symbol name to look for!',
);
}
const symbols = [...new Set(content.trim().split(/\s+/g))];
const ret = await twoslasher(redactNoErrorTruncation(code), 'ts', {
defaultOptions: { noErrorValidation: true },
});
const blocks = [];
for (const symbol of symbols) {
const block = [];
const matches: Record<string, Set<string>> = {};
for (const quickInfo of ret.staticQuickInfos) {
if (quickInfo.targetString !== symbol) continue;
(matches[quickInfo.text] =
matches[quickInfo.text] ?? new Set()).add(
`${quickInfo.line + 1}:${quickInfo.character + 1}`,
);
}
if (!Object.entries(matches).length)
block.push(`/* No symbol named \`${symbol}\` found */`);
for (const [info, locSet] of Object.entries(matches)) {
block.push(`${info} /* ${[...locSet].join(', ')} */`);
}
blocks.push(block);
}
await sendWithMessageOwnership(
msg,
blocks
.map(
block =>
`${CODEBLOCK}typescript\n${escapeCode(
block.join('\n'),
)}${CODEBLOCK}`,
)
.join(''),
);
}
@listener({ event: 'message' })
async onTwoslashCodeBlock(msg: Message) {
const match = msg.content.match(/^```ts twoslash\n([\s\S]+)```$/im);
if (!msg.author.bot && match) {
await this.twoslashBlock(msg, match[1]);
await msg.delete();
}
}
private async twoslashBlock(msg: Message, code: string) {
const ret = await twoslasher(redactNoErrorTruncation(code), 'ts', {
defaultOptions: {
noErrorValidation: true,
noStaticSemanticInfo: false,
},
});
const resultLines: string[] = [];
const twoslashLines = ret.code.split('\n');
twoslashLines.forEach((line, index) => {
resultLines.push(line);
const lineErrors = ret.errors.filter(e => e.line === index);
const lineQueries = ret.queries.filter(e => e.line === index + 1);
if (lineErrors.length + lineQueries.length === 0) return;
if (lineErrors.length) {
// Make sure all lines of errors start with a comment
const errors = lineErrors.map(
e => '// ' + e.renderedMessage.split('\n').join('\n// '),
);
// ^^^^^ ^^^^^
// hats to indicate what token is causing the issue
let linkWithHats = '';
lineErrors.forEach(e => {
if (!e.character) return;
const spaceBefore = e.character - linkWithHats.length;
linkWithHats += ' '.repeat(spaceBefore);
linkWithHats += '^'.repeat(e.length || 0);
});
if (linkWithHats.length > 0) {
resultLines.push('//' + linkWithHats.substr(2));
}
resultLines.push(...errors);
}
// Inline queries for showing the LSP lookup for a token
if (lineQueries.length) {
let queryComment = '//';
lineQueries.forEach(q => {
const spaceBefore = q.offset - queryComment.length;
queryComment += ' '.repeat(spaceBefore);
queryComment += '^? - ';
queryComment +=
q.text?.replace(
/\n/g,
'\n//' + ' '.repeat(spaceBefore),
) || '';
});
resultLines.push(queryComment);
}
});
const output = resultLines.join('\n');
return sendWithMessageOwnership(
msg,
`${CODEBLOCK}ts\n${escapeCode(output)}${CODEBLOCK}\n`,
);
}
}