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.
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).
+4
View File
@@ -4,3 +4,7 @@
[build.environment]
NODE_VERSION = "lts/*"
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 */
throw error.stack;
throw error;
}
}
+72 -15
View File
@@ -67,10 +67,13 @@ class Playground extends React.Component {
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 = {};
this.state = { content, options, selection };
this.state = { content, options, selection, needsClickForFirstRun };
this.handleOptionValueChange = this.handleOptionValueChange.bind(this);
@@ -80,14 +83,10 @@ class Playground extends React.Component {
this.setSelection = (selection) => this.setState({ selection });
this.setSelectionAsRange = () => {
const { selection, content, options } = this.state;
const { head, anchor } = selection;
const range = [head, anchor].map(
({ ch, line }) =>
content.split("\n").slice(0, line).join("\n").length +
ch +
(line ? 1 : 0)
const [rangeStart, rangeEnd] = util.convertSelectionToRange(
selection,
content
);
const [rangeStart, rangeEnd] = range.sort((a, b) => a - b);
const updatedOptions = { ...options, rangeStart, rangeEnd };
if (rangeStart === rangeEnd) {
delete updatedOptions.rangeStart;
@@ -103,6 +102,8 @@ class Playground extends React.Component {
this.rangeEndOption = props.availableOptions.find(
(opt) => opt.name === "rangeEnd"
);
this.handleInputPanelFormat = this.handleInputPanelFormat.bind(this);
}
componentDidUpdate(_, prevState) {
@@ -131,6 +132,10 @@ class Playground extends React.Component {
? getCodeSample(options.parser)
: state.content;
if (option.name === "parser") {
state.needsClickForFirstRun = false;
}
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() {
const { worker, version } = this.props;
const { content, options } = this.state;
@@ -171,6 +200,7 @@ class Playground extends React.Component {
<EditorState>
{(editorState) => (
<PrettierFormat
enabled={!this.state.needsClickForFirstRun}
worker={worker}
code={content}
options={options}
@@ -259,9 +289,10 @@ class Playground extends React.Component {
checked={editorState.showSecondFormat}
onChange={editorState.toggleSecondFormat}
/>
{editorState.showDoc && debug.doc && (
{editorState.showDoc && (
<ClipboardButton
copy={() => this.getMarkdown({ doc: debug.doc })}
disabled={!debug.doc}
>
Copy doc
</ClipboardButton>
@@ -284,6 +315,7 @@ class Playground extends React.Component {
overlayEnd={options.rangeEnd}
onChange={this.setContent}
onSelectionChange={this.setSelection}
onFormat={this.handleInputPanelFormat}
/>
) : null}
{editorState.showAst ? (
@@ -302,11 +334,36 @@ class Playground extends React.Component {
/>
) : null}
{editorState.showOutput ? (
<OutputPanel
mode={util.getCodemirrorMode(options.parser)}
value={formatted}
ruler={options.printWidth}
/>
this.state.needsClickForFirstRun ? (
<div className="editor disabled-output-panel">
<div className="explanation">
<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}
{editorState.showSecondFormat ? (
<OutputPanel
+6
View File
@@ -12,6 +12,7 @@ export default class PrettierFormat extends React.Component {
componentDidUpdate(prevProps) {
for (const key of [
"enabled",
"code",
"options",
"debugAst",
@@ -28,6 +29,7 @@ export default class PrettierFormat extends React.Component {
format() {
const {
enabled,
worker,
code,
options,
@@ -37,6 +39,10 @@ export default class PrettierFormat extends React.Component {
reformat,
} = this.props;
if (!enabled) {
return;
}
worker
.format(code, options, { ast, doc, comments, reformat })
.then((result) => this.setState(result));
+1 -1
View File
@@ -32,7 +32,7 @@ export default function WorkerApi(source) {
getMetadata() {
return postMessage({ type: "meta" });
},
format(code, options, debug) {
format(code, options, debug = {}) {
return postMessage({ type: "format", code, options, debug });
},
postMessage,
+18
View File
@@ -308,6 +308,24 @@ export default function getCodeSamples(parser) {
" </body>",
"</HTML>",
].join("\n");
case "doc-explorer":
return [
"group([",
' "(*",',
" indent([",
" line,",
' "Prettier:",',
" line,",
' "Opinionated,",',
" line,",
' "Code,",',
" line,",
' "Formatter",',
" ]),",
" line,",
' "*)",',
"])",
].join("\n");
default:
return "";
}
+1
View File
@@ -41,6 +41,7 @@ function getMarkdownSyntax(options) {
case "acorn":
case "espree":
case "meriyah":
case "doc-explorer":
return "jsx";
case "babel-ts":
case "typescript":
+18
View File
@@ -19,6 +19,7 @@ class CodeMirrorPanel extends React.Component {
delete options.rulerColor;
delete options.value;
delete options.onChange;
delete options.onFormat;
options.rulers = [makeRuler(this.props)];
@@ -26,6 +27,12 @@ class CodeMirrorPanel extends React.Component {
options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
}
if (this.props.onFormat) {
options.extraKeys = {
"Shift-Alt-F": this.handleFormat.bind(this),
};
}
this._codeMirror = CodeMirror.fromTextArea(
this._textareaRef.current,
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() {
return (
<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
* @template Obj
+24 -1
View File
@@ -112,6 +112,25 @@ header h1 {
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 */
@media (min-width: 800px) {
.options-container {
@@ -258,7 +277,7 @@ input[type="number"] {
border-color: #c8e1ff;
}
.btn:hover {
.btn:hover:enabled {
background-color: #e6ebf1;
background-image: linear-gradient(to bottom, #f0f3f6, #dce3ec);
border-color: #afb1b2;
@@ -271,6 +290,10 @@ input[type="number"] {
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
}
.btn:disabled {
color: #afb1b2;
}
.tooltip {
position: absolute;
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.postMessage({
uid: event.data.uid,
@@ -45,6 +64,8 @@ function serializeAst(ast) {
? { name: value.name, message: value.message, ...value }
: typeof value === "bigint"
? `BigInt('${String(value)}')`
: typeof value === "symbol"
? String(value)
: value,
2
);
@@ -58,6 +79,7 @@ function handleMessage(message) {
JSON.stringify(
prettier.getSupportInfo({
showUnreleased: true,
plugins: [docExplorerPlugin],
})
)
),
@@ -72,13 +94,15 @@ function handleMessage(message) {
delete options.doc;
delete options.output2;
const plugins = [{ parsers }];
const plugins = [{ parsers }, docExplorerPlugin];
options.plugins = plugins;
const formatResult = formatCode(message.code, options);
const response = {
formatted: formatResult.formatted,
cursorOffset: formatResult.cursorOffset,
error: formatResult.error,
debug: {
ast: null,
doc: null,
@@ -111,10 +135,10 @@ function handleMessage(message) {
try {
response.debug.doc = prettier.__debug.formatDoc(
prettier.__debug.printToDoc(message.code, options),
{ parser: "babel", plugins }
{ plugins }
);
} catch (e) {
response.debug.doc = String(e);
} catch {
response.debug.doc = "";
}
}
@@ -142,10 +166,23 @@ function formatCode(text, options) {
} catch (e) {
if (e.constructor && e.constructor.name === "SyntaxError") {
// Likely something wrong with the user's code
return { formatted: String(e) };
return { formatted: String(e), error: true };
}
// Likely a bug in Prettier
// 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;
}