-
Notifications
You must be signed in to change notification settings - Fork 0
/
ts.ts
220 lines (205 loc) · 7.19 KB
/
ts.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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import * as vscode from "vscode";
import { Lexer } from "retsac";
import { config } from "../config";
import { evalJsonString } from "../utils";
const lexer = new Lexer.Builder()
.useState({
// use braceDepthStack to calculate the depth of nested curly braces.
// when a new template string starts, push 0 to the front of the stack.
braceDepthStack: [0],
})
.ignore(
// first, ignore comments & regex literals
Lexer.comment("//"),
Lexer.comment("/*", "*/"),
Lexer.regexLiteral(),
// then, ignore all chars except string-beginning,
// slash (the beginning of comment & regex)
// and curly braces (to calculate nested depth)
// in one token (to optimize performance)
/[^"'`\/{}]+/,
// then, ignore non-comment-or-regex slash
/\//
// now the rest must starts with a string
// or curly braces
)
// TODO: use Lexer.anonymous, https://github.com/DiscreteTom/retsac/issues/27
// eslint-disable-next-line @typescript-eslint/naming-convention
.define({ "": Lexer.exact("{") }, (a) =>
a.then(({ input }) => input.state.braceDepthStack[0]++)
)
// eslint-disable-next-line @typescript-eslint/naming-convention
.define({ "": Lexer.exact("}") }, (a) =>
a
// reject if no '{' before '}'
.reject(({ input }) => input.state.braceDepthStack[0] === 0)
.then(({ input }) => input.state.braceDepthStack[0]--)
)
// simple strings
.define({ string: [Lexer.stringLiteral(`"`), Lexer.stringLiteral(`'`)] })
// template strings
.define(
// TODO: https://github.com/DiscreteTom/retsac/issues/28
{ string: /`(?:\\.|[^\\`$])*(?:\$\{|`|$)/ },
// reject if ends with '${'
(a) => a.reject(({ output }) => output.content.endsWith("${"))
)
.define({ tempStrLeft: /`(?:\\.|[^\\`$])*(?:\$\{|`|$)/ }, (a) =>
a
// reject if not ends with '${'
.reject(({ output }) => !output.content.endsWith("${"))
.then(({ input }) => input.state.braceDepthStack.unshift(0))
)
.define({ tempStrRight: /\}(?:\\.|[^\\`$])*(\$\{|`|$)/ }, (a) =>
a
.reject(
({ output, input }) =>
input.state.braceDepthStack[0] !== 0 || // brace not close
input.state.braceDepthStack.length === 1 || // not in template string
output.content.endsWith("${") // should be tempStrMiddle
)
.then(({ input }) => input.state.braceDepthStack.shift())
)
.define(
{ tempStrMiddle: /\}(?:\\.|[^\\`$])*(\$\{|`|$)/ },
// reject if not in template string
(a) =>
a.reject(
({ input, output }) =>
input.state.braceDepthStack[0] !== 0 || // brace not close
input.state.braceDepthStack.length === 1 || // not in template string
!output.content.endsWith("${") // should be tempStrRight
)
)
.build({ debug: config.debug });
export function tsStringParser(
document: vscode.TextDocument,
position: vscode.Position,
cancel: vscode.CancellationToken
) {
// we have to get the whole document because multi-line string is allowed
const text = document.getText();
const offset = document.offsetAt(position);
lexer.reset().feed(text);
const tempStrStack = [] as NonNullable<ReturnType<(typeof lexer)["lex"]>>[][];
/**
* `undefined` if the hover is not in a template string.
* Otherwise, it's the index of the template string in `tempStrStack`.
*/
let targetTempStrIndex: number | undefined = undefined;
while (true) {
// just return if cancellation is requested
if (cancel.isCancellationRequested) {
return;
}
const token = lexer.lex();
if (token === null) {
// no more tokens
return;
}
// if simple string or simple template string(no interpolation)
if (
token.kind === "string" &&
token.start <= offset &&
token.start + token.content.length >= offset
) {
// got a string token, and the position is in the token
// don't show hover if the string is not escaped and no newline in it
if (
token.content.indexOf("\\") === -1 &&
token.content.indexOf("\n") === -1
) {
if (config.debug) {
console.log(`got simple: ${token.content}`);
}
return;
}
return evalTsString(token.content);
}
// if the hover is in a template string, set targetTempStrIndex
if (
["tempStrLeft", "tempStrMiddle", "tempStrRight"].includes(token.kind) &&
token.start <= offset &&
offset <= token.start + token.content.length
) {
targetTempStrIndex =
token.kind === "tempStrLeft"
? tempStrStack.length
: tempStrStack.length - 1;
}
if (token.kind === "tempStrLeft") {
tempStrStack.push([token]);
} else if (token.kind === "tempStrMiddle") {
tempStrStack.at(-1)!.push(token);
} else if (token.kind === "tempStrRight") {
const tokens = tempStrStack.pop()!;
tokens.push(token);
if (targetTempStrIndex === tempStrStack.length) {
// got the target template string, calculate string value
const quoted = tokens.map((t) => t.content).join("...");
return evalJsonString(quoted);
}
}
// perf: if current token's end is after the position, no need to continue.
// make sure current token is a simple string, and not in a temp string
// otherwise the hover target may be the tempStrMiddle/tempStrRight which is after the position
if (
token.kind === "string" &&
tempStrStack.length === 0 &&
token.start + token.content.length > offset
) {
return;
}
// else, got token but not string, continue
}
}
// TODO: make this a Lexer utils in retsac
function evalTsString(quoted: string) {
// remove quotes
const quote = quoted[0];
const unquoted = quoted.slice(
// remove the first quote
1,
// the string might be un-closed, so the last char might not be the quote.
// maybe the last char of the un-closed string is an escaped quote
quoted.at(-1) === quote && quoted.at(-2) !== "\\" ? -1 : undefined
);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#literals
// IMPORTANT! all escaped chars should be searched simultaneously!
// e.g. you should NOT search `\\` first then search `\n`
return unquoted.replace(
/(\\0|\\'|\\"|\\n|\\\\|\\r|\\v|\\t|\\b|\\f|\\\n|\\`|\\x([0-9a-fA-F]{2})|\\u([0-9a-fA-F]{4}))/g,
(match) => {
if (match === `\\0`) {
return "\0";
} else if (match === `\\'`) {
return "'";
} else if (match === `\\"`) {
return '"';
} else if (match === `\\n`) {
return "\n";
} else if (match === `\\\\`) {
return "\\";
} else if (match === `\\r`) {
return "\r";
} else if (match === `\\v`) {
return "\v";
} else if (match === `\\t`) {
return "\t";
} else if (match === `\\b`) {
return "\b";
} else if (match === `\\f`) {
return "\f";
} else if (match === `\\\n`) {
return "";
} else if (match === "\\`") {
return "`";
} else if (match.startsWith("\\x")) {
return String.fromCharCode(parseInt(match.slice(2), 16));
} else {
// match.startsWith("\\u")
return String.fromCharCode(parseInt(match.slice(4), 16));
}
}
);
}