diff --git a/build/Dockerfile b/build/Dockerfile index 7bda7fe8..10eed988 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -24,7 +24,8 @@ WORKDIR /opt/playground ENV GOROOT /usr/local/go ENV APP_CLEAN_INTERVAL=10m ENV APP_DEBUG=false -ENV APP_PLAYGROUND_URL=https://play.golang.org +ENV APP_PLAYGROUND_URL='https://play.golang.org' +ENV APP_GOTIP_URL='https://gotipplay.golang.org' COPY data ./data COPY --from=ui-build /tmp/web/build ./public COPY --from=build /tmp/playground/server . @@ -32,4 +33,4 @@ COPY --from=build /tmp/playground/worker.wasm ./public COPY --from=build /tmp/playground/wasm_exec.js ./public EXPOSE 8000 ENTRYPOINT /opt/playground/server -f=/opt/playground/data/packages.json -addr=:8000 \ - -clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG} -playground-url=${APP_PLAYGROUND_URL} \ No newline at end of file + -clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG} -playground-url=${APP_PLAYGROUND_URL} -gotip-url=${APP_GOTIP_URL} \ No newline at end of file diff --git a/cmd/playground/main.go b/cmd/playground/main.go index 006c41b2..ea825d81 100644 --- a/cmd/playground/main.go +++ b/cmd/playground/main.go @@ -24,13 +24,15 @@ import ( var Version = "testing" type appArgs struct { - packagesFile string - playgroundUrl string - addr string - debug bool - buildDir string - cleanupInterval string - assetsDirectory string + packagesFile string + playgroundURL string + goTipPlaygroundURL string + addr string + debug bool + buildDir string + cleanupInterval string + assetsDirectory string + connectTimeout time.Duration } func (a appArgs) getCleanDuration() (time.Duration, error) { @@ -49,9 +51,11 @@ func main() { flag.StringVar(&args.addr, "addr", ":8080", "TCP Listen address") flag.StringVar(&args.buildDir, "wasm-build-dir", os.TempDir(), "Directory for WASM builds") flag.StringVar(&args.cleanupInterval, "clean-interval", "10m", "Build directory cleanup interval") - flag.StringVar(&args.playgroundUrl, "playground-url", goplay.DefaultPlaygroundURL, "Go Playground URL") + flag.StringVar(&args.playgroundURL, "playground-url", goplay.DefaultPlaygroundURL, "Go Playground URL") + flag.StringVar(&args.goTipPlaygroundURL, "gotip-url", goplay.DefaultGoTipPlaygroundURL, "GoTip Playground URL") flag.BoolVar(&args.debug, "debug", false, "Enable debug mode") flag.StringVar(&args.assetsDirectory, "static-dir", filepath.Join(wd, "public"), "Path to web page assets (HTML, JS, etc)") + flag.DurationVar(&args.connectTimeout, "timeout", 15*time.Second, "Go Playground server connect timeout") l := getLogger(args.debug) defer l.Sync() //nolint:errcheck @@ -92,7 +96,7 @@ func start(goRoot string, args appArgs) error { zap.S().Info("Server version: ", Version) zap.S().Infof("GOROOT is %q", goRoot) - zap.S().Infof("Playground url: %q", args.playgroundUrl) + zap.S().Infof("Playground url: %q", args.playgroundURL) zap.S().Infof("Packages file is %q", args.packagesFile) zap.S().Infof("Cleanup interval is %s", cleanInterval.String()) zap.S().Infof("Serving web page from %q", args.assetsDirectory) @@ -112,10 +116,14 @@ func start(goRoot string, args appArgs) error { go store.StartCleaner(ctx, cleanInterval, nil) r := mux.NewRouter() - pg := goplay.NewClient(args.playgroundUrl, goplay.DefaultUserAgent, 15*time.Second) - + pgClient := goplay.NewClient(args.playgroundURL, goplay.DefaultUserAgent, args.connectTimeout) + goTipClient := goplay.NewClient(args.goTipPlaygroundURL, goplay.DefaultUserAgent, args.connectTimeout) + clients := &langserver.PlaygroundServices{ + Default: pgClient, + GoTip: goTipClient, + } // API routes - langserver.New(Version, pg, packages, compiler.NewBuildService(zap.S(), store)). + langserver.New(Version, clients, packages, compiler.NewBuildService(zap.S(), store)). Mount(r.PathPrefix("/api").Subrouter()) // Web UI routes diff --git a/pkg/goplay/client.go b/pkg/goplay/client.go index 0ee5c00a..f65b4d7c 100644 --- a/pkg/goplay/client.go +++ b/pkg/goplay/client.go @@ -13,8 +13,9 @@ import ( ) const ( - DefaultUserAgent = "goplay.tools/1.0 (http://goplay.tools/)" - DefaultPlaygroundURL = "https://play.golang.org" + DefaultUserAgent = "goplay.tools/1.0 (http://goplay.tools/)" + DefaultPlaygroundURL = "https://play.golang.org" + DefaultGoTipPlaygroundURL = "https://gotipplay.golang.org" // maxSnippetSize value taken from // https://github.com/golang/playground/blob/master/app/goplay/share.go diff --git a/pkg/langserver/server.go b/pkg/langserver/server.go index efa62dff..e4a0a4c2 100644 --- a/pkg/langserver/server.go +++ b/pkg/langserver/server.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "strconv" + "strings" "time" "github.com/gorilla/mux" @@ -23,30 +24,37 @@ const ( frameTime = time.Second maxBuildTimeDuration = time.Second * 30 - wasmMimeType = "application/wasm" - formatQueryParam = "format" - artifactParamVal = "artifactId" + wasmMimeType = "application/wasm" + formatQueryParam = "format" + artifactParamVal = "artifactId" + playgroundBackendParam = "backend" + playgroundGoTip = "gotip" ) +type PlaygroundServices struct { + Default *goplay.Client + GoTip *goplay.Client +} + // Service is language server service type Service struct { - version string - log *zap.SugaredLogger - index analyzer.PackageIndex - compiler compiler.BuildService - playground *goplay.Client - limiter *rate.Limiter + version string + log *zap.SugaredLogger + index analyzer.PackageIndex + compiler compiler.BuildService + playgrounds *PlaygroundServices + limiter *rate.Limiter } // New is Service constructor -func New(version string, playground *goplay.Client, packages []*analyzer.Package, builder compiler.BuildService) *Service { +func New(version string, playgrounds *PlaygroundServices, packages []*analyzer.Package, builder compiler.BuildService) *Service { return &Service{ - compiler: builder, - version: version, - playground: playground, - log: zap.S().Named("langserver"), - index: analyzer.BuildPackageIndex(packages), - limiter: rate.NewLimiter(rate.Every(frameTime), compileRequestsPerFrame), + compiler: builder, + version: version, + playgrounds: playgrounds, + log: zap.S().Named("langserver"), + index: analyzer.BuildPackageIndex(packages), + limiter: rate.NewLimiter(rate.Every(frameTime), compileRequestsPerFrame), } } @@ -122,7 +130,7 @@ func (s *Service) provideSuggestion(req SuggestionRequest) (*SuggestionsResponse } // HandleGetVersion handles /api/version -func (s *Service) HandleGetVersion(w http.ResponseWriter, r *http.Request) error { +func (s *Service) HandleGetVersion(w http.ResponseWriter, _ *http.Request) error { WriteJSON(w, VersionResponse{Version: s.version}) return nil } @@ -149,7 +157,7 @@ func (s *Service) HandleFormatCode(w http.ResponseWriter, r *http.Request) error return err } - formatted, _, err := s.goImportsCode(r.Context(), src) + formatted, _, err := s.goImportsCode(r, src) if err != nil { if goplay.IsCompileError(err) { return NewHTTPError(http.StatusBadRequest, err) @@ -165,7 +173,8 @@ func (s *Service) HandleFormatCode(w http.ResponseWriter, r *http.Request) error // HandleShare handles snippet share func (s *Service) HandleShare(w http.ResponseWriter, r *http.Request) error { - shareID, err := s.playground.Share(r.Context(), r.Body) + client := s.getPlaygroundClientFromRequest(r) + shareID, err := client.Share(r.Context(), r.Body) defer r.Body.Close() if err != nil { if err == goplay.ErrSnippetTooLarge { @@ -184,7 +193,8 @@ func (s *Service) HandleShare(w http.ResponseWriter, r *http.Request) error { func (s *Service) HandleGetSnippet(w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) snippetID := vars["id"] - snippet, err := s.playground.GetSnippet(r.Context(), snippetID) + client := s.getPlaygroundClientFromRequest(r) + snippet, err := client.GetSnippet(r.Context(), snippetID) if err != nil { if err == goplay.ErrSnippetNotFound { return Errorf(http.StatusNotFound, "snippet %q not found", snippetID) @@ -218,7 +228,7 @@ func (s *Service) HandleRunCode(w http.ResponseWriter, r *http.Request) error { var changed bool if shouldFormat { - src, changed, err = s.goImportsCode(r.Context(), src) + src, changed, err = s.goImportsCode(r, src) if err != nil { if goplay.IsCompileError(err) { return NewHTTPError(http.StatusBadRequest, err) @@ -228,7 +238,8 @@ func (s *Service) HandleRunCode(w http.ResponseWriter, r *http.Request) error { } } - res, err := s.playground.Compile(r.Context(), src) + client := s.getPlaygroundClientFromRequest(r) + res, err := client.Compile(r.Context(), src) if err != nil { return err } @@ -276,6 +287,16 @@ func (s *Service) HandleArtifactRequest(w http.ResponseWriter, r *http.Request) return nil } +func (s *Service) getPlaygroundClientFromRequest(r *http.Request) *goplay.Client { + playgroundBackend := strings.TrimSpace(r.URL.Query().Get(playgroundBackendParam)) + if playgroundBackend == playgroundGoTip { + s.log.Debugw("Using goTip backend for request", zap.String("url", r.RequestURI)) + return s.playgrounds.GoTip + } + + return s.playgrounds.Default +} + // HandleCompile handles WASM build request func (s *Service) HandleCompile(w http.ResponseWriter, r *http.Request) error { // Limit for request timeout @@ -299,7 +320,7 @@ func (s *Service) HandleCompile(w http.ResponseWriter, r *http.Request) error { var changed bool if shouldFormat { - src, changed, err = s.goImportsCode(r.Context(), src) + src, changed, err = s.goImportsCode(r, src) if err != nil { if goplay.IsCompileError(err) { return NewHTTPError(http.StatusBadRequest, err) @@ -333,8 +354,9 @@ func (s *Service) HandleCompile(w http.ResponseWriter, r *http.Request) error { // if any error occurs, it sends error response to client and closes connection // // if "format" url query param is undefined or set to "false", just returns code as is -func (s *Service) goImportsCode(ctx context.Context, src []byte) ([]byte, bool, error) { - resp, err := s.playground.GoImports(ctx, src) +func (s *Service) goImportsCode(r *http.Request, src []byte) ([]byte, bool, error) { + client := s.getPlaygroundClientFromRequest(r) + resp, err := client.GoImports(r.Context(), src) if err != nil { if err == goplay.ErrSnippetTooLarge { return nil, false, NewHTTPError(http.StatusRequestEntityTooLarge, err) diff --git a/web/src/components/settings/SettingsModal.tsx b/web/src/components/settings/SettingsModal.tsx index b73efe1d..cee3825e 100644 --- a/web/src/components/settings/SettingsModal.tsx +++ b/web/src/components/settings/SettingsModal.tsx @@ -1,19 +1,33 @@ import React from 'react'; -import { Checkbox, Dropdown, getTheme, IconButton, IDropdownOption, Modal } from '@fluentui/react'; -import { Pivot, PivotItem } from '@fluentui/react/lib/Pivot'; -import { MessageBar, MessageBarType } from '@fluentui/react/lib/MessageBar'; -import { Link } from '@fluentui/react/lib/Link'; +import { + Checkbox, + Dropdown, + getTheme, + IconButton, + IDropdownOption, + Modal +} from '@fluentui/react'; +import {Pivot, PivotItem} from '@fluentui/react/lib/Pivot'; +import {MessageBar, MessageBarType} from '@fluentui/react/lib/MessageBar'; +import {Link} from '@fluentui/react/lib/Link'; -import { getContentStyles, getIconButtonStyles } from '~/styles/modal'; +import {getContentStyles, getIconButtonStyles} from '~/styles/modal'; import SettingsProperty from './SettingsProperty'; -import { MonacoSettings, RuntimeType } from '~/services/config'; -import { DEFAULT_FONT, getAvailableFonts } from '~/services/fonts'; -import { BuildParamsArgs, Connect, MonacoParamsChanges, SettingsState } from '~/store'; +import {MonacoSettings, RuntimeType} from '~/services/config'; +import {DEFAULT_FONT, getAvailableFonts} from '~/services/fonts'; +import { + BuildParamsArgs, + Connect, + MonacoParamsChanges, + SettingsState +} from '~/store'; const WASM_SUPPORTED = 'WebAssembly' in window; const COMPILER_OPTIONS: IDropdownOption[] = [ { key: RuntimeType.GoPlayground, text: 'Go Playground' }, + { + key: RuntimeType.GoTipPlayground, text: 'Go Playground (Go Tip)' }, { key: RuntimeType.WebAssembly, text: `WebAssembly (${WASM_SUPPORTED ? 'Experimental' : 'Unsupported'})`, @@ -62,6 +76,7 @@ export interface SettingsProps { interface SettingsModalState { isOpen?: boolean, showWarning?: boolean + showGoTipMessage?: boolean } @Connect(state => ({ @@ -77,7 +92,8 @@ export default class SettingsModal extends React.Component} /> -
- - WebAssembly is a modern runtime that gives you additional features - like possibility to interact with web browser but is unstable. - Use it at your own risk. -

