Skip to content

Commit

Permalink
Playground: Add format button and notification when saving (#2623)
Browse files Browse the repository at this point in the history
  • Loading branch information
timotheeguerin authored Nov 6, 2023
1 parent adba3b9 commit f757a19
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"changes": [
{
"packageName": "@typespec/playground",
"comment": "Add a format button to the playground command bar",
"type": "none"
},
{
"packageName": "@typespec/playground",
"comment": "Add a notification to the standalone playground when the playground has been saved.",
"type": "none"
}
],
"packageName": "@typespec/playground"
}
22 changes: 13 additions & 9 deletions packages/playground/src/react/editor-command-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Button,
Dialog,
DialogBody,
DialogSurface,
Expand All @@ -9,7 +8,12 @@ import {
ToolbarButton,
Tooltip,
} from "@fluentui/react-components";
import { Bug16Regular, Save16Regular, Settings24Regular } from "@fluentui/react-icons";
import {
Broom16Filled,
Bug16Regular,
Save16Regular,
Settings24Regular,
} from "@fluentui/react-icons";
import { CompilerOptions } from "@typespec/compiler";
import { FunctionComponent } from "react";
import { PlaygroundSample, PlaygroundTspLibrary } from "../types.js";
Expand All @@ -20,6 +24,7 @@ import { CompilerSettings } from "./settings/compiler-settings.js";
export interface EditorCommandBarProps {
documentationUrl?: string;
saveCode: () => Promise<void> | void;
formatCode: () => Promise<void> | void;
newIssue?: () => Promise<void> | void;
libraries: PlaygroundTspLibrary[];
selectedEmitter: string;
Expand All @@ -34,6 +39,7 @@ export interface EditorCommandBarProps {
export const EditorCommandBar: FunctionComponent<EditorCommandBarProps> = ({
documentationUrl,
saveCode,
formatCode,
newIssue,
libraries,
selectedEmitter,
Expand All @@ -58,12 +64,10 @@ export const EditorCommandBar: FunctionComponent<EditorCommandBarProps> = ({
<div css={{ borderBottom: "1px solid #f5f5f5" }}>
<Toolbar>
<Tooltip content="Save" relationship="description" withArrow>
<ToolbarButton
appearance="primary"
aria-label="Save"
icon={<Save16Regular />}
onClick={saveCode as any}
/>
<ToolbarButton aria-label="Save" icon={<Save16Regular />} onClick={saveCode as any} />
</Tooltip>
<Tooltip content="Format" relationship="description" withArrow>
<ToolbarButton aria-label="Format" icon={<Broom16Filled />} onClick={formatCode as any} />
</Tooltip>
{samples && (
<SamplesDropdown
Expand All @@ -79,7 +83,7 @@ export const EditorCommandBar: FunctionComponent<EditorCommandBarProps> = ({
/>
<Dialog>
<DialogTrigger>
<Button icon={<Settings24Regular />} />
<ToolbarButton icon={<Settings24Regular />} />
</DialogTrigger>
<DialogSurface>
<DialogBody>
Expand Down
8 changes: 7 additions & 1 deletion packages/playground/src/react/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ export interface EditorProps {
model: editor.IModel;
actions?: editor.IActionDescriptor[];
options: editor.IStandaloneEditorConstructionOptions;
onMount?: (data: OnMountData) => void;
}

export interface OnMountData {
editor: editor.IStandaloneCodeEditor;
}

export interface EditorCommand {
binding: number;
handle: () => void;
}

export const Editor: FunctionComponent<EditorProps> = ({ model, options, actions }) => {
export const Editor: FunctionComponent<EditorProps> = ({ model, options, actions, onMount }) => {
const editorContainerRef = useRef(null);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);

Expand All @@ -22,6 +27,7 @@ export const Editor: FunctionComponent<EditorProps> = ({ model, options, actions
automaticLayout: true,
...options,
});
onMount?.({ editor: editorRef.current });
}, []);

useEffect(() => {
Expand Down
21 changes: 18 additions & 3 deletions packages/playground/src/react/playground.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { CompilerOptions } from "@typespec/compiler";
import debounce from "debounce";
import { KeyCode, KeyMod, MarkerSeverity, Uri, editor } from "monaco-editor";
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react";
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CompletionItemTag } from "vscode-languageserver";
import { getMarkerLocation } from "../services.js";
import { BrowserHost, PlaygroundSample } from "../types.js";
import { EditorCommandBar } from "./editor-command-bar.js";
import { useMonacoModel } from "./editor.js";
import { OnMountData, useMonacoModel } from "./editor.js";
import { Footer } from "./footer.js";
import { useControllableValue } from "./hooks.js";
import { OutputView } from "./output-view.js";
Expand Down Expand Up @@ -80,6 +80,8 @@ export interface PlaygroundLinks {

export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
const { host, onSave } = props;
const editorRef = useRef<editor.IStandaloneCodeEditor | undefined>(undefined);

const [selectedEmitter, onSelectedEmitterChange] = useControllableValue(
props.emitter,
props.defaultEmitter,
Expand Down Expand Up @@ -181,6 +183,10 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
isSampleUntouched,
]);

const formatCode = useCallback(() => {
void editorRef.current?.getAction("editor.action.formatDocument")?.run();
}, [typespecModel]);

const newIssue = useCallback(async () => {
saveCode();
const bodyPayload = encodeURIComponent(`\n\n\n[Playground Link](${document.location.href})`);
Expand All @@ -196,6 +202,10 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
[saveCode]
);

const onTypeSpecEditorMount = useCallback(({ editor }: OnMountData) => {
editorRef.current = editor;
}, []);

const libraries = useMemo(() => Object.values(host.libraries), [host.libraries]);

return (
Expand Down Expand Up @@ -226,10 +236,15 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
selectedSampleName={selectedSampleName}
onSelectedSampleNameChange={onSelectedSampleNameChange}
saveCode={saveCode}
formatCode={formatCode}
newIssue={props?.links?.githubIssueUrl ? newIssue : undefined}
documentationUrl={props.links?.documentationUrl}
/>
<TypeSpecEditor model={typespecModel} actions={typespecEditorActions} />
<TypeSpecEditor
model={typespecModel}
actions={typespecEditorActions}
onMount={onTypeSpecEditorMount}
/>
</Pane>
<Pane>
<OutputView
Expand Down
53 changes: 38 additions & 15 deletions packages/playground/src/react/standalone.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { FunctionComponent } from "react";
import {
FluentProvider,
Toast,
ToastBody,
ToastTitle,
Toaster,
useToastController,
webLightTheme,
} from "@fluentui/react-components";
import { FunctionComponent, useId, useMemo } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserHost } from "../browser-host.js";
import { LibraryImportOptions } from "../core.js";
Expand All @@ -20,23 +28,38 @@ export async function createReactPlayground(config: ReactPlaygroundConfig) {

const stateStorage = createStandalonePlaygroundStateStorage();
const initialState = stateStorage.load();
const options: PlaygroundProps = {
...config,
host,
libraries: config.libraries,
defaultContent: initialState.content,
defaultEmitter: initialState.emitter ?? config.defaultEmitter,
defaultCompilerOptions: initialState.options,
defaultSampleName: initialState.sampleName,
onSave: (value) => {
stateStorage.save(value);
void navigator.clipboard.writeText(window.location.toString());
},
};

const App: FunctionComponent = () => {
const toasterId = useId();
const { dispatchToast } = useToastController(toasterId);

const options: PlaygroundProps = useMemo(
() => ({
...config,
host,
libraries: config.libraries,
defaultContent: initialState.content,
defaultEmitter: initialState.emitter ?? config.defaultEmitter,
defaultCompilerOptions: initialState.options,
defaultSampleName: initialState.sampleName,
onSave: (value) => {
stateStorage.save(value);
void navigator.clipboard.writeText(window.location.toString());
dispatchToast(
<Toast>
<ToastTitle>Saved!</ToastTitle>
<ToastBody>Playground link has been copied to the clipboard.</ToastBody>
</Toast>,
{ intent: "success" }
);
},
}),
[dispatchToast]
);

return (
<FluentProvider theme={webLightTheme}>
<Toaster toasterId={toasterId} />
<div css={{ height: "100vh" }}>
<Playground {...options} />
</div>
Expand Down
19 changes: 11 additions & 8 deletions packages/playground/src/react/typespec-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { editor } from "monaco-editor";
import { FunctionComponent } from "react";
import { Editor, useMonacoModel } from "./editor.js";
import { Editor, EditorProps, useMonacoModel } from "./editor.js";

export interface TypeSpecEditorProps {
model: editor.IModel;
actions?: editor.IActionDescriptor[];
export interface TypeSpecEditorProps extends Omit<EditorProps, "options"> {
options?: editor.IStandaloneEditorConstructionOptions;
}

export const TypeSpecEditor: FunctionComponent<TypeSpecEditorProps> = (props) => {
const options: editor.IStandaloneEditorConstructionOptions = {
export const TypeSpecEditor: FunctionComponent<TypeSpecEditorProps> = ({
actions,
options,
...other
}) => {
const resolvedOptions: editor.IStandaloneEditorConstructionOptions = {
"semanticHighlighting.enabled": true,
automaticLayout: true,
tabSize: 2,
minimap: {
enabled: false,
},
...options,
};
// Add shortcuts
return <Editor model={props.model} actions={props.actions} options={options}></Editor>;
return <Editor actions={actions} options={resolvedOptions} {...other}></Editor>;
};

export const OutputEditor: FunctionComponent<{ filename: string; value: string }> = ({
Expand Down

0 comments on commit f757a19

Please sign in to comment.