Playground: doc explorer (#10183)

This commit is contained in:
Georgii Dolzhykov authored and GitHub committed 2022-07-23 22:46:16 +03:00
1 parent ab72a2c11c
commit b65cb6c1f6
13 files changed
+217 -24

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
#### "Doc Explorer" mode for the Playground (#10183 by @thorn0)
Switch the `parser` option to the special `doc-explorer` value to play with Prettier's intermediate representation and see how it's printed with different options.
+2
View File
@@ -9,4 +9,6 @@ The basic idea is that the printer takes an AST and returns an intermediate repr
This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `["(", line, arg, line, ")"]` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified. This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `["(", line, arg, line, ")"]` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified.
The [Playground](https://prettier.io/playground) has a special mode for exploring how Prettier’s intermediate representation is printed. To get there, open the sidebar (the "Show options" button) and set the `parser` option to the special value `doc-explorer`.
More (rough) details can be found in [commands.md](https://github.com/prettier/prettier/blob/main/commands.md). More (rough) details can be found in [commands.md](https://github.com/prettier/prettier/blob/main/commands.md).
+4
View File
@@ -4,3 +4,7 @@
[build.environment] [build.environment]
NODE_VERSION = "lts/*" NODE_VERSION = "lts/*"
YARN_VERSION = "1.22.15" YARN_VERSION = "1.22.15"
[[headers]]
for = "/worker.js"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-eval'"
+1 -1
View File
@@ -96,7 +96,7 @@ function parse(text, opts) {
} }
/* istanbul ignore next */ /* istanbul ignore next */
throw error.stack; throw error;
} }
} }
+72 -15
View File
@@ -67,10 +67,13 @@ class Playground extends React.Component {
options.parser = "babel"; options.parser = "babel";
} }
const content = original.content || getCodeSample(options.parser); const codeSample = getCodeSample(options.parser);
const content = original.content || codeSample;
const needsClickForFirstRun =
options.parser === "doc-explorer" && content !== codeSample;
const selection = {}; const selection = {};
this.state = { content, options, selection }; this.state = { content, options, selection, needsClickForFirstRun };
this.handleOptionValueChange = this.handleOptionValueChange.bind(this); this.handleOptionValueChange = this.handleOptionValueChange.bind(this);
@@ -80,14 +83,10 @@ class Playground extends React.Component {
this.setSelection = (selection) => this.setState({ selection }); this.setSelection = (selection) => this.setState({ selection });
this.setSelectionAsRange = () => { this.setSelectionAsRange = () => {
const { selection, content, options } = this.state; const { selection, content, options } = this.state;
const { head, anchor } = selection; const [rangeStart, rangeEnd] = util.convertSelectionToRange(
const range = [head, anchor].map( selection,
({ ch, line }) => content
content.split("\n").slice(0, line).join("\n").length +
ch +
(line ? 1 : 0)
); );
const [rangeStart, rangeEnd] = range.sort((a, b) => a - b);
const updatedOptions = { ...options, rangeStart, rangeEnd }; const updatedOptions = { ...options, rangeStart, rangeEnd };
if (rangeStart === rangeEnd) { if (rangeStart === rangeEnd) {
delete updatedOptions.rangeStart; delete updatedOptions.rangeStart;
@@ -103,6 +102,8 @@ class Playground extends React.Component {
this.rangeEndOption = props.availableOptions.find( this.rangeEndOption = props.availableOptions.find(
(opt) => opt.name === "rangeEnd" (opt) => opt.name === "rangeEnd"
); );
this.handleInputPanelFormat = this.handleInputPanelFormat.bind(this);
} }
componentDidUpdate(_, prevState) { componentDidUpdate(_, prevState) {
@@ -131,6 +132,10 @@ class Playground extends React.Component {
? getCodeSample(options.parser) ? getCodeSample(options.parser)
: state.content; : state.content;
if (option.name === "parser") {
state.needsClickForFirstRun = false;
}
return { options, content }; return { options, content };
}); });
} }
@@ -158,6 +163,30 @@ class Playground extends React.Component {
}); });
} }
handleInputPanelFormat() {
if (this.state.options.parser !== "doc-explorer") {
return;
}
const { content, selection } = this.state;
return this.props.worker
.format(content, {
parser: "__js_expression",
cursorOffset: util.convertSelectionToRange(selection, content)[0],
})
.then(({ error, formatted, cursorOffset }) => {
if (error) {
return;
}
return {
value: formatted,
cursor: util.convertOffsetToPosition(cursorOffset, formatted),
};
});
}
render() { render() {
const { worker, version } = this.props; const { worker, version } = this.props;
const { content, options } = this.state; const { content, options } = this.state;
@@ -171,6 +200,7 @@ class Playground extends React.Component {
<EditorState> <EditorState>
{(editorState) => ( {(editorState) => (
<PrettierFormat <PrettierFormat
enabled={!this.state.needsClickForFirstRun}
worker={worker} worker={worker}
code={content} code={content}
options={options} options={options}
@@ -259,9 +289,10 @@ class Playground extends React.Component {
checked={editorState.showSecondFormat} checked={editorState.showSecondFormat}
onChange={editorState.toggleSecondFormat} onChange={editorState.toggleSecondFormat}
/> />
{editorState.showDoc && debug.doc && ( {editorState.showDoc && (
<ClipboardButton <ClipboardButton
copy={() => this.getMarkdown({ doc: debug.doc })} copy={() => this.getMarkdown({ doc: debug.doc })}
disabled={!debug.doc}
> >
Copy doc Copy doc
</ClipboardButton> </ClipboardButton>
@@ -284,6 +315,7 @@ class Playground extends React.Component {
overlayEnd={options.rangeEnd} overlayEnd={options.rangeEnd}
onChange={this.setContent} onChange={this.setContent}
onSelectionChange={this.setSelection} onSelectionChange={this.setSelection}
onFormat={this.handleInputPanelFormat}
/> />
) : null} ) : null}
{editorState.showAst ? ( {editorState.showAst ? (
@@ -302,11 +334,36 @@ class Playground extends React.Component {
/> />
) : null} ) : null}
{editorState.showOutput ? ( {editorState.showOutput ? (
<OutputPanel this.state.needsClickForFirstRun ? (
mode={util.getCodemirrorMode(options.parser)} <div className="editor disabled-output-panel">
value={formatted} <div className="explanation">
ruler={options.printWidth} <code>doc-explorer</code> involves running code
/> provided by users.
</div>
<div className="explanation">
To stay on the safe side and prevent abuse, an
explicit user action is required when a direct
link to a <code>doc-explorer</code> playground is
opened.
</div>
<div className="explanation">
Click the button below to start the playground.
</div>
<Button
onClick={() =>
this.setState({ needsClickForFirstRun: false })
}
>
Start
</Button>
</div>
) : (
<OutputPanel
mode={util.getCodemirrorMode(options.parser)}
value={formatted}
ruler={options.printWidth}
/>
)
) : null} ) : null}
{editorState.showSecondFormat ? ( {editorState.showSecondFormat ? (
<OutputPanel <OutputPanel
+6
View File
@@ -12,6 +12,7 @@ export default class PrettierFormat extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
for (const key of [ for (const key of [
"enabled",
"code", "code",
"options", "options",
"debugAst", "debugAst",
@@ -28,6 +29,7 @@ export default class PrettierFormat extends React.Component {
format() { format() {
const { const {
enabled,
worker, worker,
code, code,
options, options,
@@ -37,6 +39,10 @@ export default class PrettierFormat extends React.Component {
reformat, reformat,
} = this.props; } = this.props;
if (!enabled) {
return;
}
worker worker
.format(code, options, { ast, doc, comments, reformat }) .format(code, options, { ast, doc, comments, reformat })
.then((result) => this.setState(result)); .then((result) => this.setState(result));
+1 -1
View File
@@ -32,7 +32,7 @@ export default function WorkerApi(source) {
getMetadata() { getMetadata() {
return postMessage({ type: "meta" }); return postMessage({ type: "meta" });
}, },
format(code, options, debug) { format(code, options, debug = {}) {
return postMessage({ type: "format", code, options, debug }); return postMessage({ type: "format", code, options, debug });
}, },
postMessage, postMessage,
+18
View File
@@ -308,6 +308,24 @@ export default function getCodeSamples(parser) {
" </body>", " </body>",
"</HTML>", "</HTML>",
].join("\n"); ].join("\n");
case "doc-explorer":
return [
"group([",
' "(*",',
" indent([",
" line,",
' "Prettier:",',
" line,",
' "Opinionated,",',
" line,",
' "Code,",',
" line,",
' "Formatter",',
" ]),",
" line,",
' "*)",',
"])",
].join("\n");
default: default:
return ""; return "";
} }
+1
View File
@@ -41,6 +41,7 @@ function getMarkdownSyntax(options) {
case "acorn": case "acorn":
case "espree": case "espree":
case "meriyah": case "meriyah":
case "doc-explorer":
return "jsx"; return "jsx";
case "babel-ts": case "babel-ts":
case "typescript": case "typescript":
+18
View File
@@ -19,6 +19,7 @@ class CodeMirrorPanel extends React.Component {
delete options.rulerColor; delete options.rulerColor;
delete options.value; delete options.value;
delete options.onChange; delete options.onChange;
delete options.onFormat;
options.rulers = [makeRuler(this.props)]; options.rulers = [makeRuler(this.props)];
@@ -26,6 +27,12 @@ class CodeMirrorPanel extends React.Component {
options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]; options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
} }
if (this.props.onFormat) {
options.extraKeys = {
"Shift-Alt-F": this.handleFormat.bind(this),
};
}
this._codeMirror = CodeMirror.fromTextArea( this._codeMirror = CodeMirror.fromTextArea(
this._textareaRef.current, this._textareaRef.current,
options options
@@ -112,6 +119,17 @@ class CodeMirrorPanel extends React.Component {
} }
} }
handleFormat() {
const result = this.props.onFormat();
if (!result) {
return;
}
Promise.resolve(result).then(({ value, cursor }) => {
this.props.onChange(value);
this._codeMirror.setCursor(cursor);
});
}
render() { render() {
return ( return (
<div className="editor input"> <div className="editor input">
+24
View File
@@ -95,6 +95,30 @@ export function getAstAutoFold(parser) {
} }
} }
export function convertSelectionToRange({ head, anchor }, content) {
const lines = content.split("\n");
return [head, anchor]
.map(
({ ch, line }) =>
lines.slice(0, line).join("\n").length + ch + (line ? 1 : 0)
)
.sort((a, b) => a - b);
}
export function convertOffsetToPosition(offset, content) {
let line = 0;
let ch = 0;
for (let i = 0; i < offset && i <= content.length; i++) {
if (content[i] === "\n") {
line++;
ch = 0;
} else {
ch++;
}
}
return { line, ch };
}
/** /**
* Copied from https://github.com/prettier/prettier/blob/6fe21780115cf5f74f83876d64b03a727fbab220/src/cli/utils.js#L6-L27 * Copied from https://github.com/prettier/prettier/blob/6fe21780115cf5f74f83876d64b03a727fbab220/src/cli/utils.js#L6-L27
* @template Obj * @template Obj
+24 -1
View File
@@ -112,6 +112,25 @@ header h1 {
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
} }
.disabled-output-panel {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
text-align: center;
}
.disabled-output-panel .explanation {
font-size: 16px;
max-width: 400px;
color: darkred;
}
.disabled-output-panel .btn {
font-size: 22px;
padding: 10px 20px;
}
/* display as 2x2 grid */ /* display as 2x2 grid */
@media (min-width: 800px) { @media (min-width: 800px) {
.options-container { .options-container {
@@ -258,7 +277,7 @@ input[type="number"] {
border-color: #c8e1ff; border-color: #c8e1ff;
} }
.btn:hover { .btn:hover:enabled {
background-color: #e6ebf1; background-color: #e6ebf1;
background-image: linear-gradient(to bottom, #f0f3f6, #dce3ec); background-image: linear-gradient(to bottom, #f0f3f6, #dce3ec);
border-color: #afb1b2; border-color: #afb1b2;
@@ -271,6 +290,10 @@ input[type="number"] {
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
} }
.btn:disabled {
color: #afb1b2;
}
.tooltip { .tooltip {
position: absolute; position: absolute;
z-index: 6; z-index: 6;
+43 -6
View File
@@ -30,6 +30,25 @@ for (const file in parsersLocation) {
} }
} }
const docExplorerPlugin = {
parsers: {
"doc-explorer": {
parse: (text) =>
new Function(
`{ ${Object.keys(prettier.doc.builders)} }`,
`const result = (${text || "''"}\n); return result;`
)(prettier.doc.builders),
astFormat: "doc-explorer",
},
},
printers: {
"doc-explorer": {
print: (path) => path.getValue(),
},
},
languages: [{ name: "doc-explorer", parsers: ["doc-explorer"] }],
};
self.onmessage = function (event) { self.onmessage = function (event) {
self.postMessage({ self.postMessage({
uid: event.data.uid, uid: event.data.uid,
@@ -45,6 +64,8 @@ function serializeAst(ast) {
? { name: value.name, message: value.message, ...value } ? { name: value.name, message: value.message, ...value }
: typeof value === "bigint" : typeof value === "bigint"
? `BigInt('${String(value)}')` ? `BigInt('${String(value)}')`
: typeof value === "symbol"
? String(value)
: value, : value,
2 2
); );
@@ -58,6 +79,7 @@ function handleMessage(message) {
JSON.stringify( JSON.stringify(
prettier.getSupportInfo({ prettier.getSupportInfo({
showUnreleased: true, showUnreleased: true,
plugins: [docExplorerPlugin],
}) })
) )
), ),
@@ -72,13 +94,15 @@ function handleMessage(message) {
delete options.doc; delete options.doc;
delete options.output2; delete options.output2;
const plugins = [{ parsers }]; const plugins = [{ parsers }, docExplorerPlugin];
options.plugins = plugins; options.plugins = plugins;
const formatResult = formatCode(message.code, options); const formatResult = formatCode(message.code, options);
const response = { const response = {
formatted: formatResult.formatted, formatted: formatResult.formatted,
cursorOffset: formatResult.cursorOffset,
error: formatResult.error,
debug: { debug: {
ast: null, ast: null,
doc: null, doc: null,
@@ -111,10 +135,10 @@ function handleMessage(message) {
try { try {
response.debug.doc = prettier.__debug.formatDoc( response.debug.doc = prettier.__debug.formatDoc(
prettier.__debug.printToDoc(message.code, options), prettier.__debug.printToDoc(message.code, options),
{ parser: "babel", plugins } { plugins }
); );
} catch (e) { } catch {
response.debug.doc = String(e); response.debug.doc = "";
} }
} }
@@ -142,10 +166,23 @@ function formatCode(text, options) {
} catch (e) { } catch (e) {
if (e.constructor && e.constructor.name === "SyntaxError") { if (e.constructor && e.constructor.name === "SyntaxError") {
// Likely something wrong with the user's code // Likely something wrong with the user's code
return { formatted: String(e) }; return { formatted: String(e), error: true };
} }
// Likely a bug in Prettier // Likely a bug in Prettier
// Provide the whole stack for debugging // Provide the whole stack for debugging
return { formatted: e.stack || String(e) }; return { formatted: stringifyError(e), error: true };
} }
} }
function stringifyError(e) {
const stringified = String(e);
if (typeof e.stack !== "string") {
return stringified;
}
if (e.stack.includes(stringified)) {
// Chrome
return e.stack;
}
// Firefox
return stringified + "\n" + e.stack;
}