- Seedocumentation for more details. -

-
+
+ { showWarning && ( + + WebAssembly is a modern runtime that gives you additional features + like possibility to interact with web browser but is unstable. + Use it at your own risk. +

+ Seedocumentation for more details. +

+
+ )} + { showGoTipMessage && ( + + Gotip Playground uses the current unstable development build of Go. +

+ Seegotip help for more details. +

+
+ )}
{ - return this.post(`/run?format=${Boolean(format)}`, code); + async evaluateCode(code: string, format: boolean, backend = PlaygroundBackend.Default): Promise { + return this.post(`/run?format=${Boolean(format)}&backend=${backend}`, code); } - async formatCode(code: string): Promise { - return this.post('/format', code); + async formatCode(code: string, backend = PlaygroundBackend.Default): Promise { + return this.post(`/format?backend=${backend}`, code); } async getSnippet(id: string): Promise { diff --git a/web/src/services/config.ts b/web/src/services/config.ts index 6bd6e25a..3a4950e3 100644 --- a/web/src/services/config.ts +++ b/web/src/services/config.ts @@ -9,8 +9,9 @@ const MONACO_SETTINGS = 'ms.monaco.settings'; export enum RuntimeType { - GoPlayground = 'GO_PLAYGROUND', - WebAssembly = 'WASM' + GoPlayground = 'GO_PLAYGROUND', + GoTipPlayground = 'GO_TIP_PLAYGROUND', + WebAssembly = 'WASM' } export interface MonacoSettings { diff --git a/web/src/store/dispatch.ts b/web/src/store/dispatch.ts index a6b19b4f..36dce10b 100644 --- a/web/src/store/dispatch.ts +++ b/web/src/store/dispatch.ts @@ -1,21 +1,28 @@ -import { saveAs } from 'file-saver'; -import { push } from 'connected-react-router'; +import {saveAs} from 'file-saver'; +import {push} from 'connected-react-router'; import { Action, - ActionType, MonacoParamsChanges, newBuildParamsChangeAction, + ActionType, + MonacoParamsChanges, + newBuildParamsChangeAction, newBuildResultAction, newErrorAction, newImportFileAction, - newLoadingAction, newMonacoParamsChangeAction, + newLoadingAction, + newMonacoParamsChangeAction, newProgramWriteAction, newToggleThemeAction, newUIStateChangeAction } from './actions'; -import client, { EvalEventKind, instantiateStreaming } from '~/services/api'; -import config, { RuntimeType } from '~/services/config'; -import { DEMO_CODE } from '~/components/editor/props'; -import { getImportObject, goRun } from '~/services/go'; -import { State } from './state'; +import client, { + EvalEventKind, + instantiateStreaming, + PlaygroundBackend +} from '~/services/api'; +import config, {RuntimeType} from '~/services/config'; +import {DEMO_CODE} from '~/components/editor/props'; +import {getImportObject, goRun} from '~/services/go'; +import {State} from './state'; export type StateProvider = () => State export type DispatchFn = (a: Action | any) => any @@ -120,6 +127,10 @@ export const runFileDispatcher: Dispatcher = const res = await client.evaluateCode(editor.code, settings.autoFormat); dispatch(newBuildResultAction(res)); break; + case RuntimeType.GoTipPlayground: + const rsp = await client.evaluateCode(editor.code, settings.autoFormat, PlaygroundBackend.GoTip); + dispatch(newBuildResultAction(rsp)); + break; case RuntimeType.WebAssembly: let resp = await client.build(editor.code, settings.autoFormat); let wasmFile = await client.getArtifact(resp.fileName); @@ -143,8 +154,11 @@ export const formatFileDispatcher: Dispatcher = async (dispatch: DispatchFn, getState: StateProvider) => { dispatch(newLoadingAction()); try { - const { code } = getState().editor; - const res = await client.formatCode(code); + // Format code using GoTip is enabled to support + // any syntax changes from unstable Go specs. + const { editor: {code}, settings: { runtime } } = getState(); + const backend = runtime === RuntimeType.GoTipPlayground ? PlaygroundBackend.GoTip : PlaygroundBackend.Default; + const res = await client.formatCode(code, backend); if (res.formatted) { dispatch(newBuildResultAction(res));