├── www ├── src │ ├── themes │ │ ├── interface.ts │ │ ├── index.ts │ │ ├── Dark.ts │ │ └── Light.ts │ ├── Examples │ │ ├── index.ts │ │ └── ExamplesManager.ts │ ├── Repositories │ │ ├── index.ts │ │ ├── TextCodeRepository.ts │ │ ├── Base64CodeRepository.ts │ │ ├── interface.ts │ │ ├── GithubGistCodeRepository.ts │ │ ├── LocalCodeRepository.ts │ │ ├── SharedCodeRepository.ts │ │ └── CodeRepositoryManager.ts │ ├── PlaygroundConfig.ts │ ├── TipsManager.ts │ ├── clipboard_util.ts │ ├── main.ts │ ├── QueryParams.ts │ ├── HelpManager.ts │ ├── icons.ts │ ├── Editor │ │ └── Editor.ts │ ├── CodeRunner │ │ └── CodeRunner.ts │ ├── ThemeManager │ │ └── ThemeManager.ts │ ├── Terminal │ │ └── Terminal.ts │ ├── v-hint.ts │ ├── RunConfigurationManager │ │ └── RunConfigurationManager.ts │ └── v.ts ├── public │ ├── favicon.ico │ ├── fonts │ │ ├── FiraCode-VF.woff2 │ │ ├── FiraSans-Two.woff2 │ │ ├── FiraCode-Bold.woff2 │ │ ├── FiraCode-Light.woff2 │ │ ├── FiraCode-Medium.woff2 │ │ ├── FiraSans-Bold.woff2 │ │ ├── FiraSans-Book.woff2 │ │ ├── FiraSans-Eight.woff2 │ │ ├── FiraSans-Four.woff2 │ │ ├── FiraSans-Hair.woff2 │ │ ├── FiraSans-Heavy.woff2 │ │ ├── FiraSans-Light.woff2 │ │ ├── FiraSans-Medium.woff2 │ │ ├── FiraSans-Thin.woff2 │ │ ├── FiraSans-Ultra.woff2 │ │ ├── FiraCode-Regular.woff2 │ │ ├── FiraCode-SemiBold.woff2 │ │ ├── FiraSans-Regular.woff2 │ │ ├── FiraSans-SemiBold.woff2 │ │ ├── FiraSans-ExtraBold.woff2 │ │ ├── FiraSans-ExtraLight.woff2 │ │ └── FiraSans-UltraLight.woff2 │ ├── images │ │ ├── apple-touch-icon.png │ │ ├── open-graph │ │ │ └── play-cover.png │ │ ├── type.svg │ │ ├── type-light.svg │ │ ├── module.svg │ │ ├── module-light.svg │ │ └── logo.svg │ └── scripts │ │ ├── header.js │ │ └── header-search.js ├── styles │ ├── index.scss │ ├── scrollbar.scss │ ├── close-button.scss │ ├── shortcut.scss │ ├── fonts.scss │ ├── tips.scss │ ├── editor-themes │ │ ├── light.css │ │ └── dark.css │ ├── playground_select_examples.scss │ ├── playground_help.scss │ ├── tools.scss │ ├── colors.scss │ ├── index.css.map │ └── playground.scss ├── tsconfig.json ├── package.json ├── esbuild.config.js └── esbuild.watch.config.js ├── docs └── images │ └── cover.png ├── .editorconfig ├── server ├── v.mod ├── index_endpoint.v ├── runners │ ├── info.v │ ├── format.v │ ├── cgen.v │ ├── constants.v │ └── runner.v ├── logger │ └── logger.v ├── analytics │ ├── models.v │ └── analytics.v ├── models │ └── CodeStorage.v ├── version_endpoint.v ├── run_test_endpoint.v ├── format_endpoint.v ├── run_endpoint.v ├── cgen_endpoint.v ├── shared_code_endpoint.v ├── share_endpoint.v ├── check_output_endpoint.v ├── isolate │ └── isolate.v ├── create_bug_endpoint.v ├── server.v └── README.md ├── .gitignore ├── docker-compose.yml ├── LICENSE ├── Dockerfile ├── package.json ├── .devcontainer ├── devcontainer.json ├── docker-compose.yml ├── Dockerfile └── common-debian.sh └── README.md /www/src/themes/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITheme { 2 | name(): string 3 | } 4 | -------------------------------------------------------------------------------- /www/src/Examples/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./examples"; 2 | export * from "./ExamplesManager"; -------------------------------------------------------------------------------- /docs/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/docs/images/cover.png -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/favicon.ico -------------------------------------------------------------------------------- /www/src/themes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./interface"; 2 | export * from "./Dark"; 3 | export * from "./Light"; 4 | -------------------------------------------------------------------------------- /www/public/fonts/FiraCode-VF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraCode-VF.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Two.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Two.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraCode-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraCode-Bold.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraCode-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraCode-Light.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraCode-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraCode-Medium.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Bold.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Book.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Eight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Eight.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Four.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Four.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Hair.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Hair.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Heavy.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Heavy.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Light.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Medium.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Thin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Thin.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Ultra.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Ultra.woff2 -------------------------------------------------------------------------------- /www/public/images/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/images/apple-touch-icon.png -------------------------------------------------------------------------------- /www/public/fonts/FiraCode-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraCode-Regular.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraCode-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraCode-SemiBold.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-Regular.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-SemiBold.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-ExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-ExtraBold.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-ExtraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-ExtraLight.woff2 -------------------------------------------------------------------------------- /www/public/fonts/FiraSans-UltraLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/fonts/FiraSans-UltraLight.woff2 -------------------------------------------------------------------------------- /www/public/images/open-graph/play-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlang/playground/HEAD/www/public/images/open-graph/play-cover.png -------------------------------------------------------------------------------- /www/src/themes/Dark.ts: -------------------------------------------------------------------------------- 1 | import { ITheme } from "./interface"; 2 | 3 | export class Dark implements ITheme { 4 | name(): string { 5 | return "dark" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /www/src/themes/Light.ts: -------------------------------------------------------------------------------- 1 | import { ITheme } from "./interface"; 2 | 3 | export class Light implements ITheme { 4 | name(): string { 5 | return "light" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.v] 8 | indent_style = tab 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /server/v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'V Playground Server' 3 | version: '0.3.0' 4 | description: 'V Playground Server' 5 | license: 'MIT' 6 | dependencies: [ 7 | 'srackham.pcre2' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /www/src/Repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./interface"; 2 | export * from "./CodeRepositoryManager"; 3 | export * from "./LocalCodeRepository"; 4 | export * from "./SharedCodeRepository"; 5 | export * from "./TextCodeRepository"; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | server/bin 5 | logs 6 | node_modules 7 | 8 | playground 9 | code_storage.db 10 | 11 | /code.v 12 | /code_test.v 13 | /code 14 | /code.dSYM 15 | .vmodules/cache/README.md 16 | server/server 17 | -------------------------------------------------------------------------------- /www/src/PlaygroundConfig.ts: -------------------------------------------------------------------------------- 1 | import { ITheme } from "./themes"; 2 | 3 | /** 4 | * TODO: For future customizations. 5 | */ 6 | export interface PlaygroundConfig { 7 | embed: boolean 8 | theme: ITheme 9 | codeHash: string 10 | code: string 11 | } 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | playground: 5 | command: bash -c "cd /usr/src/app && make run" 6 | privileged: true 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | ports: 11 | - "5555:5555" 12 | volumes: 13 | - .:/usr/src/app 14 | -------------------------------------------------------------------------------- /server/index_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import os 4 | import vweb 5 | import analytics 6 | 7 | @['/'; get] 8 | fn (mut app Server) index() vweb.Result { 9 | analytics.send_analytics(app.req, 'https://play.vlang.io/') 10 | file := os.read_file('./www/public/index.html') or { panic(err) } 11 | return app.html(file) 12 | } 13 | -------------------------------------------------------------------------------- /www/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import "fonts"; 2 | @import "colors"; 3 | @import "header"; 4 | @import "tools"; 5 | @import "tips"; 6 | @import "scrollbar"; 7 | @import "shortcut"; 8 | @import "editor-themes/light"; 9 | @import "editor-themes/dark"; 10 | @import "playground_select_examples"; 11 | @import "playground_help"; 12 | @import "playground"; 13 | -------------------------------------------------------------------------------- /www/public/images/type.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES2020", 5 | "moduleResolution": "Node", 6 | "lib": [ 7 | "dom", 8 | "esnext", 9 | ], 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "baseUrl": "." 13 | }, 14 | "files": ["src/main.ts"], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /www/public/images/type-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /server/runners/info.v: -------------------------------------------------------------------------------- 1 | module runners 2 | 3 | import v.util.version 4 | import os 5 | 6 | pub fn get_version() string { 7 | return version.full_v_version(true) 8 | } 9 | 10 | pub fn get_doctor_output() !string { 11 | res := os.execute('v doctor') 12 | if res.exit_code != 0 { 13 | return error('v doctor failed, output: ${res.output}') 14 | } 15 | return res.output.all_before_last('\n') 16 | } 17 | -------------------------------------------------------------------------------- /www/src/Repositories/TextCodeRepository.ts: -------------------------------------------------------------------------------- 1 | import {CodeRepository, CodeSnippet} from "./interface"; 2 | 3 | export class TextCodeRepository implements CodeRepository { 4 | constructor(private text: string) { 5 | } 6 | 7 | saveCode(_: string): void { 8 | } 9 | 10 | getCode(onReady: (snippet: CodeSnippet) => void): void { 11 | onReady({code: this.text}) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/logger/logger.v: -------------------------------------------------------------------------------- 1 | module logger 2 | 3 | import time 4 | import os 5 | 6 | pub fn log(code string, build_res string) ! { 7 | now := time.now() 8 | now_formatted := now.custom_format('MMMM Mo YY N kk:mm:ss A') 9 | log_dir := 'logs/${now.year}-${now.month:02d}' 10 | os.mkdir_all(log_dir)! 11 | 12 | log_file := '${log_dir}/${now_formatted}' 13 | log_content := '${code}\n\n\n${build_res}' 14 | os.write_file(log_file, log_content)! 15 | } 16 | -------------------------------------------------------------------------------- /www/styles/scrollbar.scss: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 8px; 3 | height: 8px; 4 | } 5 | 6 | ::-webkit-scrollbar-thumb { 7 | background: var(--scrollbar-thumb-color); 8 | border-radius: 5px; 9 | } 10 | 11 | ::-webkit-scrollbar-track { 12 | border-radius: 5px; 13 | margin: 5px; 14 | } 15 | 16 | ::-webkit-scrollbar-corner { 17 | background-color: transparent; 18 | } 19 | 20 | ::-webkit-scrollbar-track-piece { 21 | background-color: transparent; 22 | } 23 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v-playground/frontend", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "node esbuild.config.js", 6 | "watch": "node esbuild.watch.config.js" 7 | }, 8 | "dependencies": { 9 | "@codemirror/language": "^6.2.1", 10 | "esbuild": "^0.16.17" 11 | }, 12 | "devDependencies": { 13 | "@types/codemirror": "^5.60.5", 14 | "@types/estree": "^1.0.0", 15 | "@types/tern": "^0.23.4", 16 | "typescript": "~4.9.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/analytics/models.v: -------------------------------------------------------------------------------- 1 | module analytics 2 | 3 | pub struct AnalyticsEvent { 4 | pub: 5 | id int @[omitempty; primary] 6 | url string // event page url 7 | event_kind int // actually EventKind 8 | site_id int // actually SiteId 9 | pub mut: 10 | user_agent string // user agent of the user 11 | accept_language string // accept language of the user 12 | referrer string // referrer url 13 | created_at i64 @[omitenpty] // utc timestamp 14 | } 15 | -------------------------------------------------------------------------------- /server/models/CodeStorage.v: -------------------------------------------------------------------------------- 1 | module models 2 | 3 | @[table: 'code_storage'] 4 | pub struct CodeStorage { 5 | pub: 6 | id int @[primary; sql: serial] 7 | code string 8 | hash string 9 | build_arguments string @[json: 'buildArguments'] // passed when building binary 10 | run_arguments string @[json: 'runArguments'] // passed when run binary 11 | run_configuration int @[json: 'runConfiguration'] // how to run code 12 | additional string // any future additional data 13 | } 14 | -------------------------------------------------------------------------------- /www/src/Repositories/Base64CodeRepository.ts: -------------------------------------------------------------------------------- 1 | import {CodeRepository, CodeSnippet} from "./interface"; 2 | 3 | export class Base64CodeRepository implements CodeRepository { 4 | public static readonly QUERY_PARAM_NAME = "base64" 5 | 6 | private readonly decodedCode 7 | 8 | constructor(private text: string) { 9 | this.decodedCode = atob(text) 10 | } 11 | 12 | saveCode(_: string): void { 13 | } 14 | 15 | getCode(onReady: (snippet: CodeSnippet) => void): void { 16 | onReady({code: this.decodedCode}) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/version_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import os 5 | 6 | struct VersionResponse { 7 | version string 8 | error string 9 | } 10 | 11 | // version endpoint is used to get the version of the V. 12 | // Returns VersionResponse with version or error. 13 | @['/version'; post] 14 | fn (mut app Server) version() vweb.Result { 15 | res := os.execute('v -version') 16 | if res.exit_code != 0 { 17 | return app.json(VersionResponse{ 18 | error: res.output 19 | }) 20 | } 21 | 22 | return app.json(VersionResponse{ 23 | version: res.output 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /www/styles/close-button.scss: -------------------------------------------------------------------------------- 1 | .close-button { 2 | width: 35px; 3 | height: 35px; 4 | 5 | padding: 5px; 6 | border-radius: 8px; 7 | 8 | display: grid; 9 | align-items: center; 10 | justify-items: center; 11 | 12 | cursor: pointer; 13 | 14 | &:hover { 15 | background-color: var(--button-hover-background-color); 16 | } 17 | 18 | &:hover:active { 19 | background-color: var(--button-hover-background-color); 20 | } 21 | 22 | svg { 23 | width: 100%; 24 | height: 100%; 25 | 26 | .icon-stroke { 27 | stroke: var(--text-color); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/analytics/analytics.v: -------------------------------------------------------------------------------- 1 | module analytics 2 | 3 | import net.http 4 | import json 5 | 6 | pub fn send_analytics(req http.Request, url string) { 7 | user_agent := req.header.get(.user_agent) or { '' } 8 | accept_language := req.header.get(.accept_language) or { '' } 9 | referrer := req.header.get(.referer) or { '' } 10 | 11 | event := AnalyticsEvent{ 12 | url: url 13 | event_kind: 0 14 | site_id: 0 15 | user_agent: user_agent 16 | accept_language: accept_language 17 | referrer: referrer 18 | } 19 | 20 | data := json.encode(event) 21 | 22 | http.post('http://localhost:8100/a', data) or {} 23 | } 24 | -------------------------------------------------------------------------------- /server/run_test_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import runners 5 | 6 | type RunTestResponse = RunResponse 7 | 8 | // run_test endpoint is used to run code as tests in sandbox. 9 | // Returns RunResponse with result output or error. 10 | @['/run_test'; post] 11 | fn (mut app Server) run_test() vweb.Result { 12 | snippet := app.get_request_code() or { return app.json(RunTestResponse{ 13 | error: err.msg() 14 | }) } 15 | 16 | res := runners.test(snippet) or { return app.json(RunTestResponse{ 17 | error: err.msg() 18 | }) } 19 | 20 | return app.json(RunTestResponse{ 21 | output: res.output 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /server/format_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import runners 5 | 6 | struct FormatResponse { 7 | output string 8 | error string 9 | } 10 | 11 | // format endpoint is used to format code in sandbox. 12 | // Returns FormatResponse with result output or error. 13 | @['/format'; post] 14 | fn (mut app Server) format() vweb.Result { 15 | code := app.form['code'] or { 16 | return app.json(FormatResponse{ 17 | error: 'No code was provided.' 18 | }) 19 | } 20 | 21 | res := runners.format_code(code) or { 22 | return app.json(FormatResponse{ 23 | error: 'Failed to format code:\n${err}' 24 | }) 25 | } 26 | 27 | return app.json(FormatResponse{ 28 | output: res 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /server/run_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import runners 5 | 6 | struct RunResponse { 7 | output string 8 | build_output string @[json: 'buildOutput'] 9 | error string 10 | } 11 | 12 | // run endpoint is used to run code in sandbox. 13 | // Returns RunResponse with result output or error. 14 | @['/run'; post] 15 | fn (mut app Server) run() vweb.Result { 16 | snippet := app.get_request_code() or { return app.json(RunResponse{ 17 | error: err.msg() 18 | }) } 19 | 20 | res := runners.run(snippet) or { return app.json(RunResponse{ 21 | error: err.msg() 22 | }) } 23 | 24 | return app.json(RunResponse{ 25 | output: res.output 26 | build_output: res.build_output 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /server/cgen_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import runners 5 | 6 | struct CgenResponse { 7 | cgen_code string @[json: 'cgenCode'] 8 | exit_code int @[json: 'exitCode'] 9 | build_output string @[json: 'buildOutput'] 10 | error string 11 | } 12 | 13 | // cgen endpoint is used to retrieve cgen code for a given V code. 14 | // Returns CgenResponse with generated C code or error. 15 | @['/cgen'; post] 16 | fn (mut app Server) cgen() vweb.Result { 17 | snippet := app.get_request_code() or { return app.json(CgenResponse{ 18 | error: err.msg() 19 | }) } 20 | res, exit_code, build_output := runners.retrieve_cgen_code(snippet) or { 21 | return app.json(CgenResponse{ 22 | error: err.msg() 23 | }) 24 | } 25 | return app.json(CgenResponse{ 26 | cgen_code: res 27 | exit_code: exit_code 28 | build_output: build_output 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /www/public/images/module.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /www/public/images/module-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /server/shared_code_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import models 5 | 6 | @['/p/:hash'; get] 7 | fn (mut app Server) shared_code(hash string) vweb.Result { 8 | if hash == '' { 9 | return app.index() 10 | } 11 | return app.redirect('/?query=${hash}') 12 | } 13 | 14 | struct GetByHashResponse { 15 | snippet models.CodeStorage 16 | found bool 17 | error string 18 | } 19 | 20 | // get_by_hash endpoint is used to retrieve code snippet by hash. 21 | // Returns GetByHashResponse with snippet, found and error fields. 22 | @['/query'; post] 23 | fn (mut app Server) get_by_hash() vweb.Result { 24 | hash := app.form['hash'] or { 25 | return app.json(GetByHashResponse{ 26 | error: 'No hash was provided.' 27 | }) 28 | } 29 | snippet := app.get_saved_code(hash.trim_space()) or { 30 | return app.json(GetByHashResponse{ 31 | found: false 32 | }) 33 | } 34 | 35 | return app.json(GetByHashResponse{ 36 | snippet: snippet 37 | found: true 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /www/src/Repositories/interface.ts: -------------------------------------------------------------------------------- 1 | import {SharedCodeRunConfiguration} from "./SharedCodeRepository"; 2 | 3 | /** 4 | * CodeRepository describes the interface for all code sources. 5 | * 6 | * Describe how to save and load code. 7 | */ 8 | export interface CodeRepository { 9 | /** 10 | * Saves the code to the storage. 11 | * @param code The code to save. 12 | */ 13 | saveCode(code: string): void 14 | 15 | /** 16 | * Async gets the code from the storage. 17 | * @param onReady Callback function that will be called when the code is ready. 18 | */ 19 | getCode(onReady: (snippet: CodeSnippet) => void): void 20 | } 21 | 22 | /** 23 | * CodeSnippet describes the interface for all code snippets. 24 | */ 25 | export interface CodeSnippet { 26 | id?: number 27 | code: string 28 | hash?: string 29 | buildArguments?: string 30 | runArguments?: string 31 | runConfiguration?: SharedCodeRunConfiguration 32 | additional?: {} 33 | } 34 | -------------------------------------------------------------------------------- /www/public/scripts/header.js: -------------------------------------------------------------------------------- 1 | const mobileHeader = document.querySelector(".mobile-header-aside"); 2 | const closeButton = mobileHeader.querySelector(".js-close-header-aside-button"); 3 | const overlay = document.querySelector(".js-header-mobile-overlay"); 4 | const openButton = document.querySelector(".js-open-mobile-menu"); 5 | 6 | const closeMobileHeader = () => { 7 | mobileHeader.classList.remove("open"); 8 | } 9 | 10 | const openMobileHeader = () => { 11 | mobileHeader.classList.add("open"); 12 | } 13 | 14 | openButton.addEventListener("click", () => { 15 | openMobileHeader(); 16 | }) 17 | 18 | closeButton.addEventListener("click", () => { 19 | closeMobileHeader(); 20 | }); 21 | 22 | overlay.addEventListener("click", () => { 23 | closeMobileHeader(); 24 | }); 25 | 26 | const openMenuButtons = document.querySelectorAll(".js-open-menu"); 27 | openMenuButtons.forEach(button => { 28 | const menuElement = button.nextElementSibling 29 | button.addEventListener("click", () => { 30 | menuElement.classList.toggle("open"); 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /server/share_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import models 5 | import crypto.md5 6 | 7 | struct ShareResponse { 8 | hash string 9 | error string 10 | } 11 | 12 | // share endpoint is used to share code snippets. 13 | // Returns ShareResponse with the hash of the code snippet. 14 | @['/share'; post] 15 | fn (mut app Server) share() vweb.Result { 16 | snippet := app.get_request_code() or { 17 | return app.json(ShareResponse{ 18 | hash: '' 19 | error: err.msg() 20 | }) 21 | } 22 | 23 | hash := hash_code_snippet(snippet) 24 | 25 | app.add_new_code(models.CodeStorage{ 26 | ...snippet 27 | hash: hash 28 | }) 29 | 30 | return app.json(ShareResponse{ 31 | hash: hash 32 | }) 33 | } 34 | 35 | fn hash_code_snippet(snippet models.CodeStorage) string { 36 | // snippets with the same code but different arguments should be treated as different 37 | to_hash := snippet.code + snippet.build_arguments + snippet.run_arguments + 38 | snippet.run_configuration.str() 39 | // using 10 chars is enough for now 40 | return md5.hexhash(to_hash)[0..10] 41 | } 42 | -------------------------------------------------------------------------------- /server/runners/format.v: -------------------------------------------------------------------------------- 1 | module runners 2 | 3 | import os 4 | import isolate 5 | 6 | pub fn format_code(code string) !string { 7 | box_path, box_id := isolate.init_sandbox() 8 | defer { 9 | isolate.cleanup_sandbox(box_id) 10 | } 11 | 12 | os.write_file(os.join_path(box_path, 'code.v'), code) or { 13 | return error('Failed to write code to sandbox.') 14 | } 15 | 16 | vfmt_res := isolate.execute(' 17 | isolate 18 | --box-id=${box_id} 19 | --dir=${@VEXEROOT} 20 | --env=HOME=/box 21 | --processes=3 22 | --mem=100000 23 | --wall-time=2 24 | --run 25 | -- 26 | ${@VEXEROOT}/v fmt code.v 27 | ') 28 | 29 | mut vfmt_output := $if local ? { 30 | vfmt_res.output 31 | } $else { 32 | vfmt_res.output.trim_right('\n') 33 | } 34 | if vfmt_res.exit_code != 0 { 35 | return error(prettify(vfmt_output)) 36 | } 37 | 38 | result := $if local ? { 39 | vfmt_output 40 | } $else { 41 | // isolate output message like "OK (0.033 sec real, 0.219 sec wall)" 42 | // so we need to remove it 43 | vfmt_output.all_before_last('\n') + '\n' 44 | } 45 | 46 | return result 47 | } 48 | -------------------------------------------------------------------------------- /www/styles/shortcut.scss: -------------------------------------------------------------------------------- 1 | .playground__help-tip { 2 | .shortcut { 3 | display: grid; 4 | grid-template-columns: auto max-content; 5 | grid-column-gap: 10px; 6 | align-content: center; 7 | margin-bottom: 15px; 8 | 9 | &.inline { 10 | display: inline; 11 | } 12 | 13 | .key { 14 | font-size: 14px; 15 | 16 | kbd { 17 | display: inline-block; 18 | font-family: "FiraSans", Helvetica, Arial, sans-serif; 19 | border: 1px solid #ccc; 20 | border-radius: 4px; 21 | padding: 0.05em 0.3em 0.1em; 22 | margin: 0 0.2em; 23 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px #fff inset; 24 | background-color: #f7f7f7; 25 | 26 | &:hover { 27 | cursor: pointer; 28 | user-select: none; 29 | -webkit-user-select: none; 30 | -moz-user-select: none; 31 | -ms-user-select: none; 32 | } 33 | 34 | &:active { 35 | box-shadow: 0 0 0 2px #fff inset; 36 | transform: translateY(1px); 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /www/esbuild.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const esbuild = require("esbuild"); 3 | 4 | function printResult(result) { 5 | Object.keys(result).forEach((fileName) => { 6 | // convert to kilobyte 7 | const fileSize = result[fileName].bytes / 1000; 8 | console.log(`${fileName} => ${fileSize} Kb`); 9 | }); 10 | } 11 | 12 | const styleResult = esbuild.buildSync({ 13 | bundle: true, 14 | minify: true, 15 | metafile: true, 16 | outfile: path.resolve(__dirname, "./public/style.css"), 17 | target: "esnext", 18 | entryPoints: ["./styles/index.css"], 19 | loader: { 20 | '.svg': 'base64', 21 | }, 22 | }); 23 | 24 | //printResult(styleResult?.metafile?.outputs || {}); 25 | 26 | const codeResult = esbuild.buildSync({ 27 | minify: true, 28 | bundle: true, 29 | keepNames: false, 30 | metafile: true, 31 | outfile: path.resolve(__dirname, "./public/main.bundle.js"), 32 | sourcemap: true, 33 | platform: "browser", 34 | target: "esnext", 35 | external: ["codemirror"], 36 | entryPoints: ["./src/main.ts"], 37 | }); 38 | 39 | //printResult(codeResult?.metafile?.outputs || {}); 40 | -------------------------------------------------------------------------------- /server/runners/cgen.v: -------------------------------------------------------------------------------- 1 | module runners 2 | 3 | import os 4 | import logger 5 | import isolate 6 | import models 7 | 8 | pub fn retrieve_cgen_code(snippet models.CodeStorage) !(string, int, string) { 9 | box_path, box_id := isolate.init_sandbox() 10 | defer { 11 | isolate.cleanup_sandbox(box_id) 12 | } 13 | 14 | os.write_file(os.join_path(box_path, 'code.v'), snippet.code) or { 15 | return error('Failed to write code to sandbox.') 16 | } 17 | 18 | build_res := isolate.execute(' 19 | ${@VEXEROOT}/v -showcc -keepc -cflags -DGC_MARKERS=1 -no-parallel -no-retry-compilation -skip-unused -g 20 | ${prepare_user_arguments(snippet.build_arguments)} 21 | ${box_path}/code.v 22 | ') 23 | build_output := build_res.output.trim_right('\n') 24 | 25 | logger.log(snippet.code, build_output) or { eprintln('[WARNING] Failed to log code.') } 26 | 27 | if build_res.exit_code != 0 { 28 | // skip handling of errors for now 29 | } 30 | 31 | path_to_cgen := $if macos { '/tmp/v_501/code.tmp.c' } $else { '/tmp/v_0/code.tmp.c' } 32 | cgen_file := os.read_file(path_to_cgen) or { return error('Failed to read generated C code.') } 33 | 34 | return cgen_file, build_res.exit_code, build_output 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024, V language contributors, V Open Source Community Association (VOSCA) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thevlang/vlang:buster-dev 2 | 3 | # options 4 | ARG DEV_IMG="false" 5 | 6 | # disable tzdata questions 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | 9 | # use bash 10 | SHELL ["/bin/bash", "-c"] 11 | 12 | # install apt-utils 13 | RUN apt-get update -y \ 14 | && apt-get install -y apt-utils 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) \ 15 | && apt-get clean && rm -rf /var/lib/apt/lists/* 16 | 17 | # essential tools 18 | RUN apt-get update -y && apt-get install -y --no-install-recommends \ 19 | ca-certificates \ 20 | netbase \ 21 | curl \ 22 | git \ 23 | make \ 24 | nodejs \ 25 | && apt-get clean && rm -rf /var/lib/apt/lists/* 26 | 27 | # install isolate deps 28 | RUN apt-get update -y && apt-get install -y --no-install-recommends \ 29 | libcap-dev \ 30 | libseccomp-dev \ 31 | libseccomp2 \ 32 | libcap2-bin \ 33 | asciidoc \ 34 | && apt-get clean && rm -rf /var/lib/apt/lists/* 35 | 36 | # install isolate 37 | RUN git clone https://github.com/ioi/isolate /tmp/isolate \ 38 | && cd /tmp/isolate \ 39 | && make isolate isolate-check-environment \ 40 | && make install \ 41 | && rm -rf /tmp/isolate 42 | -------------------------------------------------------------------------------- /server/runners/constants.v: -------------------------------------------------------------------------------- 1 | module runners 2 | 3 | // Non-standard block size, different for different filesystems. 4 | const block_size = 4096 5 | const fs_usage_max_size_in_bytes = 3 * 1024 * 1024 6 | 7 | // From isolate docs: 8 | // 9 | // Please note that this currently works only on the ext family of filesystems 10 | // (other filesystems use other interfaces for setting quotas). 11 | const block_max_count = u32(fs_usage_max_size_in_bytes / block_size) 12 | const inode_max_count = 50 13 | const max_run_processes_and_threads = 10 14 | const max_compiler_memory_in_kb = 100_000 15 | const max_run_memory_in_kb = 50_000 16 | const run_time_in_seconds = 2 17 | 18 | // From isolate docs: 19 | // 20 | // We recommend to use `--time` as the main limit, but set `--wall-time` to a much 21 | // higher value as a precaution against sleeping programs. 22 | const wall_time_in_seconds = 3 23 | 24 | fn prettify(output string) string { 25 | mut pretty := output 26 | if pretty.len > 10000 { 27 | pretty = pretty[..9997] + '...' 28 | } 29 | nlines := pretty.count('\n') 30 | if nlines > 100 { 31 | pretty = pretty.split_into_lines()[..100].join_lines() + '\n...and ${nlines - 100} more' 32 | } 33 | 34 | return pretty 35 | } 36 | -------------------------------------------------------------------------------- /server/check_output_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import runners 5 | import v.util.diff 6 | 7 | struct CheckOutputResponse { 8 | output string 9 | diff string 10 | is_equal bool 11 | error string 12 | } 13 | 14 | // check_output endpoint is used to check the output of the code. 15 | // Returns CheckOutputResponse with result output or error. 16 | @['/check_output'; post] 17 | fn (mut app Server) check_output() vweb.Result { 18 | snippet := app.get_request_code() or { 19 | return app.json(CheckOutputResponse{ 20 | error: err.msg() 21 | }) 22 | } 23 | expected_output := app.form['expected-output'] or { 24 | return app.json(CheckOutputResponse{ 25 | error: 'Expected output is required' 26 | }) 27 | }.replace('\r', '') 28 | 29 | res := runners.get_output(snippet) or { 30 | return app.json(CheckOutputResponse{ 31 | error: err.msg() 32 | }) 33 | } 34 | 35 | if expected_output == res { 36 | return app.json(CheckOutputResponse{ 37 | is_equal: true 38 | output: res 39 | }) 40 | } 41 | 42 | diff_res := diff.compare_text(expected_output, res) or { 43 | return app.json(CheckOutputResponse{ 44 | error: err.msg() 45 | }) 46 | } 47 | 48 | return app.json(CheckOutputResponse{ 49 | output: res 50 | diff: diff_res 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /www/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Fira+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap'); 2 | 3 | @font-face { 4 | font-family: "FiraSans"; 5 | src: url("https://play.vlang.io/fonts/FiraSans-ExtraBold.woff2"); 6 | font-weight: 800; 7 | font-display: swap; 8 | } 9 | 10 | @font-face { 11 | font-family: "FiraSans"; 12 | src: url("https://play.vlang.io/fonts/FiraSans-Bold.woff2"); 13 | font-weight: 700; 14 | font-display: swap; 15 | } 16 | 17 | @font-face { 18 | font-family: "FiraSans"; 19 | src: url("https://play.vlang.io/fonts/FiraSans-SemiBold.woff2"); 20 | font-weight: 600; 21 | font-display: swap; 22 | } 23 | 24 | @font-face { 25 | font-family: "FiraSans"; 26 | src: url("https://play.vlang.io/fonts/FiraSans-Medium.woff2"); 27 | font-weight: 500; 28 | font-display: swap; 29 | } 30 | 31 | @font-face { 32 | font-family: "FiraSans"; 33 | src: url("https://play.vlang.io/fonts/FiraSans-Regular.woff2"); 34 | font-weight: 400; 35 | font-display: swap; 36 | } 37 | 38 | @font-face { 39 | font-family: "FiraSans"; 40 | src: url("https://play.vlang.io/fonts/FiraSans-Book.woff2"); 41 | font-weight: 350; 42 | font-display: swap; 43 | } 44 | 45 | @font-face { 46 | font-family: "FiraSans"; 47 | src: url("https://play.vlang.io/fonts/FiraSans-Light.woff2"); 48 | font-weight: 300; 49 | font-display: swap; 50 | } 51 | -------------------------------------------------------------------------------- /www/src/TipsManager.ts: -------------------------------------------------------------------------------- 1 | export class TipsManager { 2 | private static readonly DONT_SHOW_AGAIN_LOCAL_STORAGE_KEY = "no-more-tips" 3 | private readonly layerElement: HTMLElement 4 | 5 | constructor() { 6 | this.layerElement = document.querySelector(".js-tips-layer") as HTMLElement 7 | this.mount() 8 | } 9 | 10 | private mount() { 11 | const closeButton = document.querySelector(".js-tips-layer__close") as HTMLElement 12 | closeButton.addEventListener("click", () => { 13 | this.hide() 14 | }) 15 | 16 | document.addEventListener("keydown", (event) => { 17 | if (!this.isShown()) { 18 | return 19 | } 20 | 21 | if (event.key === "Escape") { 22 | this.hide() 23 | } 24 | }) 25 | } 26 | 27 | public isShown(): boolean { 28 | return this.layerElement.classList.contains("open") 29 | } 30 | 31 | public show() { 32 | if (window.localStorage.getItem(TipsManager.DONT_SHOW_AGAIN_LOCAL_STORAGE_KEY) === "true") { 33 | return 34 | } 35 | 36 | this.layerElement.classList.add("open") 37 | 38 | window.localStorage.setItem(TipsManager.DONT_SHOW_AGAIN_LOCAL_STORAGE_KEY, "true") 39 | } 40 | 41 | public hide() { 42 | this.layerElement.classList.remove("open") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /www/src/clipboard_util.ts: -------------------------------------------------------------------------------- 1 | function fallbackCopyTextToClipboard(text: string): void { 2 | const textArea = document.createElement("textarea") 3 | textArea.value = text 4 | 5 | // Avoid scrolling to the bottom 6 | textArea.style.top = "0" 7 | textArea.style.left = "0" 8 | textArea.style.position = "fixed" 9 | 10 | document.body.appendChild(textArea) 11 | textArea.focus() 12 | textArea.select() 13 | 14 | try { 15 | // noinspection JSDeprecatedSymbols 16 | const successful = document.execCommand("copy") 17 | const msg = successful ? "successful" : "unsuccessful" 18 | console.log("Fallback: Copying text command was " + msg) 19 | } catch (err) { 20 | console.log("Fallback: Oops, unable to copy", err) 21 | } 22 | 23 | document.body.removeChild(textArea) 24 | } 25 | 26 | export function copyTextToClipboard(text: string, onCopy: () => void) { 27 | if (!navigator.clipboard) { 28 | fallbackCopyTextToClipboard(text) 29 | return Promise.resolve() 30 | } 31 | return navigator.clipboard.writeText(text) 32 | .then(() => { 33 | console.log("Async: Copying to clipboard was successful!") 34 | onCopy() 35 | }, err => { 36 | fallbackCopyTextToClipboard(text) 37 | console.log("Async: Could not copy text: ", err, "fallback to old method") 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /www/styles/tips.scss: -------------------------------------------------------------------------------- 1 | .tips-layer { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100vw; 6 | height: 100vh; 7 | display: none; 8 | opacity: 0; 9 | 10 | padding-top: 135px; 11 | 12 | background-color: rgba(0, 0, 0, 0.6); 13 | transition: opacity 1s; 14 | 15 | z-index: 100000; 16 | 17 | &.open { 18 | display: block; 19 | opacity: 1; 20 | } 21 | 22 | .close-button { 23 | position: absolute; 24 | top: 150px; 25 | right: 20px; 26 | width: 30px; 27 | height: 30px; 28 | padding: 0; 29 | margin: 0; 30 | border: none; 31 | border-radius: 50%; 32 | background-color: transparent; 33 | cursor: pointer; 34 | 35 | &:hover { 36 | background-color: var(--tools-button-hover); 37 | } 38 | 39 | svg { 40 | margin-top: 4px; 41 | } 42 | } 43 | 44 | .tip { 45 | position: absolute; 46 | width: max-content; 47 | margin: 0 auto; 48 | padding: 20px; 49 | border-radius: 10px; 50 | background-color: var(--background-color); 51 | border: 1px solid var(--tools-border); 52 | color: var(--text-color); 53 | font-size: 14px; 54 | line-height: 20px; 55 | 56 | .title { 57 | font-weight: 700; 58 | margin-bottom: 10px; 59 | 60 | &.big { 61 | font-size: 20px; 62 | } 63 | } 64 | 65 | span { 66 | color: var(--brand-color); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /www/src/main.ts: -------------------------------------------------------------------------------- 1 | import "./v" 2 | import "./v-hint" 3 | 4 | import { Playground, PlaygroundDefaultAction } from "./Playground" 5 | import {CodeRunner} from "./CodeRunner/CodeRunner"; 6 | 7 | CodeRunner.getVlangVersion().then(resp => { 8 | const versionElement = document.querySelector('.js-version-info') as HTMLElement 9 | versionElement.innerHTML = resp.version 10 | }) 11 | 12 | const editorElement = document.querySelector('.js-playground') as HTMLElement 13 | const playground = new Playground(editorElement) 14 | 15 | playground.registerAction(PlaygroundDefaultAction.RUN, () => { 16 | playground.run() 17 | }) 18 | 19 | playground.registerAction(PlaygroundDefaultAction.FORMAT, () => { 20 | playground.formatCode() 21 | }) 22 | 23 | playground.registerAction(PlaygroundDefaultAction.SHARE, () => { 24 | playground.shareCode() 25 | }) 26 | 27 | playground.registerAction(PlaygroundDefaultAction.CHANGE_THEME, () => { 28 | playground.changeTheme() 29 | }) 30 | 31 | playground.registerRunAsTestConsumer(() => { 32 | const runButton = document.querySelector('.js-run__action') as HTMLButtonElement; 33 | const configurationType = runButton.getAttribute("data-type"); 34 | return configurationType === "Test" 35 | }) 36 | 37 | playground.setupShortcuts() 38 | playground.askLoadUnsavedCode() 39 | 40 | window.onload = () => { 41 | const html = document.querySelector("html") as HTMLElement; 42 | html.style.opacity = '1' 43 | } 44 | -------------------------------------------------------------------------------- /www/public/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /www/src/Repositories/GithubGistCodeRepository.ts: -------------------------------------------------------------------------------- 1 | import {CodeRepository, CodeSnippet} from "./interface"; 2 | 3 | export class GithubGistCodeRepository implements CodeRepository { 4 | public static readonly QUERY_PARAM_NAME = "gist" 5 | 6 | constructor(private id: string) { 7 | } 8 | 9 | saveCode(_: string): void { 10 | } 11 | 12 | getCode(onReady: (snippet: CodeSnippet) => void): void { 13 | fetch("https://api.github.com/gists/" + this.id, { 14 | method: "get", 15 | headers: { 16 | 'Content-Type': 'application/json' 17 | } 18 | }) 19 | .then(resp => resp.json()) 20 | .then(data => { 21 | const files = data.files 22 | const firstKey = Object.keys(files)[0]; 23 | const file = files[firstKey] 24 | const url = file.raw_url 25 | 26 | fetch(url, { 27 | method: "get", 28 | headers: { 29 | 'Content-Type': 'text/plain' 30 | } 31 | }) 32 | .then(r => r.text()) 33 | .then(r => { 34 | onReady({code: r}) 35 | }) 36 | .catch(err => { 37 | console.log(err) 38 | }) 39 | }) 40 | .catch(err => { 41 | console.log(err) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /www/src/QueryParams.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * QueryParams is responsible for parsing query params from URL 3 | * and updating the URL when the params change. 4 | * 5 | * @example 6 | * const queryParams = new QueryParams(window.location.search); 7 | * queryParams.updateURLParameter('theme', 'dark') 8 | * // The URL will be updated to: http://localhost:8080/?theme=dark 9 | */ 10 | export class QueryParams { 11 | public readonly params: URLSearchParams 12 | 13 | /** 14 | * @param path - The path to parse (usually `window.location.search`). 15 | */ 16 | constructor(path: string) { 17 | this.params = new URLSearchParams(path) 18 | } 19 | 20 | /** 21 | * Update the URL with the new param. 22 | * @param param The param to update. 23 | * @param value The new value of the param. 24 | */ 25 | public updateURLParameter(param: string, value: string | null) { 26 | const url = QueryParams.updateURLParameter(window.location.href, param, value) 27 | window.history.replaceState({}, "", url) 28 | } 29 | 30 | public getURLParameter(param: string): string | null { 31 | return this.params.get(param) 32 | } 33 | 34 | private static updateURLParameter(url: string, param: string, value: string | null) { 35 | const parsedUrl = new URL(url) 36 | 37 | if (value) { 38 | parsedUrl.searchParams.set(param, value) 39 | } else { 40 | parsedUrl.searchParams.delete(param) 41 | } 42 | 43 | return parsedUrl.toString() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /www/esbuild.watch.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const esbuild = require("esbuild"); 3 | 4 | function printResult(result) { 5 | Object.keys(result).forEach((fileName) => { 6 | // convert to kilobyte 7 | const fileSize = result[fileName].bytes / 1000; 8 | console.log(`${fileName} => ${fileSize} Kb`); 9 | }); 10 | } 11 | 12 | const styleResult = esbuild.build({ 13 | bundle: true, 14 | minify: false, 15 | metafile: true, 16 | outfile: path.resolve(__dirname, "./public/style.css"), 17 | target: "esnext", 18 | entryPoints: ["./styles/index.css"], 19 | loader: { 20 | '.svg': 'base64', 21 | '.woff2': 'copy', 22 | }, 23 | watch: { 24 | onRebuild(error, result) { 25 | if (error) { 26 | console.error('watch build failed:', error) 27 | } else { 28 | console.log('watch build succeeded:', result) 29 | } 30 | }, 31 | }, 32 | }); 33 | 34 | printResult(styleResult?.metafile?.outputs || {}); 35 | 36 | const codeResult = esbuild.build({ 37 | minify: false, 38 | bundle: true, 39 | keepNames: true, 40 | metafile: true, 41 | outfile: path.resolve(__dirname, "./public/main.bundle.js"), 42 | sourcemap: true, 43 | platform: "browser", 44 | target: "es6", 45 | external: ["codemirror"], 46 | entryPoints: ["./src/main.ts"], 47 | watch: { 48 | onRebuild(error, result) { 49 | if (error) { 50 | console.error('watch build failed:', error) 51 | } else { 52 | console.log('watch build succeeded:', result) 53 | } 54 | }, 55 | }, 56 | }); 57 | 58 | printResult(codeResult?.metafile?.outputs || {}); 59 | -------------------------------------------------------------------------------- /www/src/Repositories/LocalCodeRepository.ts: -------------------------------------------------------------------------------- 1 | import {CodeRepository, CodeSnippet} from "./interface"; 2 | 3 | /** 4 | * Local code repository using the browser's local storage. 5 | */ 6 | export class LocalCodeRepository implements CodeRepository { 7 | private static readonly LOCAL_STORAGE_KEY = "code" 8 | 9 | // language=V 10 | public static readonly WELCOME_CODE = ` 11 | // Welcome to the V Playground! 12 | // Here you can edit, run, and share V code. 13 | // Let's start with a simple "Hello, Playground!" example: 14 | println('Hello, Playground!') 15 | 16 | // To run the code, click the "Run" button or just press Ctrl + R. 17 | // To format the code, click the "Format" button or just press Ctrl + L. 18 | 19 | // More examples are available in top dropdown list. 20 | // You can find Help for shortcuts in the bottom right corner or just press Ctrl + I. 21 | // See also change theme button in the top right corner. 22 | // If you want to learn more about V, visit https://vlang.io/, https://github.com/vlang/v/blob/master/doc/docs.md, and https://modules.vlang.io/ 23 | // Join us on Discord: https://discord.gg/vlang 24 | // Enjoy! 25 | `.trimStart() 26 | 27 | saveCode(code: string) { 28 | window.localStorage.setItem(LocalCodeRepository.LOCAL_STORAGE_KEY, code) 29 | } 30 | 31 | getCode(onReady: (snippet: CodeSnippet) => void) { 32 | const localCode = window.localStorage.getItem(LocalCodeRepository.LOCAL_STORAGE_KEY) 33 | if (localCode === null || localCode === undefined) { 34 | onReady({code: LocalCodeRepository.WELCOME_CODE}) 35 | return 36 | } 37 | onReady({code: localCode}) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/isolate/isolate.v: -------------------------------------------------------------------------------- 1 | module isolate 2 | 3 | import os 4 | import log 5 | 6 | // execute runs a command in a sandbox inside isolate. 7 | pub fn execute(raw_cmd string) os.Result { 8 | cmd := raw_cmd 9 | .trim_indent() 10 | .replace('\t', ' ') 11 | .replace('\r', '') 12 | .replace('\n', ' ') 13 | 14 | $if local ? { 15 | // run all after -- in a command 16 | two_dash_index := cmd.index('-- ') or { -1 } 17 | if two_dash_index != -1 { 18 | local_cmd := cmd[two_dash_index + 3..] 19 | log.info('> local cmd: ${local_cmd}') 20 | return os.execute(local_cmd) 21 | } 22 | } 23 | 24 | $if debug { 25 | log.info('> cmd: ${cmd}') 26 | } 27 | 28 | return os.execute(cmd) 29 | } 30 | 31 | // init_sandbox tries to initialize a sandbox and returns its path and box_id. 32 | // When server compiled with `local` flag, it returns `./` as sandbox path and -1 as box_id. 33 | pub fn init_sandbox() (string, int) { 34 | $if local ? { 35 | return './', -1 36 | } 37 | for box_id in 0 .. 1000 { 38 | // TODO: implement --cg when isolate releases v2 support 39 | // remove --quota if isolate throws `Cannot identify filesystem which contains /var/local/lib/isolate/0` 40 | iso_res := execute('isolate --box-id=${box_id} --init') 41 | if iso_res.exit_code == 0 { 42 | box_path := os.join_path(iso_res.output.trim_string_right('\n'), 'box') 43 | return box_path, box_id 44 | } 45 | } 46 | log.error('> init_sandbox failed to find usable sandbox') 47 | return '', -1 48 | } 49 | 50 | // cleanup_sandbox will cleanup all the artefacts left in a sandbox, given its id. 51 | pub fn cleanup_sandbox(box_id int) { 52 | if box_id == -1 { 53 | return 54 | } 55 | execute('isolate --box-id=${box_id} --cleanup') 56 | } 57 | -------------------------------------------------------------------------------- /server/create_bug_endpoint.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import runners 5 | import models 6 | import net.urllib 7 | 8 | struct CreateBugResponse { 9 | link string 10 | error string 11 | } 12 | 13 | // create_bug_url endpoint is used to create link to submit a bug on GitHub. 14 | // Returns CreateBugResponse with link or error. 15 | @['/create_bug_url'; post] 16 | fn (mut app Server) create_bug_url() vweb.Result { 17 | snippet := app.get_request_code() or { 18 | return app.json(CreateBugResponse{ 19 | error: err.msg() 20 | }) 21 | } 22 | 23 | output := runners.run(snippet) or { 24 | runners.RunResult{ 25 | output: err.msg() 26 | } 27 | } 28 | 29 | version := runners.get_version() 30 | doctor_output := runners.get_doctor_output() or { 31 | return app.json(CreateBugResponse{ 32 | error: err.msg() 33 | }) 34 | } 35 | 36 | hash := hash_code_snippet(snippet) 37 | 38 | app.add_new_code(models.CodeStorage{ 39 | ...snippet 40 | hash: hash 41 | }) 42 | 43 | shared_link := 'https://play.vlang.io/p/${hash}' 44 | code := snippet.code.trim_right('\n') 45 | 46 | mut values := urllib.new_values() 47 | values.add('template', 'bug-report.yml') 48 | 49 | values.add('description', ' 50 | Code: ${shared_link} 51 | 52 | ```v 53 | ${code} 54 | ``` 55 | '.trim_indent()) 56 | 57 | values.add('current', ' 58 | Output: 59 | 60 | ``` 61 | ${output.output} 62 | ``` 63 | '.trim_indent()) 64 | 65 | values.add('version', version) 66 | values.add('environment', ' 67 | ``` 68 | ${doctor_output} 69 | ``` 70 | '.trim_indent()) 71 | 72 | params := values.encode() 73 | url := 'https://github.com/vlang/v/issues/new?${params}' 74 | 75 | return app.json(CreateBugResponse{ 76 | link: url 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v-playground", 3 | "version": "0.4.0", 4 | "description": "Source code for V Playground website and backend", 5 | "scripts": { 6 | "install-server-deps": "cd server && v install", 7 | "build-ts": "cd ./www && npm ci && npm run build", 8 | "watch-ts": "cd ./www && npm ci && npm run watch", 9 | "sass": "cd ./www && sass styles/index.scss:styles/index.css", 10 | "sass-watch": "cd ./www && sass --watch styles/index.scss:styles/index.css", 11 | "mkdir-bin": "mkdir -p ./server/bin", 12 | "build": "npm run sass && npm run build-ts && npm run mkdir-bin && v server -o ./server/bin/server", 13 | "build-prod": "npm run sass && npm run build-ts && npm run mkdir-bin && v server -prod -o ./server/bin/server", 14 | "local-build": "npm run sass && npm run build-ts && npm run mkdir-bin && v -g -d local -d uselibbacktrace -o ./server/bin/server ./server", 15 | "serve": "npm run build && ./server/bin/server", 16 | "local-serve": "npm run local-build && ./server/bin/server", 17 | "run-docker": "docker-compose up -d", 18 | "clean": "rm -rf ./server/bin" 19 | }, 20 | "author": { 21 | "name": "The V language contributors", 22 | "url": "https://vlang.io/" 23 | }, 24 | "contributors": [ 25 | "Petr Makhnev", 26 | "shadowninja55", 27 | "Mark @walkingdevel", 28 | "Delyan Angelov @spytheman", 29 | "Alexander Medvednikov", 30 | "Carlos Silva", 31 | "Carlos Esquerdo Bernat", 32 | "RGBCube", 33 | "Ulises Jeremias Cornejo Fandos", 34 | "Enzo Baldisserri", 35 | "JalonSolov", 36 | "Raúl Hernández", 37 | "shove" 38 | ], 39 | "bugs": { 40 | "url": "https://github.com/vlang/playground/issues" 41 | }, 42 | "homepage": "https://play.vlang.io/", 43 | "devDependencies": { 44 | "esbuild": "0.19.11", 45 | "sass": "1.69.6" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/docker-existing-docker-compose 3 | // If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. 4 | { 5 | "name": "Vlang Playground Container", 6 | // Update the 'dockerComposeFile' list if you have more compose files or use different names. 7 | // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. 8 | "dockerComposeFile": [ 9 | "../docker-compose.yml", 10 | "./docker-compose.yml" 11 | ], 12 | // The 'service' property is the name of the service for the container that VS Code should 13 | // use. Update this value and .devcontainer/docker-compose.yml to the real service name. 14 | "service": "playground", 15 | // The optional 'workspaceFolder' property is the path VS Code should open by default when 16 | // connected. This is typically a file mount in .devcontainer/docker-compose.yml 17 | "workspaceFolder": "/usr/src/app", 18 | "customizations": { 19 | "vscode": { 20 | "extensions": [ 21 | "vlanguage.vscode-vlang", 22 | "ms-vscode.cpptools" 23 | ] 24 | } 25 | }, 26 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 27 | // "forwardPorts": [], 28 | // Uncomment the next line if you want start specific services in your Docker Compose config. 29 | // "runServices": [], 30 | // Uncomment the next line if you want to keep your containers running after VS Code shuts down. 31 | // "shutdownAction": "none", 32 | // Uncomment the next line to run commands after the container is created - for example installing curl. 33 | // "postCreateCommand": "", 34 | // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. 35 | "remoteUser": "vscode" 36 | } -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | # Update this to the name of the service you want to work with in your docker-compose.yml file 4 | playground: 5 | # If you want add a non-root user to your Dockerfile, you can use the "remoteUser" 6 | # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, 7 | # debugging) to execute as the user. Uncomment the next line if you want the entire 8 | # container to run as this user instead. Note that, on Linux, you may need to 9 | # ensure the UID and GID of the container user you create matches your local user. 10 | # See https://aka.ms/vscode-remote/containers/non-root for details. 11 | # 12 | # user: vscode 13 | 14 | # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 15 | # folder. Note that the path of the Dockerfile and context is relative to the *primary* 16 | # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" 17 | # array). The sample below assumes your primary file is in the root of your project. 18 | # 19 | build: 20 | context: . 21 | dockerfile: .devcontainer/Dockerfile 22 | 23 | # volumes: 24 | # # Update this to wherever you want VS Code to mount the folder of your project 25 | # - .:/workspace:cached 26 | 27 | # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details. 28 | # - /var/run/docker.sock:/var/run/docker.sock 29 | 30 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 31 | # cap_add: 32 | # - SYS_PTRACE 33 | # security_opt: 34 | # - seccomp:unconfined 35 | 36 | # Overrides default command so things don't shut down after the process ends. 37 | command: /bin/sh -c "while sleep 1000; do :; done" 38 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thevlang/vlang:buster-dev 2 | 3 | # options 4 | ARG DEV_IMG="false" 5 | 6 | # disable tzdata questions 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | 9 | # use bash 10 | SHELL ["/bin/bash", "-c"] 11 | 12 | # install isolate 13 | RUN apt-get update -y && apt-get install -y --no-install-recommends \ 14 | libcap-dev \ 15 | libseccomp-dev \ 16 | libseccomp2 \ 17 | libcap2-bin \ 18 | asciidoc \ 19 | && apt-get clean && rm -rf /var/lib/apt/lists/* 20 | 21 | RUN git clone https://github.com/ioi/isolate /tmp/isolate \ 22 | && cd /tmp/isolate \ 23 | && make isolate isolate-check-environment \ 24 | && make install \ 25 | && rm -rf /tmp/isolate 26 | 27 | ################################################################################################## 28 | # # 29 | # The code below is copied from: # 30 | # https://github.com/microsoft/vscode-remote-try-go/blob/master/.devcontainer/Dockerfile # 31 | # And modifies to use v lang instead # 32 | # # 33 | ################################################################################################## 34 | 35 | # Options for setup script 36 | ARG INSTALL_ZSH="true" 37 | ARG UPGRADE_PACKAGES="false" 38 | ARG USERNAME=vscode 39 | ARG USER_UID=1000 40 | ARG USER_GID=$USER_UID 41 | 42 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 43 | COPY .devcontainer/common-debian.sh /tmp/library-scripts/ 44 | RUN apt-get update \ 45 | && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \ 46 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 47 | -------------------------------------------------------------------------------- /www/src/Repositories/SharedCodeRepository.ts: -------------------------------------------------------------------------------- 1 | import {CodeRepository, CodeSnippet} from "./interface"; 2 | 3 | export enum SharedCodeRunConfiguration { 4 | Run, 5 | Test, 6 | Cgen, 7 | } 8 | 9 | /** 10 | * { 11 | * "snippet": { 12 | * "id": 3, 13 | * "code": "println(100)", 14 | * "hash": "21cf286fdb", 15 | * "build_arguments": [], 16 | * "run_arguments": [], 17 | * "additional": {} 18 | * }, 19 | * "found": false, 20 | * "error": "" 21 | * } 22 | */ 23 | type SharedCodeResponse = { 24 | snippet: CodeSnippet 25 | found: boolean 26 | error: string 27 | } 28 | 29 | /** 30 | * Shared code repository using the server side SQL storage. 31 | */ 32 | export class SharedCodeRepository implements CodeRepository { 33 | public static readonly QUERY_PARAM_NAME = "query" 34 | public static readonly CODE_NOT_FOUND = "Not found." 35 | 36 | private readonly hash: string 37 | 38 | constructor(hash: string) { 39 | this.hash = hash 40 | } 41 | 42 | saveCode(_: string) { 43 | // nothing to do 44 | } 45 | 46 | getCode(onReady: (snippet: CodeSnippet) => void) { 47 | return this.getSharedCode(onReady) 48 | } 49 | 50 | private getSharedCode(onReady: (snippet: CodeSnippet) => void) { 51 | const data = new FormData() 52 | data.append("hash", this.hash) 53 | 54 | fetch("/query", { 55 | method: "post", 56 | body: data, 57 | }) 58 | .then(resp => resp.json()) 59 | .then(data => data as SharedCodeResponse) 60 | .then(resp => { 61 | console.log(resp) 62 | if (!resp.found) { 63 | onReady({code: SharedCodeRepository.CODE_NOT_FOUND}) 64 | return 65 | } 66 | 67 | if (resp.error != "") { 68 | console.error(resp.error) 69 | onReady({code: SharedCodeRepository.CODE_NOT_FOUND}) 70 | return 71 | } 72 | 73 | onReady(resp.snippet) 74 | }) 75 | .catch(err => { 76 | console.log(err) 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /www/src/Repositories/CodeRepositoryManager.ts: -------------------------------------------------------------------------------- 1 | import { CodeRepository } from "./interface"; 2 | import { SharedCodeRepository } from "./SharedCodeRepository"; 3 | import { TextCodeRepository } from "./TextCodeRepository"; 4 | import { LocalCodeRepository } from "./LocalCodeRepository"; 5 | 6 | import { QueryParams } from "../QueryParams"; 7 | import { PlaygroundConfig } from "../PlaygroundConfig"; 8 | import {Base64CodeRepository} from "./Base64CodeRepository" 9 | import {GithubGistCodeRepository} from "./GithubGistCodeRepository" 10 | 11 | /** 12 | * CodeRepositoryManager is responsible for managing the code repositories. 13 | */ 14 | export class CodeRepositoryManager { 15 | 16 | /** 17 | * Base on `params` tries to select the appropriate repository to get the code. 18 | * 19 | * @param params The query parameters. 20 | * @param config The playground configuration. 21 | * @returns {CodeRepository} 22 | */ 23 | static selectRepository(params: QueryParams, config?: PlaygroundConfig): CodeRepository { 24 | if (config !== undefined && config.codeHash !== null && config.codeHash !== undefined) { 25 | return new SharedCodeRepository(config.codeHash) 26 | } 27 | 28 | if (config !== undefined && config.code !== null && config.code !== undefined) { 29 | return new TextCodeRepository(config.code) 30 | } 31 | 32 | if (config !== undefined && config.embed !== null && config.embed !== undefined && config.embed) { 33 | // By default, editor is empty for embed mode. 34 | return new TextCodeRepository("") 35 | } 36 | 37 | const repository = new LocalCodeRepository() 38 | const hash = params.getURLParameter(SharedCodeRepository.QUERY_PARAM_NAME) 39 | if (hash !== null && hash !== undefined) { 40 | return new SharedCodeRepository(hash) 41 | } 42 | 43 | const base64Code = params.getURLParameter(Base64CodeRepository.QUERY_PARAM_NAME) 44 | if (base64Code !== null && base64Code !== undefined) { 45 | return new Base64CodeRepository(base64Code) 46 | } 47 | 48 | const gistId = params.getURLParameter(GithubGistCodeRepository.QUERY_PARAM_NAME) 49 | if (gistId !== null && gistId !== undefined) { 50 | return new GithubGistCodeRepository(gistId) 51 | } 52 | 53 | return repository 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /www/styles/editor-themes/light.css: -------------------------------------------------------------------------------- 1 | .cm-s-light .cm-keyword, .cm-s-light .cm-valid-escape { color: #0033b3; } 2 | .cm-s-light .cm-builtin { color: #0033b3; } 3 | .cm-s-light .cm-atom { color: #219; } 4 | .cm-s-light .cm-number { color: #1750EB; } 5 | .cm-s-light .cm-at-identifier { color: #708} 6 | .cm-s-light .cm-def { color: #00f; } 7 | .cm-s-light .cm-variable, 8 | .cm-s-light .cm-punctuation {} 9 | .cm-s-light .cm-property { color: #871094; } 10 | .cm-s-light .cm-operator {} 11 | .cm-s-light .cm-variable-2 { color: #05a; } 12 | .cm-s-light .cm-variable-3, .cm-s-light .cm-type { font-weight: 700; } 13 | .cm-s-light .cm-comment { color: #8C8C8C; } 14 | .cm-s-light .cm-string { color: #067D17; } 15 | .cm-s-light .cm-string-2 { color: #067D17; } 16 | .cm-s-light .cm-meta { color: #555; } 17 | .cm-s-light .cm-qualifier { color: #555; } 18 | .cm-s-light .cm-bracket { color: #997; } 19 | .cm-s-light .cm-tag { color: #170; } 20 | .cm-s-light .cm-attribute { color: #9E880D !important; } 21 | .cm-s-light .cm-hash-directive { color: #9E880D !important; } 22 | .cm-s-light .cm-hr { color: #999; } 23 | .cm-s-light .cm-link { color: #00c; } 24 | .cm-s-light .cm-function {} 25 | .cm-s-light .cm-start-interpolation { color: #0037A6; } 26 | .cm-s-light .cm-end-interpolation { color: #0037A6; } 27 | .cm-s-light .cm-import-name { font-weight: 700; } 28 | 29 | .CodeMirror.cm-s-light .CodeMirror-line.cgen-highlight { background: #9cc5ef; } 30 | 31 | .CodeMirror.cm-s-light span.CodeMirror-matchingbracket { 32 | background-color: #93d9d9; 33 | color: #000; 34 | padding: 1px 0; 35 | } 36 | .CodeMirror.cm-s-light span.CodeMirror-nonmatchingbracket { color: #a22; } 37 | 38 | .cm-s-light .CodeMirror-gutters { 39 | background-color: #fff; 40 | border-right: 1px solid #eaecef; 41 | } 42 | 43 | .CodeMirror-linenumbers { 44 | border-right: 0 solid #e7e8ec; 45 | } 46 | 47 | .CodeMirror-linenumber { 48 | color: #aeb3c2; 49 | } 50 | 51 | .cm-s-light .CodeMirror-selected { background: #9cc5ef; } 52 | .cm-s-light .CodeMirror-focused .CodeMirror-selected { background: #9cc5ef; } 53 | .cm-s-light .CodeMirror-crosshair { cursor: crosshair; } 54 | .cm-s-light .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 55 | .cm-s-light .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 56 | -------------------------------------------------------------------------------- /www/src/HelpManager.ts: -------------------------------------------------------------------------------- 1 | export class HelpManager { 2 | // TODO: don't know other way to detect macOS... 3 | // noinspection JSDeprecatedSymbols 4 | static isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0 5 | 6 | private containingElement: HTMLElement 7 | private readonly element: HTMLElement 8 | private readonly helpOverlay!: HTMLElement 9 | private readonly showHelpButton!: HTMLElement 10 | private readonly closeHelpButton!: HTMLElement 11 | 12 | constructor(containingElement: HTMLElement) { 13 | this.containingElement = containingElement 14 | this.element = containingElement.getElementsByClassName("js-help-wrapper")[0] as HTMLElement 15 | 16 | if (this.element === null || this.element === undefined) { 17 | return 18 | } 19 | 20 | this.helpOverlay = this.element.querySelector(".js-help-overlay")! 21 | this.showHelpButton = this.element.querySelector(".js-show-help")! 22 | this.closeHelpButton = this.element.querySelector(".js-close-help")! 23 | 24 | this.mount() 25 | } 26 | 27 | private mount() { 28 | if (this.showHelpButton !== undefined) { 29 | this.showHelpButton.addEventListener("click", () => { 30 | this.toggleHelp() 31 | }) 32 | } 33 | 34 | if (this.helpOverlay !== undefined) { 35 | this.helpOverlay.addEventListener("click", () => { 36 | this.toggleHelp() 37 | }) 38 | } 39 | 40 | if (this.closeHelpButton !== undefined) { 41 | this.closeHelpButton.addEventListener("click", () => { 42 | this.toggleHelp() 43 | }) 44 | } 45 | 46 | // Replace shortcut with understandable for OS user: 47 | // - macOS: ⌃ 48 | // - Windows/Linux: Ctrl 49 | if (!HelpManager.isMac) { 50 | const shortcuts = document.querySelectorAll(".js-shortcut kbd.ctrl") 51 | shortcuts.forEach(function (shortcut: HTMLElement) { 52 | shortcut.innerText = "Ctrl" 53 | }) 54 | } 55 | } 56 | 57 | public closeHelp() { 58 | if (!this.helpOverlay.classList.contains("opened")) { 59 | return 60 | } 61 | this.toggleHelp() 62 | } 63 | 64 | public toggleHelp() { 65 | const help = this.containingElement.getElementsByClassName("js-help")[0] 66 | help.classList.toggle("opened") 67 | this.helpOverlay.classList.toggle("opened") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /www/styles/editor-themes/dark.css: -------------------------------------------------------------------------------- 1 | .CodeMirror.cm-s-dark { 2 | color: #BBBBBB; 3 | } 4 | 5 | .cm-s-dark .cm-keyword, .cm-s-dark .cm-valid-escape { color: #CC7832; } 6 | .cm-s-dark .cm-builtin { color: #CC7832; } 7 | .cm-s-dark .cm-atom { color: #CC7832; } 8 | .cm-s-dark .cm-number { color: #6897BB; } 9 | .cm-s-dark .cm-at-identifier { color: #9876AA} 10 | .cm-s-dark .cm-compile-time-identifier { color: #9876AA} 11 | .cm-s-dark .cm-variable, 12 | .cm-s-dark .cm-punctuation {} 13 | .cm-s-dark .cm-property { color: #9876AA; } 14 | .cm-s-dark .cm-operator {} 15 | .cm-s-dark .cm-variable-2 { color: #05a; } 16 | .cm-s-dark .cm-variable-3, .cm-type{ font-weight: 700; } 17 | .cm-s-dark .cm-comment { color: #808080; } 18 | .cm-s-dark .cm-string { color: #6A8759; } 19 | .cm-s-dark .cm-string-2 { color: #6A8759; } 20 | .cm-s-dark .cm-meta { color: #555; } 21 | .cm-s-dark .cm-qualifier { color: #555; } 22 | .cm-s-dark .cm-bracket { color: #997; } 23 | .cm-s-dark .cm-tag { color: #170; } 24 | .cm-s-dark .cm-attribute { color: #BBB529 !important; } 25 | .cm-s-dark .cm-hash-directive { color: #BBB529 !important; } 26 | .cm-s-dark .cm-hr { color: #999; } 27 | .cm-s-dark .cm-link { color: #00c; } 28 | .cm-s-dark .cm-function,.cm-def { color: #FFC66D; } 29 | .cm-s-dark .cm-start-interpolation { color: #CC7832; } 30 | .cm-s-dark .cm-end-interpolation { color: #CC7832; } 31 | .cm-s-dark .cm-import-name { color: #AFBF7E; } 32 | 33 | .CodeMirror.cm-s-dark .CodeMirror-line.cgen-highlight { background: #20407f; } 34 | 35 | .CodeMirror.cm-s-dark span.CodeMirror-matchingbracket { 36 | background-color: #336146; 37 | color: #ced9d3; 38 | padding: 1px 0; 39 | } 40 | .CodeMirror.cm-s-dark span.CodeMirror-nonmatchingbracket { color: #a22; } 41 | 42 | .cm-s-dark .CodeMirror-gutters { 43 | background-color: #1e1f22; 44 | border-right: 1px solid #323438; 45 | } 46 | 47 | .cm-s-dark .CodeMirror-linenumbers { 48 | border-right: 0 solid #e7e8ec; 49 | } 50 | 51 | .cm-s-dark .CodeMirror-linenumber { 52 | color: #4f5157; 53 | } 54 | 55 | .cm-s-dark .CodeMirror-cursor { 56 | border-left: 1px solid #bbbbbb; 57 | border-right: none; 58 | width: 0; 59 | } 60 | 61 | .cm-s-dark .CodeMirror-selected { background: #20407f; } 62 | .cm-s-dark .CodeMirror-focused .CodeMirror-selected { background: #20407f; } 63 | .cm-s-dark .CodeMirror-crosshair { cursor: crosshair; } 64 | .cm-s-dark .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 65 | .cm-s-dark .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 66 | -------------------------------------------------------------------------------- /www/src/icons.ts: -------------------------------------------------------------------------------- 1 | export const runIcons = ` 2 | 3 | 5 | 7 | 9 | 10 | ` 11 | export const testIcons = ` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ` 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # V Playground: Run, Edit, Share V Code Online 2 | 3 | The [V Playground](https://play.vlang.io/) is a place where you can run, edit and share V 4 | code online. 5 | 6 | ![](./docs/images/cover.png) 7 | 8 | ## Features 9 | 10 | - Nice and clean UI 11 | - Powerful editor with syntax highlighting and auto-completion 12 | - Ability to run test code. 13 | - Ability to see the generated C code, for the passed V code. 14 | - Pass compilation flags to the V compiler, and separate for your program. 15 | - Shareable code and editor state via URL or local storage. 16 | 17 | ## Developing 18 | 19 | If you wish to improve the playground, first you have to clone the repository: 20 | 21 | ```bash 22 | git clone https://github.com/vlang/playground 23 | cd playground 24 | ``` 25 | 26 | Install V dependencies: 27 | 28 | ```bash 29 | npm run install-server-deps 30 | ``` 31 | 32 | ### Run the playground locally 33 | ```bash 34 | npm run local-serve 35 | ``` 36 | then access the playground at 37 | 38 | 39 | ### Quick, containerized local development (recommended) 40 | 41 | #### Using Docker Compose 42 | 43 | ```bash 44 | npm run run-docker 45 | ``` 46 | 47 | then access the playground at 48 | 49 | ### Using VSCode DevContainers 50 | 51 | 1. Install Docker 52 | 2. Install [Visual Studio Code](https://code.visualstudio.com/) 53 | 3. Install the 54 | [Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) 55 | extension for VS Code 56 | 4. Clone 57 | 5. Create your application within a container (see gif below) 58 | 59 | Done. 60 | 61 | Since you are using a docker container, your main system will remain "clean". 62 | 63 | ![vscode-open-in-container](https://user-images.githubusercontent.com/17727170/197407889-88fe33b0-8e95-47fe-b2db-598fd307140e.gif) 64 | 65 | Then just run: 66 | 67 | ```sh 68 | npm run serve 69 | ``` 70 | 71 | then access the playground at 72 | 73 | ### Run the playground locally inside isolate (as on https://play.vlang.io/) 74 | 75 | > NOTE: Only works on Linux, since it uses `isolate`. 76 | 77 | #### Install Dependencies 78 | 79 | > We use isolate to sandbox the playground, so you need to install it first. 80 | 81 | ```bash 82 | git clone https://github.com/ioi/isolate /tmp/isolate 83 | cd /tmp/isolate 84 | make isolate isolate-check-environment 85 | make install 86 | ``` 87 | 88 | #### Run the server 89 | 90 | ```bash 91 | npm run serve 92 | ``` 93 | 94 | ... then access the playground at 95 | 96 | ## Server API 97 | 98 | See [server/README.md](./server/README.md) for more information about the server API. 99 | 100 | ## License 101 | 102 | This project is under the **MIT License**. 103 | See the [LICENSE](https://github.com/vlang/playground/blob/main/LICENSE) 104 | file for the full license text. 105 | 106 | -------------------------------------------------------------------------------- /www/styles/playground_select_examples.scss: -------------------------------------------------------------------------------- 1 | html { 2 | --select-background: #f7f8fa; 3 | --select-background-hover: #fff; 4 | --select-active-background: #fff; 5 | --select-color-hover: #000; 6 | --select-color: #000; 7 | --select-header-border: #ecedf1; 8 | } 9 | 10 | html[data-theme='dark'] { 11 | --select-background: #2a2a2d; 12 | --select-background-hover: #2d2e33; 13 | --select-active-background: #2d2e33; 14 | --select-color-hover: #f7f8fa; 15 | --select-color: #f7f8fa; 16 | --select-header-border: #323438; 17 | } 18 | 19 | .playground__examples__select { 20 | width: 250px; 21 | 22 | display: grid; 23 | grid-template-rows: max-content auto; 24 | grid-row-gap: 3px; 25 | 26 | label { 27 | font-size: 11px; 28 | color: var(--secondary-color); 29 | } 30 | } 31 | 32 | .dropdown { 33 | max-width: 400px; 34 | position: relative; 35 | font-family: "FiraSans", Helvetica, Arial, sans-serif; 36 | font-size: 14px; 37 | 38 | outline: none; 39 | } 40 | 41 | .dropdown__button { 42 | position: relative; 43 | display: grid; 44 | grid-template-columns: auto max-content; 45 | align-items: center; 46 | line-height: 18px; 47 | color: var(--select-color); 48 | width: 100%; 49 | text-align: left; 50 | background: var(--select-background); 51 | border: 1px solid var(--select-header-border); 52 | border-radius: 4px; 53 | height: 30px; 54 | padding: 5px 10px 5px 10px; 55 | cursor: pointer; 56 | font-size: 14px; 57 | 58 | outline: none; 59 | } 60 | 61 | .dropdown__button span { 62 | text-overflow: ellipsis; 63 | overflow: hidden; 64 | white-space: nowrap; 65 | } 66 | 67 | .dropdown__button .open-icon { 68 | margin-top: 3px; 69 | } 70 | 71 | .dropdown__list { 72 | width: 250px; 73 | margin: 10px 0; 74 | padding: 0; 75 | list-style-type: none; 76 | position: absolute; 77 | left: 40px; 78 | top: 120px; 79 | border-radius: 4px; 80 | background: var(--select-background); 81 | overflow: hidden; 82 | z-index: 1000; 83 | opacity: 0; 84 | visibility: hidden; 85 | transition: 200ms ease; 86 | 87 | font-size: 14px; 88 | color: var(--select-color); 89 | 90 | box-shadow: 0 10px 30px -0px rgba(0, 0, 0, 0.1); 91 | border: 1px solid var(--terminal-header-border); 92 | } 93 | 94 | .dropdown__list_visible { 95 | opacity: 1; 96 | visibility: visible; 97 | } 98 | 99 | .dropdown__list-item { 100 | margin: 0; 101 | padding: 10px 15px; 102 | cursor: pointer; 103 | line-height: 18px; 104 | color: var(--select-color); 105 | transition: 200ms ease; 106 | } 107 | 108 | .dropdown__list-item_active { 109 | background: var(--select-active-background); 110 | } 111 | 112 | .dropdown__list-item:hover { 113 | background: var(--select-background-hover); 114 | } 115 | 116 | .dropdown__input_hidden { 117 | display: none; 118 | } 119 | -------------------------------------------------------------------------------- /www/styles/playground_help.scss: -------------------------------------------------------------------------------- 1 | html { 2 | --help-button-background: #fff; 3 | --help-button-background-hover: #f7f8fa; 4 | --help-button-color: #545454; 5 | --help-button-border: #7c7c7c; 6 | --help-button-border-hover: #4f4f4f; 7 | --help-tip-background: #fff; 8 | --help-tip-text: #000; 9 | } 10 | 11 | html[data-theme='dark'] { 12 | --help-button-background: #2a2a2d; 13 | --help-button-background-hover: #2e2e31; 14 | --help-button-color: #e0e0e0; 15 | --help-button-border: #737373; 16 | --help-button-border-hover: #a1a1a1; 17 | --help-tip-background: #fff; 18 | --help-tip-text: #000; 19 | } 20 | 21 | .playground__help-button { 22 | width: 40px; 23 | height: 40px; 24 | border-radius: 50%; 25 | border: 1px solid var(--help-button-border); 26 | background-color: var(--help-button-background); 27 | color: var(--help-button-color); 28 | font-size: 25px; 29 | outline: none; 30 | 31 | transition: background-color 100ms, border-color 100ms; 32 | 33 | &::after { 34 | content: '?'; 35 | } 36 | } 37 | 38 | .playground__help-button:hover { 39 | cursor: pointer; 40 | border: 1px solid var(--help-button-border-hover); 41 | background-color: var(--help-button-background-hover); 42 | } 43 | 44 | .playground__help-overlay { 45 | display: none; 46 | width: 100vw; 47 | height: 100vh; 48 | position: fixed; 49 | top: 0; 50 | left: 0; 51 | z-index: 100; 52 | } 53 | 54 | .playground__help-overlay.opened { 55 | display: block; 56 | } 57 | 58 | .playground__help-tip { 59 | width: 300px; 60 | border-radius: 10px; 61 | background-color: #fff; 62 | color: var(--black); 63 | box-shadow: 0 15px 30px -10px rgba(0, 0, 0, 0.1); 64 | font-size: 16px; 65 | 66 | margin-top: -300px; 67 | padding: 15px 15px; 68 | right: 50px; 69 | bottom: 50px; 70 | 71 | position: absolute; 72 | 73 | display: none; 74 | 75 | z-index: 1000; 76 | border: 1px solid var(--terminal-header-border); 77 | 78 | @media (max-width: 1000px) { 79 | bottom: 60px; 80 | right: 20px; 81 | } 82 | } 83 | 84 | .playground__help-tip.opened { 85 | display: block; 86 | } 87 | 88 | .playground__help-tip .header { 89 | display: grid; 90 | grid-template-columns: auto max-content; 91 | align-content: center; 92 | height: 30px; 93 | 94 | margin-bottom: 20px; 95 | } 96 | 97 | .playground__help-tip .header .playground__close-help-tip-button { 98 | margin-top: 2px; 99 | border-radius: 5px; 100 | border: none; 101 | background-color: transparent; 102 | } 103 | 104 | .playground__help-tip .header .playground__close-help-tip-button:hover { 105 | cursor: pointer; 106 | } 107 | 108 | .playground__help-tip .about-link { 109 | margin-top: 20px; 110 | } 111 | 112 | .playground__help-tip .about-link a { 113 | color: #000; 114 | } 115 | 116 | .playground__help-tip .about-link a:visited { 117 | color: #000; 118 | } 119 | -------------------------------------------------------------------------------- /server/server.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vweb 4 | import os 5 | import db.sqlite 6 | import isolate 7 | import models 8 | import flag 9 | import time 10 | import log 11 | 12 | const default_port = 5555 13 | 14 | struct Server { 15 | vweb.Context 16 | pub mut: 17 | db sqlite.DB 18 | } 19 | 20 | // get_request_code retrieves information about the code to run from the request. 21 | // If the code is not provided, an error is returned. 22 | fn (mut app Server) get_request_code() !models.CodeStorage { 23 | return models.CodeStorage{ 24 | id: 0 25 | code: app.form['code'] or { return error('No code was provided.') } 26 | hash: '' 27 | build_arguments: app.form['build-arguments'] or { '' } 28 | run_arguments: app.form['run-arguments'] or { '' } 29 | run_configuration: app.form['run-configuration'] or { '0' }.int() 30 | } 31 | } 32 | 33 | // add_new_code adds a new code snippet to the database. 34 | fn (mut app Server) add_new_code(snippet models.CodeStorage) { 35 | println('Added new code snippet with hash: ${snippet.hash}, run configuration: ${snippet.run_configuration}') 36 | 37 | count := sql app.db { 38 | select count from models.CodeStorage where hash == snippet.hash 39 | } or { 0 } 40 | 41 | if count != 0 { 42 | println('Code with ${snippet.hash} already added') 43 | return 44 | } 45 | 46 | sql app.db { 47 | insert snippet into models.CodeStorage 48 | } or { panic(err) } 49 | } 50 | 51 | // get_saved_code retrieves a code snippet from the database by its hash. 52 | fn (mut app Server) get_saved_code(hash string) ?models.CodeStorage { 53 | found := sql app.db { 54 | select from models.CodeStorage where hash == hash 55 | } or { panic(err) } 56 | 57 | if found.len == 0 { 58 | return none 59 | } 60 | 61 | return found.last() 62 | } 63 | 64 | // init_once initializes the server. 65 | fn (mut app Server) init_once() { 66 | app.db = sqlite.connect('code_storage.db') or { panic(err) } 67 | sql app.db { 68 | create table models.CodeStorage 69 | } or { panic(err) } 70 | isolate.execute('isolate --cleanup') 71 | app.handle_static('./www/public', true) 72 | app.serve_static('/', './www/public/') 73 | } 74 | 75 | // precompile_vfmt prepares the vfmt binary in the sandbox. 76 | // 77 | // V can't compile fmt first time in isolate because: 78 | // 79 | // `folder '/opt/vlang/cmd/tools' is not writable` 80 | // 81 | // when run `v fmt`, so we need to run `v fmt` first time outside isolate. 82 | fn precompile_vfmt() { 83 | result := os.execute('${@VEXEROOT}/v fmt') 84 | 85 | if result.exit_code != 0 { 86 | panic(result.output) 87 | } 88 | 89 | $if debug { 90 | eprintln('v fmt successfully precompiled.') 91 | } 92 | } 93 | 94 | fn report_memory_usage() { 95 | time.sleep(2 * time.second) 96 | for { 97 | log.info('server gc_memory_use: ${gc_memory_use()}') 98 | time.sleep(2 * time.minute) 99 | } 100 | } 101 | 102 | // precompile_vtest prepares the vtest binary in the sandbox. 103 | // See `precompile_vfmt` for more details. 104 | fn precompile_vtest() { 105 | result := os.execute('${@VEXEROOT}/v test .') 106 | 107 | if result.exit_code != 0 { 108 | panic(result.output) 109 | } 110 | 111 | $if debug { 112 | eprintln('v test successfully precompiled.') 113 | } 114 | } 115 | 116 | fn main() { 117 | mut fp := flag.new_flag_parser(os.args) 118 | fp.application('Playground server') 119 | fp.version('v0.2.0') 120 | fp.description('A playground server for V language.') 121 | fp.skip_executable() 122 | port := fp.int('port', `p`, default_port, 'port to run the server on') 123 | 124 | fp.finalize() or { 125 | eprintln(err) 126 | println(fp.usage()) 127 | return 128 | } 129 | 130 | precompile_vfmt() 131 | precompile_vtest() 132 | 133 | mut app := &Server{} 134 | app.init_once() 135 | spawn report_memory_usage() 136 | vweb.run(app, port) 137 | } 138 | -------------------------------------------------------------------------------- /www/src/Examples/ExamplesManager.ts: -------------------------------------------------------------------------------- 1 | import {examples, IExample} from "./examples" 2 | 3 | export class ExamplesManager { 4 | private readonly selectElement: HTMLElement 5 | private onSelectHandler: ((example: IExample) => void) | null = null 6 | 7 | constructor() { 8 | this.selectElement = document.querySelector(".js-examples__select") as HTMLElement 9 | } 10 | 11 | public registerOnSelectHandler(handler: (example: IExample) => void) { 12 | this.onSelectHandler = handler 13 | } 14 | 15 | public mount() { 16 | if (this.selectElement === null || this.selectElement === undefined) { 17 | return 18 | } 19 | 20 | const examplesSelectList = document.querySelector(".dropdown__list")! 21 | const examplesButton = this.selectElement.querySelector(".dropdown__button") 22 | 23 | if (examplesSelectList !== null && examplesButton !== null) { 24 | examples.forEach((example: IExample, index: number) => { 25 | examplesSelectList.innerHTML += ExamplesManager.exampleElementListTemplate(example.name, index) 26 | }) 27 | 28 | const examplesButtonSpan = examplesButton.querySelector("span")! 29 | examplesButtonSpan.innerText = examples[0].name 30 | } 31 | 32 | const dropdownItems = examplesSelectList.querySelectorAll(".dropdown__list-item") 33 | dropdownItems.forEach((option: HTMLElement) => { 34 | option.addEventListener("click", () => { 35 | const exampleName = option.innerText 36 | const example = examples.find((example) => { 37 | return example.name === exampleName 38 | }) 39 | 40 | if (this.onSelectHandler !== null && example) { 41 | this.onSelectHandler(example) 42 | } 43 | }) 44 | }) 45 | 46 | const dropdownBtn = this.selectElement.querySelector(".dropdown__button")! 47 | const dropdownInput = this.selectElement.querySelector(".dropdown__input_hidden")! 48 | const dropdownList = document.querySelector(".dropdown__list")! 49 | 50 | dropdownBtn.addEventListener("click", function () { 51 | dropdownList.classList.toggle("dropdown__list_visible") 52 | this.classList.toggle("dropdown__button_active") 53 | }) 54 | 55 | dropdownItems.forEach(function (option: HTMLElement) { 56 | option.addEventListener("click", function (e) { 57 | dropdownItems.forEach(function (el) { 58 | el.classList.remove("dropdown__list-item_active") 59 | }) 60 | const target = e.target as HTMLElement 61 | target.classList.add("dropdown__list-item_active") 62 | 63 | const dropdownBtnSpan = dropdownBtn.querySelector("span")! 64 | dropdownBtnSpan.innerText = this.innerText 65 | dropdownInput.value = this.dataset.value ?? "" 66 | dropdownList.classList.remove("dropdown__list_visible") 67 | }) 68 | }) 69 | 70 | document.addEventListener("click", function (e) { 71 | if (e.target !== dropdownBtn && !dropdownBtn.contains(e.target as Node)) { 72 | dropdownBtn.classList.remove("dropdown__button_active") 73 | dropdownList.classList.remove("dropdown__list_visible") 74 | } 75 | }) 76 | 77 | document.addEventListener("keydown", function (e) { 78 | if (e.key === "Tab" || e.key === "Escape") { 79 | dropdownBtn.classList.remove("dropdown__button_active") 80 | dropdownList.classList.remove("dropdown__list_visible") 81 | } 82 | }) 83 | } 84 | 85 | static exampleElementListTemplate = function (name: string, index: number) { 86 | let className = "" 87 | if (index === 0) { 88 | className = "dropdown__list-item_active" 89 | } 90 | return ` 91 | 92 | ` 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # V Playground Server 2 | 3 | This is the server for the V Playground. 4 | 5 | ## Endpoints 6 | 7 | ### GET `/` 8 | 9 | Returns the playground index page. 10 | 11 | ### POST `/run` 12 | 13 | Run the code and return the output. 14 | If an error occurs, the error will be returned in the `error` field. 15 | 16 | Required form field: 17 | 18 | - `code` — code to run 19 | 20 | Additional form fields: 21 | 22 | - `build_arguments` — build arguments when building binary 23 | - `run_arguments` — run arguments when running binary 24 | - `run_configuration` — run configuration type 25 | 26 | #### Request 27 | 28 | ```curl 29 | curl -X POST localhost:5555/run -F 'code="println(100)"' 30 | ``` 31 | 32 | #### Response 33 | 34 | ```json 35 | { 36 | "output": "100", 37 | "error": "" 38 | } 39 | ``` 40 | 41 | ### POST `/run_test` 42 | 43 | Run the code as a test and return the output. 44 | If an error occurs, the error will be returned in the `error` field. 45 | 46 | Required form field: 47 | 48 | - `code` — code to test 49 | 50 | Additional form fields: 51 | 52 | - `build_arguments` — build arguments when building binary 53 | - `run_arguments` — run arguments when running binary 54 | - `run_configuration` — run configuration type 55 | 56 | #### Request 57 | 58 | ```curl 59 | curl -X POST localhost:5555/run_test -F 'code="fn test_foo() { assert 100 == 100 }"' 60 | ``` 61 | 62 | #### Response 63 | 64 | ```json 65 | { 66 | "output": "---- Testing... ", 67 | "error": "" 68 | } 69 | ``` 70 | 71 | ### POST `/cgen` 72 | 73 | Retrieve the C code generated from the passed V code. 74 | If an error occurs, the error will be returned in the `error` field. 75 | 76 | Required form field: 77 | 78 | - `code` — code to generate C code 79 | 80 | Additional form fields: 81 | 82 | - `build_arguments` — build arguments when building binary 83 | 84 | #### Request 85 | 86 | ```curl 87 | curl -X POST localhost:5555/cgen -F 'code="println(100)"' 88 | ``` 89 | 90 | #### Response 91 | 92 | ```json 93 | { 94 | "cgenCode": "...", 95 | "error": "" 96 | } 97 | ``` 98 | 99 | ### POST `/format` 100 | 101 | Format the code and return the formatted code. 102 | If an error occurs, the error will be returned in the `error` field. 103 | 104 | Required form field: 105 | 106 | - `code` — code to format 107 | 108 | #### Request 109 | 110 | ```curl 111 | curl -X POST localhost:5555/format -F 'code="println( 100 )"' 112 | ``` 113 | 114 | #### Response 115 | 116 | ```json 117 | { 118 | "output": "println(100)\n", 119 | "error": "" 120 | } 121 | ``` 122 | 123 | ### POST `/share` 124 | 125 | Share the code and return the hash. 126 | If an error occurs, the error will be returned in the `error` field. 127 | 128 | Required form field: 129 | 130 | - `code` — code to share 131 | 132 | Additional form fields: 133 | 134 | - `build_arguments` — build arguments when building binary 135 | - `run_arguments` — run arguments when running binary 136 | - `run_configuration` — run configuration type 137 | 138 | #### Request 139 | 140 | ```curl 141 | curl -X POST localhost:5555/share -F 'code="println(100)"' 142 | ``` 143 | 144 | #### Response 145 | 146 | ```json 147 | { 148 | "hash": "21cf286fdb", 149 | "error": "" 150 | } 151 | ``` 152 | 153 | ### POST `/query` 154 | 155 | Return the code for the given hash. 156 | If code for the hash does not exist, `found` field will be false. 157 | If an error occurs, the error will be returned in the `error` field. 158 | 159 | Returns JSON contains `snippet` field which is the snippet for the hash. 160 | Snippet structure is next: 161 | 162 | ```json 163 | { 164 | "id": { 165 | "type": "integer", 166 | "description": "id of the snippet" 167 | }, 168 | "code": { 169 | "type": "string", 170 | "description": "code of the snippet" 171 | }, 172 | "hash": { 173 | "type": "string", 174 | "description": "hash of the snippet" 175 | }, 176 | "build_arguments": { 177 | "type": "array", 178 | "description": "build arguments when building binary" 179 | }, 180 | "run_arguments": { 181 | "type": "array", 182 | "description": "run arguments when running binary" 183 | }, 184 | "run_configuration": { 185 | "type": "integer", 186 | "description": "run configuration type" 187 | }, 188 | "additional": { 189 | "type": "object", 190 | "description": "additional data" 191 | } 192 | } 193 | ``` 194 | 195 | Required form field: 196 | 197 | - `hash` — hash of the code 198 | 199 | #### Request 200 | 201 | ```curl 202 | curl -X POST localhost:5555/query -F 'hash="21cf286fdb"' 203 | ``` 204 | 205 | #### Response 206 | 207 | ```json 208 | { 209 | "snippet": { 210 | "id": 3, 211 | "code": "println(100)", 212 | "hash": "21cf286fdb", 213 | "buildArguments": [], 214 | "runArguments": [], 215 | "runConfiguration": 0, 216 | "additional": {} 217 | }, 218 | "found": false, 219 | "error": "" 220 | } 221 | ``` 222 | -------------------------------------------------------------------------------- /www/src/Editor/Editor.ts: -------------------------------------------------------------------------------- 1 | import {CodeRepository, LocalCodeRepository, SharedCodeRepository} from "../Repositories" 2 | import {ITheme} from "../themes" 3 | import {codeIfSharedLinkBroken} from "../Examples" 4 | import {Terminal} from "../Terminal/Terminal" 5 | import {RunnableCodeSnippet} from "../CodeRunner/CodeRunner"; 6 | import { 7 | RunConfigurationManager, 8 | toSharedRunConfiguration 9 | } from "../RunConfigurationManager/RunConfigurationManager"; 10 | 11 | export class Editor { 12 | private static readonly FONT_LOCAL_STORAGE_KEY = "editor-font-size" 13 | 14 | private wrapperElement: HTMLElement 15 | private readonly textAreaElement: HTMLTextAreaElement 16 | private repository: CodeRepository 17 | public editor: CodeMirror.Editor 18 | 19 | constructor(id: string, wrapper: HTMLElement, repository: CodeRepository, public terminal: Terminal, readOnly: boolean, mode: string) { 20 | const editorConfig = { 21 | mode: mode, 22 | lineNumbers: true, 23 | matchBrackets: true, 24 | extraKeys: { 25 | "Ctrl-Space": "autocomplete", 26 | "Ctrl-/": "toggleComment", 27 | }, 28 | readOnly: readOnly, 29 | indentWithTabs: true, 30 | indentUnit: 4, 31 | autoCloseBrackets: true, 32 | showHint: true, 33 | lint: { 34 | async: true, 35 | lintOnChange: true, 36 | delay: 20, 37 | }, 38 | toggleLineComment: { 39 | indent: true, 40 | padding: " ", 41 | }, 42 | theme: "dark", 43 | } 44 | 45 | this.wrapperElement = wrapper 46 | 47 | this.textAreaElement = wrapper.querySelector(`textarea.${id}`)! as HTMLTextAreaElement 48 | // @ts-ignore 49 | this.editor = CodeMirror.fromTextArea(this.textAreaElement, editorConfig) 50 | this.repository = repository 51 | 52 | this.initFont() 53 | } 54 | 55 | private initFont() { 56 | const fontSize = window.localStorage.getItem(Editor.FONT_LOCAL_STORAGE_KEY) 57 | if (fontSize !== null) { 58 | this.setEditorFontSize(fontSize) 59 | } 60 | } 61 | 62 | public changeEditorFontSize(delta: number) { 63 | const cm = document.getElementsByClassName("CodeMirror")[0] as HTMLElement 64 | const fontSize = window.getComputedStyle(cm, null).getPropertyValue("font-size") 65 | if (fontSize) { 66 | const newFontSize = parseInt(fontSize) + delta 67 | cm.style.fontSize = newFontSize + "px" 68 | window.localStorage.setItem(Editor.FONT_LOCAL_STORAGE_KEY, newFontSize.toString()) 69 | this.editor.refresh() 70 | } 71 | } 72 | 73 | private setEditorFontSize(size: string) { 74 | const cm = document.getElementsByClassName("CodeMirror")[0] as HTMLElement 75 | cm.style.fontSize = size + "px" 76 | this.refresh() 77 | } 78 | 79 | public setCode(code: string, preserveCursor: boolean = false) { 80 | const cursor = this.editor.getCursor() 81 | this.editor.setValue(code) 82 | this.repository.saveCode(code) 83 | 84 | if (preserveCursor) { 85 | this.editor.setCursor(cursor) 86 | } 87 | } 88 | 89 | public getCode() { 90 | return this.editor.getValue() 91 | } 92 | 93 | public saveCode() { 94 | const isSharedCodeRepository = this.repository instanceof SharedCodeRepository 95 | 96 | if (isSharedCodeRepository) { 97 | this.repository = new LocalCodeRepository() 98 | } 99 | 100 | this.repository.saveCode(this.getCode()) 101 | } 102 | 103 | public getRunnableCodeSnippet(runConfiguration: RunConfigurationManager): RunnableCodeSnippet { 104 | return new RunnableCodeSnippet(this.getCode(), runConfiguration.buildArguments, runConfiguration.runArguments, toSharedRunConfiguration(runConfiguration.configuration)) 105 | } 106 | 107 | public clear() { 108 | this.setCode("") 109 | } 110 | 111 | public setTheme(theme: ITheme) { 112 | this.editor.setOption("theme", theme.name()) 113 | } 114 | 115 | public showCompletion() { 116 | this.editor.execCommand("autocomplete") 117 | } 118 | 119 | public refresh() { 120 | this.editor.refresh() 121 | } 122 | 123 | public hide() { 124 | const realEditorElement = this.textAreaElement.parentElement as HTMLElement 125 | console.log(realEditorElement) 126 | if (realEditorElement !== undefined) { 127 | realEditorElement.style.display = "none" 128 | } 129 | 130 | const editorsElement = realEditorElement.parentElement 131 | editorsElement?.classList?.remove("two-editors") 132 | } 133 | 134 | public show() { 135 | const realEditorElement = this.textAreaElement.parentElement as HTMLElement 136 | console.log(realEditorElement) 137 | if (realEditorElement !== undefined) { 138 | realEditorElement.style.display = "grid" 139 | } 140 | 141 | const editorsElement = realEditorElement.parentElement 142 | editorsElement?.classList?.add("two-editors") 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /www/src/CodeRunner/CodeRunner.ts: -------------------------------------------------------------------------------- 1 | import {SharedCodeRunConfiguration} from "../Repositories"; 2 | 3 | type RunCodeResponse = { 4 | output: string 5 | buildOutput: string 6 | error: string 7 | } 8 | 9 | type RetrieveCgenCodeResponse = { 10 | cgenCode: string 11 | exitCode: number 12 | buildOutput: string 13 | error: string 14 | } 15 | 16 | type FormatCodeResponse = { 17 | output: string 18 | error: string 19 | } 20 | 21 | export type ShareCodeResponse = { 22 | hash: string 23 | error: string 24 | } 25 | 26 | type CreateBugResponse = { 27 | link: string 28 | error: string 29 | } 30 | 31 | type VersionResponse = { 32 | version: string 33 | error: string 34 | } 35 | 36 | export class RunnableCodeSnippet { 37 | constructor( 38 | public code: string, 39 | public buildArguments: string[], 40 | public runArguments: string[], 41 | public runConfiguration: SharedCodeRunConfiguration, 42 | ) { 43 | } 44 | 45 | public toFormData(): FormData { 46 | const data = new FormData() 47 | data.append("code", this.code) 48 | data.append("build-arguments", this.buildArguments.join(" ")) 49 | data.append("run-arguments", this.runArguments.join(" ")) 50 | data.append("run-configuration", this.runConfiguration.toString()) 51 | return data 52 | } 53 | } 54 | 55 | /** 56 | * CodeRunner describes how to run, format, and share code. 57 | */ 58 | export class CodeRunner { 59 | public static runCode(snippet: RunnableCodeSnippet): Promise { 60 | return fetch("/run", { 61 | method: "post", 62 | body: snippet.toFormData(), 63 | }) 64 | .then(resp => { 65 | if (resp.status != 200) { 66 | throw new Error(CodeRunner.buildErrorMessage("run", resp)) 67 | } 68 | 69 | return resp 70 | }) 71 | .then(resp => resp.json()) 72 | .then(data => data as RunCodeResponse) 73 | } 74 | 75 | public static runTest(snippet: RunnableCodeSnippet): Promise { 76 | return fetch("/run_test", { 77 | method: "post", 78 | body: snippet.toFormData(), 79 | }) 80 | .then(resp => { 81 | if (resp.status != 200) { 82 | throw new Error(CodeRunner.buildErrorMessage("test", resp)) 83 | } 84 | 85 | return resp 86 | }) 87 | .then(resp => resp.json()) 88 | .then(data => data as RunCodeResponse) 89 | } 90 | 91 | public static retrieveCgenCode(snippet: RunnableCodeSnippet): Promise { 92 | return fetch("/cgen", { 93 | method: "post", 94 | body: snippet.toFormData(), 95 | }) 96 | .then(resp => { 97 | if (resp.status != 200) { 98 | throw new Error(CodeRunner.buildErrorMessage("cgen", resp)) 99 | } 100 | 101 | return resp 102 | }) 103 | .then(resp => resp.json()) 104 | .then(data => data as RetrieveCgenCodeResponse) 105 | } 106 | 107 | public static formatCode(snippet: RunnableCodeSnippet): Promise { 108 | return fetch("/format", { 109 | method: "post", 110 | body: snippet.toFormData(), 111 | }) 112 | .then(resp => resp.json()) 113 | .then(data => data as FormatCodeResponse) 114 | } 115 | 116 | public static shareCode(snippet: RunnableCodeSnippet): Promise { 117 | return fetch("/share", { 118 | method: "post", 119 | body: snippet.toFormData(), 120 | }) 121 | .then(resp => { 122 | if (resp.status != 200) { 123 | throw new Error(CodeRunner.buildErrorMessage("share", resp)) 124 | } 125 | 126 | return resp 127 | }) 128 | .then(resp => resp.json()) 129 | .then(data => data as ShareCodeResponse) 130 | } 131 | 132 | public static createBugUrl(snippet: RunnableCodeSnippet): Promise { 133 | return fetch("/create_bug_url", { 134 | method: "post", 135 | body: snippet.toFormData(), 136 | }) 137 | .then(resp => { 138 | if (resp.status != 200) { 139 | throw new Error(CodeRunner.buildErrorMessage("create_bug_url", resp)) 140 | } 141 | 142 | return resp 143 | }) 144 | .then(resp => resp.json()) 145 | .then(data => data as CreateBugResponse) 146 | } 147 | 148 | public static getVlangVersion(): Promise { 149 | return fetch("/version", { 150 | method: "post", 151 | }) 152 | .then(resp => { 153 | if (resp.status != 200) { 154 | throw new Error(CodeRunner.buildErrorMessage("version", resp)) 155 | } 156 | 157 | return resp 158 | }) 159 | .then(resp => resp.json()) 160 | .then(data => data as VersionResponse) 161 | } 162 | 163 | private static buildErrorMessage(kind: string, response: Response): string { 164 | const base = `Failed to invoke \`/${kind}\` endpoint` 165 | const responseStatus = response.status.toString() + " " + response.statusText 166 | return `${base}: ${responseStatus}` 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /www/src/ThemeManager/ThemeManager.ts: -------------------------------------------------------------------------------- 1 | import {QueryParams} from "../QueryParams"; 2 | import {ITheme, Dark, Light} from "../themes"; 3 | 4 | type ThemeCallback = (newTheme: ITheme) => void; 5 | 6 | /** 7 | * ThemeManager is responsible for managing the theme of the playground. 8 | * It will register a callback to the change theme button and will update the 9 | * theme when the user clicks on the button. 10 | * It will also update the theme when the user changes the theme in the URL. 11 | * 12 | * @param queryParams The query params of the URL. 13 | * @param changeThemeButtons The button to change the theme or null. 14 | * 15 | * @example 16 | * const changeThemeButtons = document.querySelector('.js-change-theme') 17 | * const queryParams = new QueryParams(window.location.search); 18 | * const themeManager = new ThemeManager(queryParams, changeThemeButtons) 19 | * 20 | * themeManager.registerOnChange((theme) => { 21 | * // Do something with the theme 22 | * }) 23 | */ 24 | export class ThemeManager { 25 | private static readonly QUERY_PARAM_NAME = "theme" 26 | private static readonly LOCAL_STORAGE_KEY = "theme" 27 | 28 | private themes: ITheme[] = [new Dark(), new Light()] 29 | private currentTheme: ITheme | null = null 30 | private onChange: ThemeCallback[] = [] 31 | private readonly queryParams: QueryParams 32 | private readonly changeThemeButtons: NodeListOf | null = null 33 | private readonly predefinedTheme: ITheme | null = null 34 | private fromQueryParam: boolean = false 35 | 36 | constructor(queryParams: QueryParams, predefinedTheme: ITheme | null = null) { 37 | this.queryParams = queryParams 38 | this.predefinedTheme = predefinedTheme 39 | this.changeThemeButtons = document.querySelectorAll(".js-change-theme__action") 40 | } 41 | 42 | public registerOnChange(callback: ThemeCallback): void { 43 | this.onChange.push(callback) 44 | } 45 | 46 | public loadTheme(): void { 47 | const themeFromQuery = this.queryParams.getURLParameter(ThemeManager.QUERY_PARAM_NAME) 48 | if (themeFromQuery !== null && themeFromQuery !== undefined) { 49 | this.fromQueryParam = true 50 | const theme = this.findTheme(themeFromQuery) 51 | this.turnTheme(theme) 52 | return 53 | } 54 | 55 | const themeFromLocalStorage = window.localStorage.getItem(ThemeManager.LOCAL_STORAGE_KEY) 56 | if (themeFromLocalStorage !== null && themeFromLocalStorage !== undefined) { 57 | const theme = this.findTheme(themeFromLocalStorage) 58 | this.turnTheme(theme) 59 | return 60 | } 61 | 62 | if (this.predefinedTheme !== null && this.predefinedTheme !== undefined) { 63 | this.turnTheme(this.predefinedTheme) 64 | return 65 | } 66 | 67 | const preferDark = window.matchMedia("(prefers-color-scheme: dark)").matches; 68 | const defaultTheme = preferDark ? new Dark() : new Light(); 69 | 70 | this.turnTheme(defaultTheme) 71 | } 72 | 73 | private findTheme(themeFromLocalStorage: string) { 74 | let foundThemes = this.themes.filter(theme => theme.name() === themeFromLocalStorage) 75 | let theme = foundThemes[0] 76 | if (foundThemes.length == 0) { 77 | theme = new Dark() 78 | } 79 | return theme 80 | } 81 | 82 | private turnTheme(theme: ITheme): void { 83 | this.currentTheme = theme 84 | this.onChange.forEach(callback => callback(theme)) 85 | 86 | if (this.changeThemeButtons !== null) { 87 | this.changeThemeButtons.forEach((button) => { 88 | const svgSun = button.querySelector(".sun") as HTMLElement 89 | const svgMoon = button.querySelector(".moon") as HTMLElement 90 | if (svgSun !== null && svgMoon !== null) { 91 | if (theme.name() === "dark") { 92 | svgSun.style.display = "block" 93 | svgMoon.style.display = "none" 94 | } else { 95 | svgSun.style.display = "none" 96 | svgMoon.style.display = "block" 97 | } 98 | } 99 | }) 100 | } 101 | 102 | const html = document.querySelector("html")! 103 | html.setAttribute("data-theme", theme.name()) 104 | 105 | if (!this.fromQueryParam) { 106 | // Don't update saved theme state if we're loading from query param. 107 | window.localStorage.setItem(ThemeManager.LOCAL_STORAGE_KEY, theme.name()) 108 | } 109 | 110 | if (this.fromQueryParam) { 111 | // We update the query param only if we loaded from it. 112 | // If we don't change, then the user can change the theme and then reload the page. 113 | // In this case, the page will load with the theme from the URL, and the user 114 | // will think that his theme change has not been saved (and will not be saved 115 | // until he removes the theme from the URL). 116 | // To avoid this, we update the URL if the user changes theme. 117 | this.queryParams.updateURLParameter(ThemeManager.QUERY_PARAM_NAME, theme.name()) 118 | } 119 | } 120 | 121 | public turnDarkTheme(): void { 122 | this.turnTheme(new Dark()) 123 | } 124 | 125 | public turnLightTheme(): void { 126 | this.turnTheme(new Light()) 127 | } 128 | 129 | public toggleTheme(): void { 130 | if (!this.currentTheme) { 131 | return 132 | } 133 | 134 | if (this.currentTheme.name() === "light") { 135 | this.turnDarkTheme() 136 | } else { 137 | this.turnLightTheme() 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /server/runners/runner.v: -------------------------------------------------------------------------------- 1 | module runners 2 | 3 | import os 4 | import isolate 5 | import models 6 | import logger 7 | import srackham.pcre2 8 | 9 | pub struct RunResult { 10 | pub: 11 | output string 12 | build_output string 13 | } 14 | 15 | // run runs the code in sandbox. 16 | pub fn run(snippet models.CodeStorage) !RunResult { 17 | return run_in_sandbox(snippet, false) 18 | } 19 | 20 | // test runs the code as tests in sandbox. 21 | pub fn test(snippet models.CodeStorage) !RunResult { 22 | return run_in_sandbox(snippet, true) 23 | } 24 | 25 | // get_output run the code in sandbox and returns the output. 26 | pub fn get_output(snippet models.CodeStorage) !string { 27 | box_path, box_id := isolate.init_sandbox() 28 | defer { 29 | isolate.cleanup_sandbox(box_id) 30 | } 31 | 32 | file := 'code.v' 33 | os.write_file(os.join_path(box_path, file), snippet.code) or { 34 | return error('Failed to write code to sandbox.') 35 | } 36 | 37 | build_res := isolate.execute(' 38 | isolate 39 | --box-id=${box_id} 40 | --dir=${@VEXEROOT} 41 | --env=HOME=/box 42 | --processes=${max_run_processes_and_threads} 43 | --mem=${max_compiler_memory_in_kb} 44 | --wall-time=${wall_time_in_seconds} 45 | --run 46 | -- 47 | ${@VEXEROOT}/v -cflags -DGC_MARKERS=1 -no-parallel -no-retry-compilation -g ${prepare_user_arguments(snippet.build_arguments)} ${file} 48 | ') 49 | build_output := build_res.output.trim_right('\n') 50 | 51 | logger.log(snippet.code, build_output) or { eprintln('[WARNING] Failed to log code.') } 52 | 53 | if build_res.exit_code != 0 { 54 | return error(prettify(build_output)) 55 | } 56 | 57 | run_res := isolate.execute(' 58 | isolate 59 | --box-id=${box_id} 60 | --dir=${@VEXEROOT} 61 | --env=HOME=/box 62 | --processes=${max_run_processes_and_threads} 63 | --mem=${max_run_memory_in_kb} 64 | --time=${run_time_in_seconds} 65 | --wall-time=${wall_time_in_seconds} 66 | --run 67 | -- 68 | ./code ${prepare_user_arguments(snippet.run_arguments)} 69 | ') 70 | 71 | is_reached_resource_limit := run_res.exit_code == 1 72 | && run_res.output.contains('Resource temporarily unavailable') 73 | is_out_of_memory := run_res.exit_code == 1 74 | && run_res.output.contains('GC Warning: Out of Memory!') 75 | 76 | if is_reached_resource_limit || is_out_of_memory { 77 | return error('The program reached the resource limit assigned to it.') 78 | } 79 | 80 | mut run_res_result := run_res.output.trim_right('\n') 81 | $if !local ? { 82 | // isolate output message like "OK (0.033 sec real, 0.219 sec wall)" 83 | // so we need to remove it 84 | run_res_result = run_res_result.all_before_last('\n') + '\n' 85 | } 86 | 87 | return run_res_result 88 | } 89 | 90 | // run_in_sandbox is common function for running tests and code in sandbox. 91 | fn run_in_sandbox(snippet models.CodeStorage, as_test bool) !RunResult { 92 | box_path, box_id := isolate.init_sandbox() 93 | defer { 94 | isolate.cleanup_sandbox(box_id) 95 | } 96 | 97 | file := if as_test { 'code_test.v' } else { 'code.v' } 98 | 99 | os.write_file(os.join_path(box_path, file), snippet.code) or { 100 | return error('Failed to write code to sandbox.') 101 | } 102 | 103 | if as_test { 104 | run_res := isolate.execute(' 105 | isolate 106 | --box-id=${box_id} 107 | --dir=${@VEXEROOT} 108 | --env=HOME=/box 109 | --processes=${max_run_processes_and_threads} 110 | --mem=${max_compiler_memory_in_kb} 111 | --wall-time=${wall_time_in_seconds} 112 | --run 113 | -- 114 | ${@VEXEROOT}/v -cflags -DGC_MARKERS=1 -no-parallel -no-retry-compilation -g ${prepare_user_arguments(snippet.build_arguments)} test ${file} 115 | ') 116 | run_output := run_res.output.trim_right('\n') 117 | 118 | logger.log(snippet.code, run_output) or { eprintln('[WARNING] Failed to log code.') } 119 | 120 | return RunResult{ 121 | output: prettify(run_output) 122 | build_output: '' 123 | } 124 | } 125 | 126 | build_res := isolate.execute(' 127 | isolate 128 | --box-id=${box_id} 129 | --dir=${@VEXEROOT} 130 | --env=HOME=/box 131 | --processes=${max_run_processes_and_threads} 132 | --mem=${max_compiler_memory_in_kb} 133 | --wall-time=${wall_time_in_seconds} 134 | --run 135 | -- 136 | ${@VEXEROOT}/v -cflags -DGC_MARKERS=1 -no-parallel -no-retry-compilation -g ${prepare_user_arguments(snippet.build_arguments)} ${file} 137 | ') 138 | build_output := build_res.output.trim_right('\n') 139 | 140 | logger.log(snippet.code, build_output) or { eprintln('[WARNING] Failed to log code.') } 141 | 142 | if build_res.exit_code != 0 { 143 | return error(prettify(build_output)) 144 | } 145 | 146 | run_res := isolate.execute(' 147 | isolate 148 | --box-id=${box_id} 149 | --dir=${@VEXEROOT} 150 | --env=HOME=/box 151 | --processes=${max_run_processes_and_threads} 152 | --mem=${max_run_memory_in_kb} 153 | --time=${run_time_in_seconds} 154 | --wall-time=${wall_time_in_seconds} 155 | --run 156 | -- 157 | ./code ${prepare_user_arguments(snippet.run_arguments)} 158 | ') 159 | 160 | is_reached_resource_limit := run_res.exit_code == 1 161 | && run_res.output.contains('Resource temporarily unavailable') 162 | is_out_of_memory := run_res.exit_code == 1 163 | && run_res.output.contains('GC Warning: Out of Memory!') 164 | 165 | if is_reached_resource_limit || is_out_of_memory { 166 | return error('The program reached the resource limit assigned to it.') 167 | } 168 | 169 | mut run_res_result := run_res.output.trim_right('\n') 170 | mut run_res_result_lines := run_res_result.split_into_lines() 171 | 172 | // isolate output message like "OK (0.033 sec real, 0.219 sec wall)" 173 | // we want to remove it 174 | if run_res_result_lines.last().starts_with('OK (') { 175 | run_res_result_lines = unsafe { run_res_result_lines#[..-1] } 176 | run_res_result = run_res_result_lines.join('\n') 177 | } 178 | 179 | return RunResult{ 180 | output: prettify(run_res_result) 181 | build_output: prettify(build_output) 182 | } 183 | } 184 | 185 | const regex_arguments_validator = pcre2.compile('[^\\w\\d\\-=]') or { panic(err) } 186 | 187 | fn prepare_user_arguments(args string) string { 188 | return runners.regex_arguments_validator.replace_all(args, ' ') 189 | } 190 | -------------------------------------------------------------------------------- /www/styles/tools.scss: -------------------------------------------------------------------------------- 1 | .tools { 2 | width: 100%; 3 | height: 65px; 4 | color: #fff; 5 | 6 | -webkit-backdrop-filter: saturate(50%) blur(5px); 7 | backdrop-filter: saturate(50%) blur(5px); 8 | background: var(--tools-background-color); 9 | z-index: 10; 10 | 11 | overflow-x: scroll; 12 | overflow-y: visible; 13 | 14 | &::-webkit-scrollbar { 15 | height: 4px !important; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background-color: transparent; 20 | } 21 | 22 | .container { 23 | height: 100%; 24 | display: grid; 25 | grid-template-columns: max-content auto max-content; 26 | align-items: center; 27 | padding: 5px 15px 5px 40px; 28 | 29 | .additional-arguments { 30 | display: grid; 31 | grid-template-columns: max-content max-content; 32 | grid-column-gap: 10px; 33 | 34 | margin-left: 10px; 35 | 36 | .additional-arguments-input { 37 | display: grid; 38 | grid-template-rows: max-content auto; 39 | grid-row-gap: 3px; 40 | 41 | height: 100%; 42 | 43 | label { 44 | font-size: 11px; 45 | color: var(--secondary-color); 46 | } 47 | 48 | input { 49 | height: 30px; 50 | padding: 0 10px; 51 | 52 | font-size: 13px; 53 | font-family: ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo, monospace; 54 | border: 1px solid var(--border-color); 55 | outline: none; 56 | 57 | color: var(--text-color); 58 | background-color: var(--tools-background-color); 59 | 60 | &:focus { 61 | border: 1px solid var(--brand-color); 62 | } 63 | } 64 | } 65 | 66 | } 67 | 68 | .buttons { 69 | display: grid; 70 | height: 100%; 71 | grid-template-columns: repeat(4, max-content); 72 | align-content: center; 73 | grid-column-gap: 8px; 74 | 75 | margin-left: 20px; 76 | } 77 | 78 | .buttons button { 79 | height: 38px; 80 | padding: 8px 20px 6px 17px; 81 | border: none; 82 | border-radius: 12px; 83 | background-color: transparent; 84 | color: var(--text-color); 85 | font-size: 15px; 86 | line-height: 1.5; 87 | display: grid; 88 | grid-template-columns: max-content max-content; 89 | grid-column-gap: 8px; 90 | transition: background-color 75ms; 91 | 92 | &:hover { 93 | cursor: pointer; 94 | background-color: var(--tools-button-hover); 95 | } 96 | 97 | .icon { 98 | path { 99 | fill: var(--text-color); 100 | } 101 | 102 | rect { 103 | fill: var(--text-color); 104 | } 105 | 106 | padding-top: 2px; 107 | 108 | .run-icon { 109 | fill: #fff; 110 | } 111 | } 112 | } 113 | 114 | .run-button-select { 115 | height: 38px; 116 | border: none; 117 | color: #fff; 118 | font-size: 15px; 119 | line-height: 1.4; 120 | display: grid; 121 | transition: background-color 150ms ease 100ms; 122 | margin-left: 20px; 123 | grid-template-columns: max-content max-content max-content; 124 | 125 | button { 126 | padding: 8px 20px 6px 17px; 127 | border-radius: 8px 0 0 8px; 128 | background-color: #3474f0; 129 | border: none; 130 | color: #fff; 131 | font-size: 15px; 132 | line-height: 1.5; 133 | display: grid; 134 | grid-template-columns: max-content max-content; 135 | grid-column-gap: 8px; 136 | align-items: center; 137 | 138 | &:hover { 139 | cursor: pointer; 140 | background-color: #2a5ed0; 141 | } 142 | 143 | &:focus { 144 | outline: none; 145 | } 146 | 147 | .label { 148 | margin-top: -2px; 149 | } 150 | } 151 | 152 | .open-run-select { 153 | border-radius: 0 8px 8px 0; 154 | padding: 8px 7px 4px 7px; 155 | background-color: #3474f0; 156 | 157 | &:hover { 158 | cursor: pointer; 159 | background-color: #2a5ed0; 160 | } 161 | 162 | .open-icon { 163 | padding: 3px 0 0 0; 164 | } 165 | } 166 | } 167 | 168 | button.theme { 169 | padding-top: 3px; 170 | grid-template-columns: max-content; 171 | margin-left: 20px; 172 | 173 | &:hover { 174 | background-color: transparent; 175 | 176 | .icon { 177 | .theme-icon { 178 | path { 179 | fill: #9b9b9b; 180 | } 181 | } 182 | } 183 | } 184 | 185 | .theme-icon { 186 | width: 22px; 187 | 188 | path { 189 | fill: var(--text-color); 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | .run-configurations-list { 197 | width: 250px; 198 | position: absolute; 199 | top: 130px; 200 | right: 20px; 201 | background-color: var(--tools-background-color); 202 | border: 1px solid var(--select-header-border); 203 | border-radius: 8px; 204 | z-index: 1000; 205 | padding: 15px 10px; 206 | box-shadow: 0 15px 8px -10px rgba(0, 0, 0, 0.2); 207 | 208 | h4 { 209 | margin: 0 0 10px; 210 | padding-left: 10px; 211 | color: #6e737b; 212 | font-size: 13px; 213 | font-weight: 700; 214 | } 215 | 216 | .configuration { 217 | padding: 6px 7px 4px; 218 | border-radius: 6px; 219 | font-size: 15px; 220 | display: grid; 221 | grid-template-columns: max-content max-content; 222 | grid-column-gap: 8px; 223 | color: var(--text-color); 224 | 225 | &:hover { 226 | cursor: pointer; 227 | background-color: var(--configuration-item-hover); 228 | } 229 | 230 | span { 231 | margin-top: -2px; 232 | color: var(--text-color); 233 | } 234 | 235 | svg { 236 | margin-top: -1px; 237 | } 238 | } 239 | } 240 | 241 | .run-configurations-list.hidden { 242 | display: none; 243 | } 244 | 245 | .run-configurations-list-overlay { 246 | display: none; 247 | width: 100vw; 248 | height: 100vh; 249 | position: fixed; 250 | top: 0; 251 | left: 0; 252 | z-index: 100; 253 | } 254 | 255 | .run-configurations-list-overlay.opened { 256 | display: block; 257 | } 258 | -------------------------------------------------------------------------------- /.devcontainer/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | 7 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] 8 | 9 | INSTALL_ZSH=${1:-"true"} 10 | USERNAME=${2:-"vscode"} 11 | USER_UID=${3:-1000} 12 | USER_GID=${4:-1000} 13 | UPGRADE_PACKAGES=${5:-"true"} 14 | 15 | set -e 16 | 17 | if [ "$(id -u)" -ne 0 ]; then 18 | echo -e 'Script must be run a root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 19 | exit 1 20 | fi 21 | 22 | # Treat a user name of "none" as root 23 | if [ "${USERNAME}" = "none" ] || [ "${USERNAME}" = "root" ]; then 24 | USERNAME=root 25 | USER_UID=0 26 | USER_GID=0 27 | fi 28 | 29 | # Load markers to see which steps have already run 30 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 31 | if [ -f "${MARKER_FILE}" ]; then 32 | echo "Marker file found:" 33 | cat "${MARKER_FILE}" 34 | source "${MARKER_FILE}" 35 | fi 36 | 37 | # Ensure apt is in non-interactive to avoid prompts 38 | export DEBIAN_FRONTEND=noninteractive 39 | 40 | # Fn to call apt-get if needed 41 | apt-get-update-if-needed() 42 | { 43 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 44 | echo "Running apt-get update..." 45 | apt-get update 46 | else 47 | echo "Skipping apt-get update." 48 | fi 49 | } 50 | 51 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 52 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 53 | apt-get-update-if-needed 54 | 55 | PACKAGE_LIST="apt-utils \ 56 | git \ 57 | openssh-client \ 58 | less \ 59 | iproute2 \ 60 | procps \ 61 | curl \ 62 | wget \ 63 | unzip \ 64 | nano \ 65 | jq \ 66 | lsb-release \ 67 | ca-certificates \ 68 | apt-transport-https \ 69 | dialog \ 70 | gnupg2 \ 71 | libc6 \ 72 | libgcc1 \ 73 | libgssapi-krb5-2 \ 74 | libicu[0-9][0-9] \ 75 | liblttng-ust0 \ 76 | libstdc++6 \ 77 | zlib1g \ 78 | locales \ 79 | sudo" 80 | 81 | # Install libssl1.1 if available 82 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 83 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.1" 84 | fi 85 | 86 | # Install appropriate version of libssl1.0.x if available 87 | LIBSSL=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 88 | if [ "$(echo "$LIBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 89 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 90 | # Debian 9 91 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.2" 92 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 93 | # Ubuntu 18.04, 16.04, earlier 94 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.0" 95 | fi 96 | fi 97 | 98 | echo "Packages to verify are installed: ${PACKAGE_LIST}" 99 | apt-get -y install --no-install-recommends ${PACKAGE_LIST} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 100 | 101 | PACKAGES_ALREADY_INSTALLED="true" 102 | fi 103 | 104 | # Get to latest versions of all packages 105 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 106 | apt-get-update-if-needed 107 | apt-get -y upgrade --no-install-recommends 108 | apt-get autoremove -y 109 | fi 110 | 111 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 112 | # Common need for both applications and things like the agnoster ZSH theme. 113 | if [ "${LOCALE_ALREADY_SET}" != "true" ]; then 114 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 115 | locale-gen 116 | LOCALE_ALREADY_SET="true" 117 | fi 118 | 119 | # Create or update a non-root user to match UID/GID - see https://aka.ms/vscode-remote/containers/non-root-user. 120 | if id -u $USERNAME > /dev/null 2>&1; then 121 | # User exists, update if needed 122 | if [ "$USER_GID" != "$(id -G $USERNAME)" ]; then 123 | groupmod --gid $USER_GID $USERNAME 124 | usermod --gid $USER_GID $USERNAME 125 | fi 126 | if [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 127 | usermod --uid $USER_UID $USERNAME 128 | fi 129 | else 130 | # Create user 131 | groupadd --gid $USER_GID $USERNAME 132 | useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME 133 | fi 134 | 135 | # Add add sudo support for non-root user 136 | if [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 137 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 138 | chmod 0440 /etc/sudoers.d/$USERNAME 139 | EXISTING_NON_ROOT_USER="${USERNAME}" 140 | fi 141 | 142 | # Ensure ~/.local/bin is in the PATH for root and non-root users for bash. (zsh is later) 143 | if [ "${DOT_LOCAL_ALREADY_ADDED}" != "true" ]; then 144 | echo "export PATH=\$PATH:\$HOME/.local/bin" | tee -a /root/.bashrc >> /home/$USERNAME/.bashrc 145 | chown $USER_UID:$USER_GID /home/$USERNAME/.bashrc 146 | DOT_LOCAL_ALREADY_ADDED="true" 147 | fi 148 | 149 | # Optionally install and configure zsh 150 | if [ "${INSTALL_ZSH}" = "true" ] && [ ! -d "/root/.oh-my-zsh" ] && [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 151 | apt-get-update-if-needed 152 | apt-get install -y zsh 153 | curl -fsSLo- https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh | bash 2>&1 154 | echo "export PATH=\$PATH:\$HOME/.local/bin" >> /root/.zshrc 155 | if [ "${USERNAME}" != "root" ]; then 156 | cp -fR /root/.oh-my-zsh /home/$USERNAME 157 | cp -f /root/.zshrc /home/$USERNAME 158 | sed -i -e "s/\/root\/.oh-my-zsh/\/home\/$USERNAME\/.oh-my-zsh/g" /home/$USERNAME/.zshrc 159 | chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc 160 | fi 161 | ZSH_ALREADY_INSTALLED="true" 162 | fi 163 | 164 | # Write marker file 165 | mkdir -p "$(dirname "${MARKER_FILE}")" 166 | echo -e "\ 167 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 168 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 169 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 170 | DOT_LOCAL_ALREADY_ADDED=${DOT_LOCAL_ALREADY_ADDED}\n\ 171 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 172 | -------------------------------------------------------------------------------- /www/src/Terminal/Terminal.ts: -------------------------------------------------------------------------------- 1 | type OnCloseCallback = () => void 2 | type OnWriteCallback = (text: string) => void 3 | type FilterCallback = (text: string) => boolean 4 | 5 | export class Terminal { 6 | private readonly element: HTMLElement 7 | private onClose: OnCloseCallback | null = null 8 | private onWrite: OnWriteCallback | null = null 9 | private filters: FilterCallback[] = [] 10 | private tabsElement: HTMLElement; 11 | 12 | constructor(element: HTMLElement) { 13 | this.element = element 14 | this.tabsElement = this.element.querySelector(".js-terminal__tabs") as HTMLElement 15 | 16 | this.attachResizeHandler(element) 17 | } 18 | 19 | public registerCloseHandler(handler: () => void) { 20 | this.onClose = handler 21 | } 22 | 23 | public registerWriteHandler(handler: (text: string) => void) { 24 | this.onWrite = handler 25 | } 26 | 27 | public registerFilter(filter: FilterCallback) { 28 | this.filters.push(filter) 29 | } 30 | 31 | public getTabElement(name: string): HTMLElement | null { 32 | return this.tabsElement.querySelector(`input[value='${name}']`) as HTMLElement 33 | } 34 | 35 | public openTab(name: string) { 36 | const tabsInput = this.getTabElement(name) 37 | if (tabsInput !== null) { 38 | (tabsInput as HTMLInputElement).checked = true 39 | tabsInput.dispatchEvent(new Event("change")) 40 | } 41 | } 42 | 43 | public openOutputTab() { 44 | this.openTab("output") 45 | } 46 | 47 | public openBuildLogTab() { 48 | this.openTab("build-log") 49 | } 50 | 51 | public write(text: string) { 52 | this.writeImpl(text, true) 53 | } 54 | 55 | public writeOutput(text: string) { 56 | this.writeImpl(text, false) 57 | } 58 | 59 | private writeImpl(text: string, buildLog: boolean) { 60 | const lines = text.split("\n") 61 | const outputElement = this.getTerminalOutputElement(buildLog) 62 | const filteredLines = lines.filter(line => this.filters.every(filter => filter(line))) 63 | const newText = filteredLines.map(this.highlightLine).join("\n") 64 | outputElement.innerHTML += newText + "\n" 65 | 66 | if (this.onWrite !== null) { 67 | this.onWrite(text) 68 | } 69 | } 70 | 71 | private highlightLine(line: string): string { 72 | // code.v:4:30: error: `sss` evaluated but not used 73 | if (line.startsWith('code.v:') || line.startsWith('code_test.v:')) { 74 | const parts = line.split(':') 75 | const name = parts[0] 76 | const lineNo = parseInt(parts[1]) 77 | const columnNo = parseInt(parts[2]) 78 | const kind = parts[3].trim() 79 | const message = parts.slice(4).join(':') 80 | return `${name}:${lineNo}:${columnNo}: ${kind}:${message}` 81 | } 82 | 83 | if (line.trim().startsWith("FAIL") && line.includes('code_test.v')) { 84 | const data = line.trim().substring(4) 85 | return `FAIL ${data}` 86 | } 87 | 88 | if (line.trim().startsWith("OK") && line.includes('code_test.v')) { 89 | const data = line.trim().substring(2) 90 | return `OK ${data}` 91 | } 92 | 93 | return line 94 | } 95 | 96 | 97 | public clear() { 98 | this.getTerminalOutputElement(false).innerHTML = "" 99 | this.getTerminalOutputElement(true).innerHTML = "" 100 | } 101 | 102 | public mount() { 103 | const closeButton = this.element.querySelector(".js-terminal__close-buttom") as HTMLElement 104 | if (closeButton === null || closeButton === undefined || this.onClose === null) { 105 | return 106 | } 107 | 108 | closeButton.addEventListener("click", this.onClose) 109 | 110 | const tabsElement = this.element.querySelector(".js-terminal__tabs") as HTMLElement 111 | const tabsInputs = tabsElement.querySelectorAll("input") 112 | tabsInputs.forEach(input => { 113 | input.addEventListener("change", () => { 114 | const value = input.value 115 | if (value === "output") { 116 | this.getTerminalOutputElement(false).style.display = "block" 117 | this.getTerminalOutputElement(true).style.display = "none" 118 | } else { 119 | this.getTerminalOutputElement(false).style.display = "none" 120 | this.getTerminalOutputElement(true).style.display = "block" 121 | } 122 | }) 123 | }) 124 | } 125 | 126 | private getTerminalOutputElement(buildLog: boolean): HTMLElement { 127 | if (buildLog) { 128 | return this.element.querySelector(".js-terminal__build-log") as HTMLElement 129 | } 130 | return this.element.querySelector(".js-terminal__output") as HTMLElement 131 | } 132 | 133 | private attachResizeHandler(element: HTMLElement) { 134 | const header = element.querySelector('.header'); 135 | if (!header) return; 136 | 137 | let mouseDown = false; 138 | header.addEventListener('mousedown', (e) => { 139 | const target = e.target as Element; 140 | if (target.tagName.toLowerCase() === 'label') return; 141 | mouseDown = true; 142 | document.body.classList.add('dragging'); 143 | }); 144 | 145 | header.addEventListener('touchstart', (e) => { 146 | const target = e.target as Element; 147 | if (target.tagName.toLowerCase() === 'label') return; 148 | mouseDown = true; 149 | document.body.classList.add('dragging'); 150 | }) 151 | 152 | // @ts-ignore 153 | header.addEventListener('touchmove', (e: TouchEvent) => { 154 | if (!mouseDown) return; 155 | element.style.height = `${document.body.clientHeight - e.touches[0].clientY + header.clientHeight / 2}px`; 156 | e.preventDefault() 157 | }) 158 | 159 | document.addEventListener('mousemove', (e: MouseEvent) => { 160 | if (!mouseDown) return; 161 | element.style.height = `${document.body.clientHeight - e.clientY + header.clientHeight / 2}px`; 162 | }); 163 | 164 | document.addEventListener('mouseup', () => { 165 | mouseDown = false; 166 | document.body.classList.remove('dragging'); 167 | }); 168 | 169 | document.addEventListener('touchend', () => { 170 | mouseDown = false; 171 | document.body.classList.remove('dragging'); 172 | }) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /www/src/v-hint.ts: -------------------------------------------------------------------------------- 1 | import {atoms, builtinTypes, keywords, pseudoKeywords} from "./v" 2 | import {Editor, Position, Token} from "codemirror" 3 | 4 | // @ts-ignore 5 | const Pos = CodeMirror.Pos 6 | 7 | /** 8 | * Describe a completion variant. 9 | */ 10 | interface CompletionVariant { 11 | /** 12 | * The text to be matched and inserted. 13 | */ 14 | text: string, 15 | 16 | /** 17 | * The text to be displayed in the completion list. 18 | */ 19 | displayText: string, 20 | 21 | /** 22 | * The class name to be applied to the completion list item. 23 | * Used to style the completion list item. 24 | */ 25 | className: string 26 | } 27 | 28 | /** 29 | * Describe a completions variants. 30 | */ 31 | interface CompletionVariants { 32 | from: Position; 33 | to: Position; 34 | list: Array; 35 | } 36 | 37 | /** 38 | * Some base builtin modules. 39 | */ 40 | const baseModules = [ 41 | "arrays", 42 | "benchmark", "bitfield", 43 | "cli", "clipboard", "compress", "context", "crypto", 44 | "darwin", "datatypes", "dl", "dlmalloc", 45 | "encoding", "eventbus", 46 | "flag", "fontstash", 47 | "gg", "gx", 48 | "hash", 49 | "io", 50 | "js", "json", 51 | "log", 52 | "math", "mssql", "mysql", 53 | "net", 54 | "orm", "os", 55 | "pg", "picoev", "picohttpparser", 56 | "rand", "readline", "regex", "runtime", 57 | "semver", "sokol", "sqlite", "stbi", "strconv", "strings", "sync", "szip", 58 | "term", "time", "toml", 59 | "v", "vweb", 60 | "x", 61 | ] 62 | 63 | const baseAttributes = [ 64 | "params", "noinit", "required", "skip", "assert_continues", 65 | "unsafe", "manualfree", "heap", "nonnull", "primary", "inline", 66 | "direct_array_access", "live", "flag", "noinline", "noreturn", "typedef", "console", 67 | "sql", "table", "deprecated", "deprecated_after", "export", "callconv" 68 | ] 69 | 70 | const word = "[\\w_]+" 71 | // [noinit] 72 | export const simpleAttributesRegexp = new RegExp(`^(${baseAttributes.join("|")})]$`) 73 | 74 | // [key: value] 75 | const keyValue = `(${word}: ${word})` 76 | export const singleKeyValueAttributesRegexp = new RegExp(`^${keyValue}]$`) 77 | 78 | // [attr1; attr2] 79 | export const severalSingleKeyValueAttributesRegexp = new RegExp(`^(${baseAttributes.join("|")}(; ?)?){2,}]$`) 80 | 81 | // [key: value; key: value] 82 | export const keyValueAttributesRegexp = new RegExp(`^((${keyValue})(; )?){2,}]$`) 83 | 84 | // [if expr ?] 85 | export const ifAttributesRegexp = new RegExp(`^if ${word} \\??]`) 86 | 87 | function computeCompletionVariants(editor: Editor): CompletionVariants | null { 88 | // some additional information for the current token. 89 | let context: Token[] = [] 90 | // find the token at the cursor 91 | const cur = editor.getCursor() 92 | let token = editor.getTokenAt(cur) 93 | 94 | const knownImports = new Set() 95 | for (let i = 0; i < Math.min(editor.lineCount(), 10); i++) { 96 | const lineTokens = editor.getLineTokens(i).filter(tkn => tkn.type != null) 97 | if (lineTokens.length > 0 && lineTokens[0].string === "import") { 98 | knownImports.add(lineTokens[lineTokens.length - 1].string) 99 | } 100 | } 101 | 102 | const lineTokens = editor.getLineTokens(cur.line) 103 | if (lineTokens.length > 0 && lineTokens[0].string === "import") { 104 | // if the first token is "import", then we are in an import statement, 105 | // so add this information to context. 106 | context.push(lineTokens[0]) 107 | } 108 | 109 | const len = token.string.length 110 | const prevToken = editor.getTokenAt(Pos(cur.line, cur.ch - len)) 111 | if (token.string === ".") { 112 | context.push(token) 113 | } 114 | if (prevToken.string === ".") { 115 | context.push(prevToken) 116 | } 117 | 118 | if (/\b(?:string|comment)\b/.test(token.type ?? "")) return null 119 | 120 | // if it's not a 'word-style' token, ignore the token. 121 | if (!/^[\w$_]*$/.test(token.string)) { 122 | token = { 123 | start: cur.ch, end: cur.ch, string: "", state: token.state, 124 | type: token.string === "." ? "property" : null, 125 | } 126 | } else if (token.end > cur.ch) { 127 | token.end = cur.ch 128 | token.string = token.string.slice(0, cur.ch - token.start) 129 | } 130 | 131 | return { 132 | list: getCompletions(token, knownImports, context), 133 | from: Pos(cur.line, token.start), 134 | to: Pos(cur.line, token.end), 135 | } 136 | } 137 | 138 | function getCompletions(token: Token, knownImports: Set, context: Token[]): CompletionVariant[] { 139 | const variants: CompletionVariant[] = [] 140 | const tokenValue = token.string 141 | 142 | function addCompletionVariant(variant: CompletionVariant) { 143 | const variantText = variant.text 144 | 145 | // if no matching text, ignore 146 | if (!variantText.startsWith(tokenValue)) { 147 | return 148 | } 149 | 150 | const alreadyContains = variants.find((f) => f.text === variantText) 151 | if (!alreadyContains) { 152 | variants.push(variant) 153 | } 154 | } 155 | 156 | if (context && context.length) { 157 | const lastToken = context.pop() 158 | if (lastToken !== undefined) { 159 | if (lastToken.type === "keyword" && lastToken.string === "import") { 160 | baseModules.forEach((text) => { 161 | addCompletionVariant({ 162 | text: text, 163 | displayText: text, 164 | className: "completion-module", 165 | }) 166 | }) 167 | return variants 168 | } 169 | 170 | // disable completion after dot 171 | if (lastToken.string === ".") { 172 | return [] 173 | } 174 | } 175 | } 176 | 177 | knownImports.forEach((text) => { 178 | addCompletionVariant({ 179 | text: text, 180 | displayText: text, 181 | className: "completion-module", 182 | }) 183 | }) 184 | 185 | keywords.forEach((text) => { 186 | addCompletionVariant({ 187 | text: text + " ", 188 | displayText: text, 189 | className: "completion-keyword", 190 | }) 191 | }) 192 | 193 | pseudoKeywords.forEach((text) => { 194 | addCompletionVariant({ 195 | text: text + " ", 196 | displayText: text, 197 | className: "completion-keyword", 198 | }) 199 | }) 200 | 201 | atoms.forEach((text) => { 202 | addCompletionVariant({ 203 | text: text, 204 | displayText: text, 205 | className: "completion-atom", 206 | }) 207 | }) 208 | 209 | builtinTypes.forEach((text) => { 210 | addCompletionVariant({ 211 | text: text, 212 | displayText: text, 213 | className: "completion-type", 214 | }) 215 | }) 216 | 217 | return variants 218 | } 219 | 220 | const hintHelper = (editor: Editor) => computeCompletionVariants(editor) 221 | 222 | // @ts-ignore 223 | CodeMirror.registerHelper("hint", "v", hintHelper) 224 | -------------------------------------------------------------------------------- /www/styles/colors.scss: -------------------------------------------------------------------------------- 1 | html { 2 | // common colors 3 | --dark-gray: #1e1f22; 4 | --almost-black: #1a1a1a; 5 | 6 | --gray-22: #222226; 7 | --gray-24: #242424; 8 | --gray-32: #323438; 9 | --gray-3a: #3a3a3a; 10 | --gray-3b: #3b3c3c; 11 | --gray-5a: #5a5a5c; 12 | --gray-7e: #7e7e80; 13 | --gray-be: #bebebf; 14 | --gray-cb: #cbc9c9; 15 | --gray-ef: #efefef; 16 | --gray-ec: #ecedf1; 17 | --gray-e0: #e0e0e0; 18 | --gray-87: rgba(255, 255, 255, .87); 19 | --gray-f9: #f9f9f9; 20 | --dark-transparent-gray: rgba(25, 25, 28, .7); 21 | --background-transparent-gray: rgba(30, 31, 34, .7); 22 | --semi-dark-transparent-gray: rgba(0, 0, 0, 0.32); 23 | --light-transparent-gray: rgba(255, 255, 255, 0.05); 24 | --light-dark-transparent-gray: rgba(0, 0, 0, 0.05); 25 | --background-light-transparent-gray: rgba(255, 255, 255, .7); 26 | --light-white: #f3f3f3; 27 | --white: #fff; 28 | --black: #000; 29 | --blue: #3890fa; 30 | --dark-blue: #0969da; 31 | 32 | --light-salat-green: #AFBF7E; 33 | --light-green: rgb(77, 187, 95); 34 | --dark-transparent-green: rgba(77, 187, 95, 0.2); 35 | 36 | --orange: rgb(244, 92, 75); 37 | --dark-transparent-orange: rgba(244, 92, 74, 0.2); 38 | 39 | --brand-color: #ff6b00; 40 | --brand-color-dark: #cc5307; 41 | } 42 | 43 | // light theme 44 | html { 45 | // common colors 46 | --main-color: var(--white); 47 | 48 | --background-color: var(--main-color); 49 | --border-color: var(--gray-e0); 50 | --divider-color: var(--gray-e0); 51 | --text-color: var(--black); 52 | --secondary-color: var(--gray-7e); 53 | 54 | --scrollbar-thumb-color: var(--gray-ef); 55 | 56 | // header 57 | --header-background-color: var(--main-color); 58 | --header-transparent-background-color: var(--background-light-transparent-gray); 59 | 60 | // footer 61 | --footer-background-color: var(--main-color); 62 | 63 | // overlay 64 | --overlay-background-color: var(--semi-dark-transparent-gray); 65 | 66 | //