Skip to content

Commit e371ab2

Browse files
authored
Merge pull request #1141 from nojaf/paste-as-rescript-json
Add paste as JSON.t, ReScript JSX
2 parents b862b0b + 036f216 commit e371ab2

File tree

8 files changed

+1126
-8
lines changed

8 files changed

+1126
-8
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
> - :house: [Internal]
1111
> - :nail_care: [Polish]
1212
13+
## master
14+
15+
#### :rocket: New Feature
16+
17+
- Paste as JSON.t or ReScript JSX in VSCode. https://github.com/rescript-lang/rescript-vscode/pull/1141
18+
1319
## 1.66.0
1420

1521
#### :bug: Bug fix

client/src/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export { openCompiled } from "./commands/open_compiled";
1010
export { switchImplIntf } from "./commands/switch_impl_intf";
1111
export { dumpDebug, dumpDebugRetrigger } from "./commands/dump_debug";
1212
export { dumpServerState } from "./commands/dump_server_state";
13+
export { pasteAsRescriptJson } from "./commands/paste_as_rescript_json";
14+
export { pasteAsRescriptJsx } from "./commands/paste_as_rescript_jsx";
1315

1416
export const codeAnalysisWithReanalyze = (
1517
targetDir: string | null,
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { env, window, Position, Selection, TextDocument } from "vscode";
2+
3+
const INDENT_SIZE = 2;
4+
const INDENT_UNIT = " ".repeat(INDENT_SIZE);
5+
6+
const indent = (level: number) => INDENT_UNIT.repeat(level);
7+
8+
const isLikelyJson = (text: string): boolean => {
9+
const trimmed = text.trim();
10+
if (trimmed.length === 0) {
11+
return false;
12+
}
13+
const first = trimmed[0];
14+
if (first === "{" || first === "[" || first === '"' || first === "-") {
15+
return true;
16+
}
17+
if (first >= "0" && first <= "9") {
18+
return true;
19+
}
20+
if (
21+
trimmed.startsWith("true") ||
22+
trimmed.startsWith("false") ||
23+
trimmed.startsWith("null")
24+
) {
25+
return true;
26+
}
27+
return false;
28+
};
29+
30+
const ensureFloatString = (value: number): string => {
31+
const raw = Number.isFinite(value) ? String(value) : "0";
32+
if (raw.includes(".") || raw.includes("e") || raw.includes("E")) {
33+
return raw;
34+
}
35+
return `${raw}.`;
36+
};
37+
38+
const formatJsonValue = (value: unknown, level = 0): string => {
39+
if (value === null) {
40+
return "JSON.Null";
41+
}
42+
43+
switch (typeof value) {
44+
case "string":
45+
return `JSON.String(${JSON.stringify(value)})`;
46+
case "number":
47+
return `JSON.Number(${ensureFloatString(value)})`;
48+
case "boolean":
49+
return `JSON.Boolean(${value})`;
50+
case "object":
51+
if (Array.isArray(value)) {
52+
return formatArray(value, level);
53+
}
54+
return formatObject(value as Record<string, unknown>, level);
55+
default:
56+
return "JSON.Null";
57+
}
58+
};
59+
60+
const formatObject = (
61+
value: Record<string, unknown>,
62+
level: number,
63+
): string => {
64+
const entries = Object.entries(value);
65+
if (entries.length === 0) {
66+
return "JSON.Object(dict{})";
67+
}
68+
const nextLevel = level + 1;
69+
const lines = entries.map(
70+
([key, val]) =>
71+
`${indent(nextLevel)}${JSON.stringify(key)}: ${formatJsonValue(
72+
val,
73+
nextLevel,
74+
)}`,
75+
);
76+
return `JSON.Object(dict{\n${lines.join(",\n")}\n${indent(level)}})`;
77+
};
78+
79+
const formatArray = (values: unknown[], level: number): string => {
80+
if (values.length === 0) {
81+
return "JSON.Array([])";
82+
}
83+
const nextLevel = level + 1;
84+
const lines = values.map(
85+
(item) => `${indent(nextLevel)}${formatJsonValue(item, nextLevel)}`,
86+
);
87+
return `JSON.Array([\n${lines.join(",\n")}\n${indent(level)}])`;
88+
};
89+
90+
export type JsonConversionResult =
91+
| { kind: "success"; formatted: string }
92+
| { kind: "notJson" }
93+
| { kind: "error"; errorMessage: string };
94+
95+
export const convertPlainTextToJsonT = (text: string): JsonConversionResult => {
96+
if (!isLikelyJson(text)) {
97+
return { kind: "notJson" };
98+
}
99+
100+
try {
101+
const parsed = JSON.parse(text);
102+
return { kind: "success", formatted: formatJsonValue(parsed) };
103+
} catch {
104+
return {
105+
kind: "error",
106+
errorMessage: "Clipboard JSON could not be parsed.",
107+
};
108+
}
109+
};
110+
111+
export const getBaseIndent = (
112+
document: TextDocument,
113+
position: Position,
114+
): string => {
115+
const linePrefix = document
116+
.lineAt(position)
117+
.text.slice(0, position.character);
118+
return /^\s*$/.test(linePrefix) ? linePrefix : "";
119+
};
120+
121+
export const applyBaseIndent = (formatted: string, baseIndent: string) => {
122+
if (baseIndent.length === 0) {
123+
return formatted;
124+
}
125+
126+
return formatted
127+
.split("\n")
128+
.map((line, index) => (index === 0 ? line : `${baseIndent}${line}`))
129+
.join("\n");
130+
};
131+
132+
export const buildInsertionText = (
133+
document: TextDocument,
134+
position: Position,
135+
formatted: string,
136+
) => {
137+
const baseIndent = getBaseIndent(document, position);
138+
return applyBaseIndent(formatted, baseIndent);
139+
};
140+
141+
const computeEndPosition = (
142+
insertionStart: Position,
143+
indentedText: string,
144+
): Position => {
145+
const lines = indentedText.split("\n");
146+
if (lines.length === 1) {
147+
return insertionStart.translate(0, lines[0].length);
148+
}
149+
return new Position(
150+
insertionStart.line + lines.length - 1,
151+
lines[lines.length - 1].length,
152+
);
153+
};
154+
155+
export const pasteAsRescriptJson = async () => {
156+
const editor = window.activeTextEditor;
157+
if (!editor) {
158+
window.showInformationMessage(
159+
"No active editor to paste the ReScript JSON into.",
160+
);
161+
return;
162+
}
163+
164+
const clipboardText = await env.clipboard.readText();
165+
const conversion = convertPlainTextToJsonT(clipboardText);
166+
167+
if (conversion.kind === "notJson") {
168+
window.showInformationMessage("Clipboard does not appear to contain JSON.");
169+
return;
170+
}
171+
172+
if (conversion.kind === "error") {
173+
window.showErrorMessage("Clipboard JSON could not be parsed.");
174+
return;
175+
}
176+
177+
const formatted = conversion.formatted;
178+
const selection = editor.selection;
179+
const indentedText = buildInsertionText(
180+
editor.document,
181+
selection.start,
182+
formatted,
183+
);
184+
const insertionStart = selection.start;
185+
const didEdit = await editor.edit((editBuilder) => {
186+
editBuilder.replace(selection, indentedText);
187+
});
188+
189+
if (didEdit) {
190+
const endPosition = computeEndPosition(insertionStart, indentedText);
191+
editor.selection = new Selection(endPosition, endPosition);
192+
}
193+
};
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { env, window, Position, Selection } from "vscode";
2+
3+
import { buildInsertionText } from "./paste_as_rescript_json";
4+
import { transformJsx } from "./transform-jsx";
5+
6+
export type JsxConversionResult =
7+
| { kind: "success"; formatted: string }
8+
| { kind: "empty" }
9+
| { kind: "error"; errorMessage: string };
10+
11+
export const convertPlainTextToRescriptJsx = (
12+
text: string,
13+
): JsxConversionResult => {
14+
if (text.trim().length === 0) {
15+
return { kind: "empty" };
16+
}
17+
18+
try {
19+
// If you ever need to fix a bug in transformJsx,
20+
// please do so in https://github.com/nojaf/vanilla-jsx-to-rescript-jsx/blob/main/index.ts
21+
// and then copy the changes to transform-jsx.ts
22+
const formatted = transformJsx(text);
23+
return { kind: "success", formatted };
24+
} catch (error) {
25+
const errorMessage =
26+
error instanceof Error ? error.message : "Unknown conversion error.";
27+
return {
28+
kind: "error",
29+
errorMessage,
30+
};
31+
}
32+
};
33+
34+
const computeEndPosition = (
35+
insertionStart: Position,
36+
indentedText: string,
37+
): Position => {
38+
const lines = indentedText.split("\n");
39+
if (lines.length === 1) {
40+
return insertionStart.translate(0, lines[0].length);
41+
}
42+
return new Position(
43+
insertionStart.line + lines.length - 1,
44+
lines[lines.length - 1].length,
45+
);
46+
};
47+
48+
export const pasteAsRescriptJsx = async () => {
49+
const editor = window.activeTextEditor;
50+
if (!editor) {
51+
window.showInformationMessage(
52+
"No active editor to paste the ReScript JSX into.",
53+
);
54+
return;
55+
}
56+
57+
const clipboardText = await env.clipboard.readText();
58+
const conversion = convertPlainTextToRescriptJsx(clipboardText);
59+
60+
if (conversion.kind === "empty") {
61+
window.showInformationMessage(
62+
"Clipboard does not appear to contain any JSX content.",
63+
);
64+
return;
65+
}
66+
67+
if (conversion.kind === "error") {
68+
window.showErrorMessage(
69+
`Clipboard JSX could not be transformed: ${conversion.errorMessage}`,
70+
);
71+
return;
72+
}
73+
74+
const formatted = conversion.formatted;
75+
const selection = editor.selection;
76+
const indentedText = buildInsertionText(
77+
editor.document,
78+
selection.start,
79+
formatted,
80+
);
81+
const insertionStart = selection.start;
82+
const didEdit = await editor.edit((editBuilder) => {
83+
editBuilder.replace(selection, indentedText);
84+
});
85+
86+
if (didEdit) {
87+
const endPosition = computeEndPosition(insertionStart, indentedText);
88+
editor.selection = new Selection(endPosition, endPosition);
89+
}
90+
};

0 commit comments

Comments
 (0)