├── .prettierignore ├── .eslintignore ├── data ├── workbench.gif ├── app.service ├── screenshots │ ├── code.png │ ├── code-dark.png │ ├── library.png │ ├── welcome.png │ ├── library-dark.png │ └── welcome-dark.png ├── icons │ ├── re.sonny.Workbench.png │ └── hicolor │ │ └── symbolic │ │ └── apps │ │ └── re.sonny.Workbench-symbolic.svg ├── app.desktop ├── meson.build └── app.gschema.xml ├── .husky └── pre-commit ├── .gitconfig ├── src ├── langs │ ├── python │ │ ├── workbench-python-previewer │ │ ├── ruff.toml │ │ ├── Builder.js │ │ ├── meson.build │ │ ├── python.js │ │ └── PythonDocument.js │ ├── rust │ │ ├── README.md │ │ ├── template │ │ │ ├── meson.build │ │ │ ├── Cargo.toml │ │ │ ├── workbench.rs │ │ │ └── lib.rs │ │ ├── RustDocument.js │ │ ├── rust.js │ │ └── Compiler.js │ ├── javascript │ │ ├── meson.build │ │ ├── biome.json │ │ ├── template │ │ │ └── jsconfig.json │ │ ├── Builder.js │ │ ├── JavaScriptDocument.js │ │ └── javascript.js │ ├── xml │ │ ├── ltx.js │ │ ├── XmlDocument.js │ │ └── xml.js │ ├── css │ │ ├── meson.build │ │ ├── css.js │ │ ├── prettier.js │ │ └── CssDocument.js │ ├── typescript │ │ ├── meson.build │ │ ├── template │ │ │ └── tsconfig.json │ │ ├── TypeScriptDocument.js │ │ ├── types │ │ │ └── ambient.d.ts │ │ ├── typescript.js │ │ └── Compiler.js │ ├── vala │ │ ├── workbench.vala │ │ ├── ValaDocument.js │ │ ├── vala.js │ │ └── Compiler.js │ └── blueprint │ │ ├── BlueprintDocument.js │ │ └── blueprint.js ├── style-dark.css ├── lib │ ├── README.md │ └── rollup.config.js ├── libworkbench │ ├── re.sonny.Workbench.libworkbench.gresource.xml │ ├── workbench-preview-window.blp │ ├── workbench.h │ ├── workbench-preview-window.h │ ├── workbench-completion-provider.h │ ├── workbench-preview-window.c │ ├── workbench-completion-request.h │ └── meson.build ├── cli │ ├── README.md │ ├── css.js │ ├── python.js │ ├── bin.js │ ├── meson.build │ ├── rust.js │ ├── typescript.js │ ├── javascript.js │ ├── format.js │ ├── vala.js │ ├── lint.js │ ├── blueprint.js │ └── util.js ├── main.js ├── icons │ ├── re.sonny.Workbench-person-symbolic.svg │ ├── re.sonny.Workbench-down-symbolic.svg │ ├── re.sonny.Workbench-up-symbolic.svg │ ├── re.sonny.Workbench-multitasking-windows-symbolic.svg │ ├── re.sonny.Workbench-larger-brush-symbolic.svg │ ├── re.sonny.Workbench-speakers-symbolic.svg │ ├── re.sonny.Workbench-library-symbolic.svg │ ├── re.sonny.Workbench-terminal-symbolic.svg │ ├── re.sonny.Workbench-eraser4-symbolic.svg │ ├── re.sonny.Workbench-screenshot-symbolic.svg │ ├── re.sonny.Workbench-test-pass-symbolic.svg │ ├── re.sonny.Workbench-gamepad-symbolic.svg │ ├── re.sonny.Workbench-external-link-symbolic.svg │ ├── re.sonny.Workbench-network-wireless-symbolic.svg │ ├── re.sonny.Workbench-preview-symbolic.svg │ └── re.sonny.Workbench-ui-symbolic.svg ├── Previewer │ ├── crasher.vala │ ├── meson.build │ ├── previewer.xml │ ├── External.js │ └── DBusPreviewer.js ├── PanelStyle.js ├── project-readme.md ├── shortcutsWindow.js ├── bin.js ├── widgets │ ├── CodeView.blp │ └── CodeFind.blp ├── flatpak.js ├── workbench ├── Library │ ├── EntryRow.blp │ ├── EntryRow.js │ └── Library.blp ├── Extensions │ ├── Extension.blp │ ├── Extension.js │ ├── Extensions.blp │ └── Extensions.js ├── lsp │ ├── LSP.js │ └── sourceview.js ├── style.css ├── overrides.js ├── Permissions │ ├── Permissions.js │ └── Permissions.blp ├── Document.js ├── meson.build ├── PanelCode.js ├── WorkbenchHoverProvider.js ├── init.js ├── shortcutsWindow.blp ├── Devtools.js ├── about.js ├── actions.js ├── log_handler.js └── common.js ├── meson_options.txt ├── .gitignore ├── .github ├── CODEOWNERS ├── FUNDING.yml └── workflows │ ├── deploy.yaml │ └── CI.yaml ├── .gitmodules ├── .vscode ├── extensions.json └── settings.json ├── .editorconfig ├── .gitattributes ├── test ├── previewer.test.js ├── isDeviceInputOverrideAvailable.test.js └── isDiagnosticInRange.test.js ├── docs ├── maintenance.md └── design-goals.md ├── meson.build ├── package.json ├── .eslintrc.yaml ├── Workbench.doap └── CONTRIBUTING.md /.prettierignore: -------------------------------------------------------------------------------- 1 | src/lib 2 | troll 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib 2 | troll 3 | flatpak 4 | demos 5 | -------------------------------------------------------------------------------- /data/workbench.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/workbench.gif -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /data/app.service: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=@app_id@ 3 | Exec=@bindir@/workbench --gapplication-service 4 | -------------------------------------------------------------------------------- /data/screenshots/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/screenshots/code.png -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [re.sonny.Commit] 2 | title-length-hint=72 3 | body-length-wrap=50 4 | auto-capitalize-title=true 5 | -------------------------------------------------------------------------------- /data/screenshots/code-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/screenshots/code-dark.png -------------------------------------------------------------------------------- /data/screenshots/library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/screenshots/library.png -------------------------------------------------------------------------------- /data/screenshots/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/screenshots/welcome.png -------------------------------------------------------------------------------- /data/icons/re.sonny.Workbench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/icons/re.sonny.Workbench.png -------------------------------------------------------------------------------- /data/screenshots/library-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/screenshots/library-dark.png -------------------------------------------------------------------------------- /data/screenshots/welcome-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workbenchdev/Workbench/HEAD/data/screenshots/welcome-dark.png -------------------------------------------------------------------------------- /src/langs/python/workbench-python-previewer: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PYTHONPATH="@pkgdatadir@" python "@pkgdatadir@/python-previewer.py" "@pkgdatadir@/previewer.xml" "$@" 3 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option( 2 | 'profile', 3 | type: 'combo', 4 | choices: [ 5 | 'default', 6 | 'development' 7 | ], 8 | value: 'default' 9 | ) 10 | -------------------------------------------------------------------------------- /src/langs/rust/README.md: -------------------------------------------------------------------------------- 1 | # Rust 2 | 3 | ## Update Cargo.toml 4 | 5 | ```sh 6 | cargo install cargo-edit 7 | cd template 8 | cargo upgrade 9 | cargo check 10 | ``` 11 | -------------------------------------------------------------------------------- /src/langs/rust/template/meson.build: -------------------------------------------------------------------------------- 1 | install_data(['Cargo.lock', 'Cargo.toml', 'lib.rs', 'workbench.rs'], 2 | install_dir : pkgdatadir / 'langs/rust/template') 3 | -------------------------------------------------------------------------------- /src/style-dark.css: -------------------------------------------------------------------------------- 1 | #panel_code, 2 | #panel_style, 3 | #panel_ui { 4 | background-color: #262626; 5 | } 6 | 7 | #toolbar_devtools { 8 | background-color: #262626; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/README.md: -------------------------------------------------------------------------------- 1 | # lib 2 | 3 | npm / Node.js libraries "compiled" to run with GJS. 4 | 5 | To rebuild them run 6 | 7 | ```sh 8 | ../../node_modules/.bin/rollup -c rollup.config.js 9 | ``` 10 | -------------------------------------------------------------------------------- /src/langs/javascript/meson.build: -------------------------------------------------------------------------------- 1 | configure_file( 2 | input: 'template/jsconfig.json', 3 | output: 'jsconfig.json', 4 | install_dir: pkgdatadir / 'langs/javascript/template/', 5 | configuration: bin_conf, 6 | ) 7 | -------------------------------------------------------------------------------- /src/langs/xml/ltx.js: -------------------------------------------------------------------------------- 1 | import { escapeXML, escapeXMLText, parse, Element, createElement } from "ltx"; 2 | import SaxLtx from "ltx/src/parsers/ltx.js"; 3 | 4 | export { escapeXML, escapeXMLText, SaxLtx, parse, Element, createElement }; 5 | -------------------------------------------------------------------------------- /src/libworkbench/re.sonny.Workbench.libworkbench.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | workbench-preview-window.ui 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/cli/README.md: -------------------------------------------------------------------------------- 1 | # workbench-cli 2 | 3 | This is sort of a headless Workbench. 4 | 5 | It exposes the formatter and linter used in Workbench for the different languages. 6 | 7 | ## Hack 8 | 9 | ```sh 10 | make cli 11 | ./build-aux/fun workbench-cli ci demos/src/* 12 | ``` 13 | -------------------------------------------------------------------------------- /src/langs/python/ruff.toml: -------------------------------------------------------------------------------- 1 | [lint] 2 | ignore = [ 3 | "E402", # Module level import not at top of file -> gi imports may come after gi.require_version. 4 | "E501", # Line too long. This is probably better enforced manually where it makes sense. 5 | ] 6 | select = [ "W", "E", "F", "ARG"] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .flatpak 2 | .flatpak-builder 3 | build 4 | builddir 5 | /flatpak 6 | node_modules 7 | repo 8 | install 9 | .eslintcache 10 | *~ 11 | *.compiled 12 | *.flatpak 13 | .fenv 14 | .venv 15 | __pycache__ 16 | *.pyc 17 | *.gresource 18 | .frun 19 | 20 | # IDEs / editors 21 | .idea 22 | 23 | target 24 | -------------------------------------------------------------------------------- /src/langs/xml/XmlDocument.js: -------------------------------------------------------------------------------- 1 | import * as xml from "../../langs/xml/xml.js"; 2 | import Document from "../../Document.js"; 3 | 4 | export class XmlDocument extends Document { 5 | async format() { 6 | const code = xml.format(this.buffer.text, 2); 7 | this.code_view.replaceText(code, false); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /src/langs/javascript/ @sonnyp 2 | *.js @sonnyp 3 | 4 | /src/langs/vala/ @lw64 5 | *.vala @lw64 6 | 7 | /src/langs/rust/ @Hofer-Julian 8 | *.rs @Hofer-Julian 9 | Cargo.toml @Hofer-Julian 10 | Cargo.lock @Hofer-Julian 11 | 12 | /src/langs/python/ @theCapypara 13 | *.py @theCapypara 14 | 15 | *.c @andyholmes 16 | *.h @andyholmes 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "troll"] 2 | path = troll 3 | url = https://github.com/sonnyp/troll.git 4 | [submodule "demos"] 5 | path = demos 6 | url = https://github.com/workbenchdev/demos.git 7 | [submodule "src/langs/typescript/template/gi-types"] 8 | path = gi-types 9 | url = https://gitlab.gnome.org/BrainBlasted/gi-typescript-definitions.git 10 | branch = nightly 11 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import "./init.js"; 2 | import "./log_handler.js"; 3 | import application from "./application.js"; 4 | 5 | pkg.initGettext(); 6 | 7 | import "./language-specs/blueprint.lang"; 8 | import "./style.css"; 9 | import "./style-dark.css"; 10 | import "./libworkbench/workbench-preview-window.blp"; 11 | 12 | export function main(argv) { 13 | return application.runAsync(argv); 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bilelmoussaoui.flatpak-vscode", 4 | "editorconfig.editorconfig", 5 | "esbenp.prettier-vscode", 6 | "mrorz.language-gettext", 7 | "yzhang.markdown-all-in-one", 8 | "mesonbuild.mesonbuild", 9 | "prince781.vala", 10 | "bodil.blueprint-gtk", 11 | "dbaeumer.vscode-eslint", 12 | "charliermarsh.ruff" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/libworkbench/workbench-preview-window.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $WorkbenchPreviewWindow: Adw.Window { 5 | title: _("Preview"); 6 | default-width: 600; 7 | default-height: 800; 8 | width-request: 360; 9 | height-request: 360; 10 | 11 | content: Adw.ToolbarView toolbar_view { 12 | top-bar-style: raised; 13 | 14 | [top] 15 | Adw.HeaderBar {} 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{Makefile,**.mk}] 14 | indent_style = tab 15 | 16 | [*.py] 17 | indent_size = 4 18 | 19 | [*.rs] 20 | indent_size = 4 21 | 22 | [*.vala] 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-person-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Previewer/crasher.vala: -------------------------------------------------------------------------------- 1 | public void main (string[] args) { 2 | Adw.init (); 3 | 4 | var builder = new Gtk.Builder (); 5 | var output = new Gtk.Window (); 6 | 7 | try { 8 | builder.add_from_string (args[1], -1); 9 | var object = builder.get_object (args[2]) as Gtk.Widget; 10 | output.set_child (object); 11 | } catch (Error e) { 12 | GLib.error (e.message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/libworkbench/workbench.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Workbench Contributors 3 | // SPDX-FileContributor: Andy Holmes 4 | 5 | #pragma once 6 | 7 | #define WORKBENCH_INSIDE 8 | 9 | #include "libworkbench-enums.h" 10 | 11 | #include "workbench-completion-provider.h" 12 | #include "workbench-completion-request.h" 13 | #include "workbench-preview-window.h" 14 | 15 | #undef WORKBENCH_INSIDE 16 | 17 | -------------------------------------------------------------------------------- /src/langs/css/meson.build: -------------------------------------------------------------------------------- 1 | gjspack = find_program(meson.project_source_root() / 'troll/gjspack/bin/gjspack') 2 | custom_target('prettier', 3 | input: ['prettier.js'], 4 | output: ['prettier', 'prettier.src.gresource'], 5 | command: [ 6 | gjspack, 7 | '--resource-root', meson.project_source_root() / 'src', 8 | '@INPUT0@', 9 | '@OUTDIR@', 10 | ], 11 | install: true, 12 | install_dir: get_option('bindir'), 13 | build_always_stale: true, 14 | ) 15 | 16 | -------------------------------------------------------------------------------- /data/app.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | # TRANSLATORS: Don't translate 3 | Name=Workbench 4 | Comment=Learn and prototype with GNOME technologies 5 | Exec=workbench %U 6 | Terminal=false 7 | Type=Application 8 | Categories=WebDevelopment;Development;IDE;GNOME;GTK; 9 | Icon=@app_id@ 10 | # TRANSLATORS: Don't translate 11 | Keywords=CSS;JavaScript;GJS;Blueprint;builder;Vala;GTK;libadwaita;Python;PyGObject;Rust;doc;playground;code;TypeScript; 12 | DBusActivatable=true 13 | StartupNotify=true 14 | -------------------------------------------------------------------------------- /src/langs/typescript/meson.build: -------------------------------------------------------------------------------- 1 | configure_file( 2 | input: 'template/tsconfig.json', 3 | output: 'tsconfig.json', 4 | install_dir: pkgdatadir / 'langs/typescript/template/', 5 | configuration: bin_conf, 6 | ) 7 | 8 | install_data( 9 | ['types/ambient.d.ts'], 10 | install_dir: pkgdatadir / 'langs/typescript', 11 | preserve_path: true, 12 | ) 13 | 14 | install_subdir( 15 | meson.project_source_root() / 'gi-types', 16 | install_dir: pkgdatadir / 'langs/typescript', 17 | ) 18 | -------------------------------------------------------------------------------- /src/langs/python/Builder.js: -------------------------------------------------------------------------------- 1 | import dbus_previewer from "../../Previewer/DBusPreviewer.js"; 2 | 3 | export default function PythonBuilder({ session }) { 4 | async function run() { 5 | try { 6 | const proxy = await dbus_previewer.getProxy("python"); 7 | await proxy.RunAsync(session.file.get_path(), session.file.get_uri()); 8 | } catch (err) { 9 | console.error(err); 10 | return false; 11 | } 12 | 13 | return true; 14 | } 15 | 16 | return { run }; 17 | } 18 | -------------------------------------------------------------------------------- /src/langs/python/meson.build: -------------------------------------------------------------------------------- 1 | bin_conf = configuration_data() 2 | bin_conf.set('pkgdatadir', pkgdatadir) 3 | 4 | install_data('gdbus_ext.py', install_dir: pkgdatadir) 5 | install_data('python-previewer.py', install_dir: pkgdatadir) 6 | install_data('ruff.toml', install_dir: pkgdatadir) 7 | 8 | configure_file( 9 | input: 'workbench-python-previewer', 10 | output: 'workbench-python-previewer', 11 | configuration: bin_conf, 12 | install: true, 13 | install_dir: get_option('bindir') 14 | ) 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json -diff 2 | 3 | test/assert.js -diff 4 | test/uvu.js -diff 5 | 6 | src/lib/ltx.js -diff 7 | src/lib/postcss.js -diff 8 | src/lib/prettier.js -diff 9 | src/lib/prettier-babel.js -diff 10 | src/lib/prettier-postcss.js -diff 11 | src/lib/prettier-xml.js -diff 12 | 13 | data/icons/re.sonny.Workbench-symbolic.svg -diff 14 | data/icons/re.sonny.Workbench.Devel.svg -diff 15 | data/icons/re.sonny.Workbench.svg -diff 16 | 17 | # po/re.sonny.Workbench.pot -diff 18 | # po/*.po -diff 19 | -------------------------------------------------------------------------------- /src/langs/javascript/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.3.3/schema.json", 3 | "javascript": { 4 | "globals": ["workbench"] 5 | }, 6 | "formatter": { 7 | "indentStyle": "space", 8 | "indentWidth": 2 9 | }, 10 | "linter": { 11 | "rules": { 12 | "recommended": false, 13 | "correctness": { 14 | "noUndeclaredVariables": "error", 15 | "noUnusedVariables": "warn", 16 | "noUnusedImports": "warn" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-down-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/PanelStyle.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | import GObject from "gi://GObject"; 3 | 4 | export default function PanelStyle({ builder, settings }) { 5 | const button_style = builder.get_object("button_style"); 6 | const panel_style = builder.get_object("panel_style"); 7 | settings.bind( 8 | "show-style", 9 | button_style, 10 | "active", 11 | Gio.SettingsBindFlags.DEFAULT, 12 | ); 13 | button_style.bind_property( 14 | "active", 15 | panel_style, 16 | "visible", 17 | GObject.BindingFlags.SYNC_CREATE, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /test/previewer.test.js: -------------------------------------------------------------------------------- 1 | import "../src/init.js"; 2 | 3 | import WebKit from "gi://WebKit"; 4 | import Source from "gi://GtkSource"; 5 | 6 | import tst, { assert } from "../troll/tst/tst.js"; 7 | import { getObjectClass } from "../src/Previewer/utils.js"; 8 | 9 | const test = tst("previewer"); 10 | 11 | test("getObjectClass", () => { 12 | assert.equal(getObjectClass("WebKitWebView"), WebKit.WebView); 13 | assert.equal( 14 | getObjectClass("GtkSourceCompletionProvider"), 15 | Source.CompletionProvider, 16 | ); 17 | }); 18 | 19 | export default test; 20 | -------------------------------------------------------------------------------- /docs/maintenance.md: -------------------------------------------------------------------------------- 1 | ## Maintenance 2 | 3 | Notes and instructions for maintainers. 4 | 5 | ## Release 6 | 7 | ```sh 8 | $V = 45 9 | 10 | git checkout l10n 11 | git pull 12 | git checkout main 13 | git merge --squash l10n 14 | meson compile re.sonny.Workbench-pot -C _build 15 | meson compile re.sonny.Workbench-update-po -C _build 16 | git commit -m 'Update translations' 17 | 18 | # Update version 19 | # bump version in meson.build 20 | # add release notes to metainfo 21 | git add meson.build 22 | 23 | git commit -m '$V' 24 | git tag '$V' 25 | git push 26 | git push origin $V 27 | ``` 28 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-up-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-multitasking-windows-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/langs/vala/workbench.vala: -------------------------------------------------------------------------------- 1 | namespace workbench { 2 | public static Gtk.Builder builder; 3 | public static Gtk.Window window; 4 | public static string uri; 5 | public string resolve (string path) { 6 | return File.new_for_uri (workbench.uri).resolve_relative_path (path).get_uri (); 7 | } 8 | } 9 | 10 | public void set_builder (Gtk.Builder builder) { 11 | workbench.builder = builder; 12 | } 13 | 14 | public void set_window (Gtk.Window window) { 15 | workbench.window = window; 16 | } 17 | 18 | public void set_base_uri (string uri) { 19 | workbench.uri = uri; 20 | } 21 | -------------------------------------------------------------------------------- /src/cli/css.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | import { getLanguage } from "../common.js"; 4 | import { checkFile, diagnose } from "./util.js"; 5 | 6 | const languageId = "css"; 7 | 8 | export default async function css({ file, lspc }) { 9 | print(` ${file.get_path()}`); 10 | 11 | await diagnose({ file, lspc, languageId }); 12 | 13 | await checkFile({ 14 | lspc, 15 | file, 16 | lang: getLanguage(languageId), 17 | uri: file.get_uri(), 18 | }); 19 | 20 | await lspc._notify("textDocument/didClose", { 21 | textDocument: { 22 | uri: file.get_uri(), 23 | }, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/project-readme.md: -------------------------------------------------------------------------------- 1 | This is a Workbench project. 2 | 3 | To open and run this; [install Workbench from Flathub](https://flathub.org/apps/re.sonny.Workbench) and open this project folder with it. 4 | 5 | ## Icons 6 | 7 | You can embed icons into your project by adding them to the [`./icons`](./icons/) subfolder. 8 | 9 | Then you can reference them by name in UI. For example 10 | 11 | ``` 12 | # Given a file icons/moon-symbolic.svg 13 | Gtk.Image { 14 | icon-name: "moon-symbolic"; 15 | } 16 | ``` 17 | 18 | Press "Run" if your icon is not detected yet. 19 | 20 | Please refer to Workbench Library entry "Using Icons" for more information. 21 | -------------------------------------------------------------------------------- /src/langs/typescript/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "Bundler", 5 | // TODO: should probably be fixed to ES2023, or whatever standard is 6 | // currently supported by the latest GJS 7 | "target": "ESNext", 8 | "outDir": "compiled_javascript", 9 | "baseUrl": ".", 10 | "paths": { 11 | "*": ["*", "@pkgdatadir@/langs/typescript/gi-types/*"] 12 | }, 13 | "skipLibCheck": true 14 | }, 15 | "include": [ 16 | "main.ts", 17 | "@pkgdatadir@/langs/typescript/types/ambient.d.ts", 18 | "@pkgdatadir@/langs/typescript/gi-types/gi.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/langs/javascript/template/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | // TODO: should probably be fixed to ES2023, or whatever standard is 7 | // currently supported by the latest GJS 8 | "target": "ESNext", 9 | "outDir": "compiled_javascript", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": ["*", "@pkgdatadir@/langs/typescript/gi-types/*"] 13 | }, 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "main.js", 18 | "@pkgdatadir@/langs/typescript/types/ambient.d.ts", 19 | "@pkgdatadir@/langs/typescript/gi-types/gi.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/libworkbench/workbench-preview-window.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | G_BEGIN_DECLS 8 | 9 | #define WORKBENCH_TYPE_PREVIEW_WINDOW (workbench_preview_window_get_type()) 10 | 11 | G_DECLARE_FINAL_TYPE (WorkbenchPreviewWindow, workbench_preview_window, WORKBENCH, PREVIEW_WINDOW, AdwWindow) 12 | 13 | WorkbenchPreviewWindow *workbench_preview_window_new (void) G_GNUC_WARN_UNUSED_RESULT; 14 | 15 | GtkWidget *workbench_preview_window_get_content (WorkbenchPreviewWindow *self); 16 | void workbench_preview_window_set_content (WorkbenchPreviewWindow *self, 17 | GtkWidget *content); 18 | 19 | G_END_DECLS 20 | -------------------------------------------------------------------------------- /src/Previewer/meson.build: -------------------------------------------------------------------------------- 1 | executable('workbench-previewer-module', 2 | 'previewer.vala', 3 | dependencies: [ dependency('gtksourceview-5'), dependency('gmodule-2.0'), dependency('libadwaita-1'), dependency('shumate-1.0'), dependency('webkitgtk-6.0'), libworkbench_vapi ], 4 | # vala_args: [ '--gresourcesdir=' + meson.current_build_dir() ], 5 | install: true, 6 | link_with: libworkbench, 7 | ) 8 | 9 | executable('workbench-crasher', 10 | 'crasher.vala', 11 | dependencies: [ dependency('gio-2.0'), dependency('gmodule-2.0'), dependency('libadwaita-1') ], 12 | # vala_args: [ '--gresourcesdir=' + meson.current_build_dir() ], 13 | install: true, 14 | ) 15 | 16 | install_data('previewer.xml', install_dir: pkgdatadir) 17 | -------------------------------------------------------------------------------- /src/shortcutsWindow.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | 3 | import resource from "./shortcutsWindow.blp" with { type: "uri" }; 4 | 5 | import { build } from "../troll/src/builder.js"; 6 | 7 | export default function ShortcutsWindow({ application }) { 8 | let window; 9 | 10 | const action_shortcuts = new Gio.SimpleAction({ 11 | name: "shortcuts", 12 | }); 13 | action_shortcuts.connect("activate", () => { 14 | if (!window) { 15 | ({ window } = build(resource)); 16 | } 17 | window.set_transient_for(application.active_window); 18 | window.present(); 19 | }); 20 | application.add_action(action_shortcuts); 21 | application.set_accels_for_action("app.shortcuts", ["question"]); 22 | } 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: sonnyp 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/cli/python.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | import { PYTHON_LSP_CONFIG, getLanguage } from "../common.js"; 4 | 5 | import { checkFile, diagnose } from "./util.js"; 6 | 7 | const languageId = "python"; 8 | 9 | export default async function python({ file, lspc }) { 10 | print(` ${file.get_path()}`); 11 | 12 | await lspc._request("workspace/didChangeConfiguration", { 13 | settings: PYTHON_LSP_CONFIG, 14 | }); 15 | 16 | await diagnose({ file, lspc, languageId }); 17 | 18 | await checkFile({ 19 | lspc, 20 | file, 21 | lang: getLanguage(languageId), 22 | uri: file.get_uri(), 23 | }); 24 | 25 | await lspc._notify("textDocument/didClose", { 26 | textDocument: { 27 | uri: file.get_uri(), 28 | }, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S gjs -m 2 | 3 | import Gio from "gi://Gio"; 4 | import { exit, programArgs } from "system"; 5 | import { setConsoleLogDomain } from "console"; 6 | import GLib from "gi://GLib"; 7 | 8 | // eslint-disable-next-line no-restricted-globals 9 | imports.package.init({ 10 | name: "@app_id@", 11 | version: "@version@", 12 | prefix: "@prefix@", 13 | libdir: "@libdir@", 14 | datadir: "@datadir@", 15 | }); 16 | 17 | const app_id = "@app_id@.cli"; 18 | 19 | setConsoleLogDomain(app_id); 20 | GLib.set_application_name("workbench-cli"); 21 | 22 | const resource = Gio.Resource.load( 23 | `/app/share/${app_id}/${app_id}.src.gresource`, 24 | ); 25 | resource._register(); 26 | 27 | const module = await import("resource:///re/sonny/Workbench/cli/main.js"); 28 | const exit_code = await module.main(programArgs); 29 | exit(exit_code); 30 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-larger-brush-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/langs/css/css.js: -------------------------------------------------------------------------------- 1 | import { createLSPClient } from "../../common.js"; 2 | import { getLanguage } from "../../util.js"; 3 | 4 | export function setup({ document }) { 5 | const { file, buffer, code_view } = document; 6 | 7 | const lspc = createLSPClient({ 8 | lang: getLanguage("css"), 9 | root_uri: file.get_parent().get_uri(), 10 | quiet: true, 11 | }); 12 | lspc.buffer = buffer; 13 | lspc.uri = file.get_uri(); 14 | lspc.connect( 15 | "notification::textDocument/publishDiagnostics", 16 | (_self, params) => { 17 | if (params.uri !== file.get_uri()) { 18 | return; 19 | } 20 | code_view.handleDiagnostics(params.diagnostics); 21 | }, 22 | ); 23 | 24 | lspc.start().catch(console.error); 25 | buffer.connect("modified-changed", () => { 26 | if (!buffer.get_modified()) return; 27 | lspc.didChange().catch(console.error); 28 | }); 29 | 30 | return lspc; 31 | } 32 | -------------------------------------------------------------------------------- /src/langs/rust/template/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "demo" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async-channel = "2.2.0" 10 | libc = "0.2" 11 | rand = "0.8.5" 12 | url = "2.5.0" 13 | 14 | [dependencies.soup3] 15 | version = "0.6.0" 16 | features = ["v3_4"] 17 | 18 | [dependencies.webkit6] 19 | version = "0.3.0" 20 | features = ["v2_42"] 21 | 22 | [dependencies.gtk] 23 | version = "0.8.1" 24 | package = "gtk4" 25 | features = ["gnome_46"] 26 | 27 | [dependencies.adw] 28 | version = "0.6.0" 29 | package = "libadwaita" 30 | features = ["v1_5"] 31 | 32 | [dependencies.ashpd] 33 | version = "0.8.1" 34 | features = ["gtk4"] 35 | 36 | [dependencies.shumate] 37 | version = "0.5.0" 38 | package = "libshumate" 39 | features = ["v1_2"] 40 | 41 | [lib] 42 | crate-type = ["cdylib"] 43 | path = "lib.rs" 44 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-speakers-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/bin.js: -------------------------------------------------------------------------------- 1 | #!@GJS@ -m 2 | 3 | import { exit, programArgs } from "system"; 4 | import { setConsoleLogDomain } from "console"; 5 | import Xdp from "gi://Xdp"; 6 | 7 | // eslint-disable-next-line no-restricted-globals 8 | imports.package.init({ 9 | name: "@app_id@", 10 | version: "@version@", 11 | prefix: "@prefix@", 12 | libdir: "@libdir@", 13 | datadir: "@datadir@", 14 | }); 15 | setConsoleLogDomain(pkg.name); 16 | 17 | if (!Xdp.Portal.running_under_flatpak()) { 18 | console.error( 19 | "Flatpak required\nWorkbench is only meant to be run sandboxed in a specific target environment.\nBypassing this will exposes users to arbitrary code execution and breakage.", 20 | ); 21 | exit(1); 22 | } 23 | 24 | globalThis.__DEV__ = pkg.name.endsWith(".Devel"); 25 | if (__DEV__) { 26 | pkg.sourcedir = "@sourcedir@"; 27 | } 28 | 29 | const module = await import("resource:///re/sonny/Workbench/main.js"); 30 | const exit_code = await module.main(programArgs); 31 | exit(exit_code); 32 | -------------------------------------------------------------------------------- /src/langs/rust/RustDocument.js: -------------------------------------------------------------------------------- 1 | import { setup } from "./rust.js"; 2 | 3 | import Document from "../../Document.js"; 4 | import { applyTextEdits } from "../../lsp/sourceview.js"; 5 | 6 | export class RustDocument extends Document { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.lspc = setup({ document: this }); 11 | this.code_view.lspc = this.lspc; 12 | } 13 | 14 | async format() { 15 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 16 | const text_edits = await this.lspc.request("textDocument/formatting", { 17 | textDocument: { 18 | uri: this.file.get_uri(), 19 | }, 20 | options: { 21 | tabSize: 4, 22 | insertSpaces: true, 23 | trimTrailingWhitespace: true, 24 | insertFinalNewline: true, 25 | trimFinalNewlines: true, 26 | }, 27 | }); 28 | 29 | applyTextEdits(text_edits, this.buffer); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/langs/rust/template/workbench.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use gtk::gio; 4 | use gtk::prelude::*; 5 | 6 | #[allow(dead_code)] 7 | pub(crate) fn builder() -> &'static gtk::Builder { 8 | unsafe { 9 | crate::BUILDER 10 | .as_ref() 11 | .expect("Builder instance should already be initialized.") 12 | } 13 | } 14 | 15 | #[allow(dead_code)] 16 | pub(crate) fn window() -> &'static adw::Window { 17 | unsafe { 18 | crate::WINDOW 19 | .as_ref() 20 | .expect("Window instance should already be initialized.") 21 | } 22 | } 23 | 24 | #[allow(dead_code)] 25 | pub(crate) fn resolve(path: impl AsRef) -> String { 26 | unsafe { 27 | let uri = crate::URI 28 | .as_ref() 29 | .expect("URI instance should already be initialized."); 30 | gio::File::for_uri(uri) 31 | .resolve_relative_path(path) 32 | .uri() 33 | .to_string() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/widgets/CodeView.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using GtkSource 5; 3 | 4 | template $CodeView: Gtk.Widget { 5 | layout-manager: BoxLayout { 6 | orientation: vertical; 7 | }; 8 | 9 | vexpand: true; 10 | 11 | ScrolledWindow scrolled_window { 12 | vexpand: true; 13 | 14 | GtkSource.View source_view { 15 | buffer: GtkSource.Buffer { 16 | implicit-trailing-newline: false; 17 | }; 18 | 19 | [internal-child completion] 20 | GtkSource.Completion { 21 | select-on-show: true; 22 | } 23 | 24 | monospace: true; 25 | auto-indent: true; 26 | highlight-current-line: true; 27 | indent-on-tab: true; 28 | indent-width: 2; 29 | insert-spaces-instead-of-tabs: true; 30 | show-line-marks: true; 31 | show-line-numbers: true; 32 | smart-backspace: true; 33 | tab-width: 2; 34 | } 35 | } 36 | 37 | $CodeFind code_find { 38 | vexpand: false; 39 | source-view: source_view; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | flatpak: 10 | name: "Flatpak" 11 | runs-on: ubuntu-latest 12 | container: 13 | image: bilelmoussaoui/flatpak-github-actions:gnome-nightly 14 | options: --privileged 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 21 | name: "Build" 22 | with: 23 | bundle: re.sonny.Workbench.Devel.flatpak 24 | manifest-path: build-aux/re.sonny.Workbench.Devel.json 25 | cache-key: flatpak-builder-${{ github.sha }} 26 | 27 | - uses: flatpak/flatpak-github-actions/flat-manager@v6 28 | name: "Deploy" 29 | with: 30 | repository: nightly 31 | flat-manager-url: https://flat-manager.gnome.org/ 32 | token: ${{ secrets.GNOME_NIGHTLY_TOKEN }} 33 | -------------------------------------------------------------------------------- /docs/design-goals.md: -------------------------------------------------------------------------------- 1 | # Previewer 2 | 3 | Any valid GTK XML or Blueprint `UI` should be able to render in `Preview`. 4 | 5 | At least when it comes to the internal/in-process previwer, the preview should be as helpful as possible. Missing signal handlers or objects shouldn't prevent the preview from updating. 6 | 7 | Triggering signal handlers should log a helpful message. 8 | 9 | Missing objects should present a helpful message. 10 | 11 | Out of process preview/code can crash but not take Workbench down with it. 12 | 13 | # Resilience 14 | 15 | Workbench itself should not crash under any circumstances. 16 | 17 | # Clarity 18 | 19 | When changing the parameters - Workbench should reset to a clean slate 20 | 21 | Changing the parameters includes 22 | 23 | - Changing Code language 24 | - Changing UI language 25 | - Opening a file 26 | - Opening a demo 27 | 28 | Resetting to a clean slate involves 29 | 30 | - re-rend the preview to what CSS/UI dictates 31 | - clear the console 32 | - scroll console to end 33 | -------------------------------------------------------------------------------- /src/langs/typescript/TypeScriptDocument.js: -------------------------------------------------------------------------------- 1 | import { setup } from "./typescript.js"; 2 | 3 | import Document from "../../Document.js"; 4 | import { applyTextEdits } from "../../lsp/sourceview.js"; 5 | 6 | export class TypeScriptDocument extends Document { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.lspc = setup({ document: this }); 11 | this.code_view.lspc = this.lspc; 12 | } 13 | 14 | async format() { 15 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 16 | const text_edits = await this.lspc.request("textDocument/formatting", { 17 | textDocument: { 18 | uri: this.file.get_uri(), 19 | }, 20 | options: { 21 | tabSize: 2, 22 | insertSpaces: true, 23 | trimTrailingWhitespace: true, 24 | insertFinalNewline: true, 25 | trimFinalNewlines: true, 26 | }, 27 | }); 28 | 29 | applyTextEdits(text_edits, this.buffer); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'Workbench', 3 | ['vala', 'c', 'rust'], 4 | version: '49.0', 5 | meson_version: '>= 1.0.0', 6 | license: 'GPL-3.0-only', 7 | default_options: [ 8 | 'libdir=lib', 9 | 'warning_level=2', 10 | 'werror=false', 11 | ], 12 | ) 13 | 14 | gnome = import('gnome') 15 | 16 | if get_option('profile') == 'development' 17 | app_id = 're.sonny.Workbench.Devel' 18 | vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD').stdout().strip() 19 | if vcs_tag == '' 20 | version_suffix = '-devel' 21 | else 22 | version_suffix = '-@0@'.format(vcs_tag) 23 | endif 24 | else 25 | app_id = 're.sonny.Workbench' 26 | version_suffix = '' 27 | endif 28 | 29 | prefix = get_option('prefix') 30 | bindir = prefix / 'bin' 31 | datadir = prefix / get_option('datadir') 32 | pkgdatadir = datadir / app_id 33 | 34 | subdir('data') 35 | subdir('src') 36 | 37 | gnome.post_install( 38 | glib_compile_schemas: true, 39 | gtk_update_icon_cache: true, 40 | update_desktop_database: true, 41 | ) 42 | -------------------------------------------------------------------------------- /src/langs/css/prettier.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S gjs -m 2 | 3 | // Shim to Node.js prettier CLI for GTKCssLanguageServer 4 | // ./src/langs/css/prettier.js --stdin-filepath src/style.css 5 | // or 6 | // ./troll/gjspack/bin/gjspack src/langs/css/prettier.js src/langs/css && ./src/langs/css/prettier --stdin-filepath src/style.css 7 | 8 | import { exit } from "system"; 9 | import Gio from "gi://Gio"; 10 | 11 | import { format } from "../../lib/prettier.js"; 12 | import prettier_postcss from "../../lib/prettier-postcss.js"; 13 | 14 | const idx = ARGV.indexOf("--stdin-filepath"); 15 | if (idx < 0) exit(1); 16 | const filename = ARGV[idx + 1]; 17 | if (!filename) exit(1); 18 | 19 | const file = Gio.File.new_for_path(filename); 20 | const [, contents] = file.load_contents(null); 21 | const text = new TextDecoder().decode(contents); 22 | 23 | const formatted = await format(text, { 24 | parser: "css", 25 | plugins: [prettier_postcss], 26 | }); 27 | 28 | const stream = new Gio.UnixOutputStream({ fd: 1 }); 29 | stream.write_all(formatted, null); 30 | -------------------------------------------------------------------------------- /src/flatpak.js: -------------------------------------------------------------------------------- 1 | import GLib from "gi://GLib"; 2 | 3 | let flatpak_info; 4 | export function getFlatpakInfo() { 5 | if (flatpak_info) return flatpak_info; 6 | flatpak_info = new GLib.KeyFile(); 7 | try { 8 | flatpak_info.load_from_file("/.flatpak-info", GLib.KeyFileFlags.NONE); 9 | } catch (err) { 10 | if (!err.matches(GLib.FileError, GLib.FileError.NOENT)) { 11 | console.error(err); 12 | } 13 | return null; 14 | } 15 | return flatpak_info; 16 | } 17 | 18 | export function getFlatpakId() { 19 | return getFlatpakInfo().get_string("Application", "name"); 20 | } 21 | 22 | // https://repology.org/project/flatpak/versions 23 | export function isDeviceInputOverrideAvailable(flatpak_version) { 24 | flatpak_version ??= getFlatpakInfo().get_string( 25 | "Instance", 26 | "flatpak-version", 27 | ); 28 | 29 | // https://github.com/flatpak/flatpak/releases/tag/1.15.6 30 | return ( 31 | flatpak_version.localeCompare("1.15.6", undefined, { 32 | numeric: true, 33 | sensitivity: "base", 34 | }) > -1 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/langs/python/python.js: -------------------------------------------------------------------------------- 1 | import { PYTHON_LSP_CONFIG, createLSPClient } from "../../common.js"; 2 | import { getLanguage } from "../../util.js"; 3 | 4 | export function setup({ document }) { 5 | const { file, buffer, code_view } = document; 6 | 7 | const lspc = createLSPClient({ 8 | lang: getLanguage("python"), 9 | root_uri: file.get_parent().get_uri(), 10 | quiet: true, 11 | }); 12 | 13 | lspc.request("workspace/didChangeConfiguration", { 14 | settings: PYTHON_LSP_CONFIG, 15 | }); 16 | 17 | lspc.buffer = buffer; 18 | lspc.uri = file.get_uri(); 19 | lspc.connect( 20 | "notification::textDocument/publishDiagnostics", 21 | (_self, params) => { 22 | if (params.uri !== file.get_uri()) { 23 | return; 24 | } 25 | code_view.handleDiagnostics(params.diagnostics); 26 | }, 27 | ); 28 | 29 | lspc.start().catch(console.error); 30 | 31 | buffer.connect("modified-changed", () => { 32 | if (!buffer.get_modified()) return; 33 | lspc.didChange().catch(console.error); 34 | }); 35 | 36 | return lspc; 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import ignore from "rollup-plugin-ignore"; 4 | import nodePolyfills from "rollup-plugin-node-polyfills"; 5 | 6 | export default [ 7 | { 8 | input: "../langs/xml/ltx.js", 9 | output: { 10 | file: "ltx.js", 11 | }, 12 | plugins: [nodePolyfills(), commonjs(), nodeResolve()], 13 | }, 14 | 15 | { 16 | input: "../../node_modules/prettier/standalone.mjs", 17 | output: { 18 | file: "prettier.js", 19 | }, 20 | }, 21 | 22 | { 23 | input: "../../node_modules/prettier/plugins/postcss.mjs", 24 | output: { 25 | file: "prettier-postcss.js", 26 | }, 27 | }, 28 | 29 | { 30 | input: "../../node_modules/postcss/lib/postcss.mjs", 31 | output: { 32 | file: "postcss.js", 33 | }, 34 | plugins: [ 35 | commonjs(), 36 | ignore(["picocolors", "source-map-js", "path", "fs", "url"]), 37 | nodeResolve({ resolveOnly: ["nanoid"] }), 38 | ], 39 | }, 40 | ]; 41 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-library-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/workbench: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # export G_MESSAGES_DEBUG=@app_id@ 4 | 5 | # Required to allow pkgconfig to find pc files in /app/lib/pkgconfig 6 | export PKG_CONFIG_PATH=/app/lib/pkgconfig/:$PKG_CONFIG_PATH 7 | 8 | source /usr/lib/sdk/rust-stable/enable.sh 2> /dev/null 9 | source /usr/lib/sdk/vala/enable.sh 2> /dev/null 10 | source /usr/lib/sdk/llvm11/enable.sh 2> /dev/null 11 | source /usr/lib/sdk/node24/enable.sh 2> /dev/null 12 | 13 | ## enabling the typescript extension 14 | export PATH=$PATH:/usr/lib/sdk/typescript/bin 15 | 16 | # TODO: Figure out how to use gcc with mold so we can drop llvm 17 | export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=clang 18 | export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold" 19 | export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=clang 20 | export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold" 21 | 22 | # We do not support translations but the AboutWindow is translated by default 23 | LANG=en_US.UTF-8 24 | 25 | mkdir -p $XDG_RUNTIME_DIR/$FLATPAK_ID 26 | @command@ 27 | -------------------------------------------------------------------------------- /src/libworkbench/workbench-completion-provider.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | // SPDX-FileCopyrightText: Workbench Contributors 3 | // SPDX-FileContributor: Andy Holmes 4 | 5 | #pragma once 6 | 7 | #if !defined (WORKBENCH_INSIDE) && !defined (WORKBENCH_COMPILATION) 8 | # error "Only can be included directly." 9 | #endif 10 | 11 | #include 12 | #include 13 | 14 | #include "workbench-completion-request.h" 15 | 16 | G_BEGIN_DECLS 17 | 18 | #define WORKBENCH_TYPE_COMPLETION_PROVIDER (workbench_completion_provider_get_type()) 19 | 20 | G_DECLARE_DERIVABLE_TYPE (WorkbenchCompletionProvider, workbench_completion_provider, WORKBENCH, COMPLETION_PROVIDER, GObject) 21 | 22 | struct _WorkbenchCompletionProviderClass 23 | { 24 | GObjectClass parent_class; 25 | 26 | /* signal closures */ 27 | void (*completion_request) (WorkbenchCompletionProvider *self, 28 | WorkbenchCompletionRequest *request); 29 | 30 | /*< private >*/ 31 | gpointer padding[8]; 32 | }; 33 | 34 | G_END_DECLS 35 | -------------------------------------------------------------------------------- /test/isDeviceInputOverrideAvailable.test.js: -------------------------------------------------------------------------------- 1 | import "../src/init.js"; 2 | 3 | import tst, { assert } from "../troll/tst/tst.js"; 4 | 5 | import { isDeviceInputOverrideAvailable } from "../src/flatpak.js"; 6 | 7 | const test = tst("isDeviceInputOverrideAvailable"); 8 | 9 | test("returns a boolean", () => { 10 | assert.equal(typeof isDeviceInputOverrideAvailable(), "boolean"); 11 | }); 12 | 13 | test("returns true if Flatpak version is equal to 1.15.6", () => { 14 | assert.equal(isDeviceInputOverrideAvailable("1.15.6"), true); 15 | }); 16 | 17 | test("returns true if Flatpak version is higher than 1.15.6", () => { 18 | assert.equal(isDeviceInputOverrideAvailable("1.15.7"), true); 19 | assert.equal(isDeviceInputOverrideAvailable("1.16.5"), true); 20 | assert.equal(isDeviceInputOverrideAvailable("2.15.4"), true); 21 | }); 22 | 23 | test("returns false if Flatpak version is lower than 1.15.6", () => { 24 | assert.equal(isDeviceInputOverrideAvailable("1.15.5"), false); 25 | assert.equal(isDeviceInputOverrideAvailable("1.14.7"), false); 26 | assert.equal(isDeviceInputOverrideAvailable("0.16.7"), false); 27 | }); 28 | 29 | export default test; 30 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-terminal-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-eraser4-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-screenshot-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Library/EntryRow.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $EntryRow: Adw.ActionRow { 5 | activatable: true; 6 | 7 | accessibility { 8 | labelled-by: title_label; 9 | described-by: description_label; 10 | } 11 | 12 | [prefix] 13 | Box contents { 14 | orientation: horizontal; 15 | 16 | Box labels_box { 17 | margin-top: 6; 18 | margin-bottom: 6; 19 | spacing: 3; 20 | orientation: vertical; 21 | 22 | Label title_label { 23 | xalign: 0; 24 | wrap: true; 25 | wrap-mode: word_char; 26 | } 27 | 28 | Label description_label { 29 | styles [ 30 | "dim-label", 31 | "caption", 32 | ] 33 | 34 | xalign: 0; 35 | wrap: true; 36 | wrap-mode: word_char; 37 | natural-wrap-mode: none; 38 | } 39 | 40 | Box languages_box { 41 | orientation: horizontal; 42 | spacing: 3; 43 | margin-top: 3; 44 | } 45 | } 46 | } 47 | 48 | [suffix] 49 | Image { 50 | icon-name: "go-next-symbolic"; 51 | margin-start: 6; 52 | hexpand: true; 53 | halign: end; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/langs/javascript/Builder.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | import GLib from "gi://GLib"; 3 | 4 | import { buildRuntimePath } from "../../util.js"; 5 | 6 | export default function JavascriptBuilder() { 7 | async function run(text) { 8 | // We have to create a new file each time 9 | // because gjs doesn't appear to use etag for module caching 10 | // ?foo=Date.now() also does not work as expected 11 | // https://gitlab.gnome.org/GNOME/gjs/-/issues/618 12 | const path = buildRuntimePath(`workbench-${Date.now()}`); 13 | const file_javascript = Gio.File.new_for_path(path); 14 | await file_javascript.replace_contents_async( 15 | new GLib.Bytes(text), 16 | null, 17 | false, 18 | Gio.FileCreateFlags.NONE, 19 | null, 20 | ); 21 | 22 | let exports; 23 | try { 24 | exports = await import(`file://${file_javascript.get_path()}`); 25 | } catch (err) { 26 | console.error(err); 27 | return false; 28 | } finally { 29 | file_javascript 30 | .delete_async(GLib.PRIORITY_DEFAULT, null) 31 | .catch(console.error); 32 | } 33 | 34 | return exports; 35 | } 36 | 37 | return { run }; 38 | } 39 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-test-pass-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/cli/meson.build: -------------------------------------------------------------------------------- 1 | bin_conf = configuration_data() 2 | bin_conf.set('GJS', find_program('gjs').full_path()) 3 | bin_conf.set('version', meson.project_version() + version_suffix) 4 | bin_conf.set('app_id', app_id) 5 | bin_conf.set('prefix', prefix) 6 | bin_conf.set('libdir', get_option('prefix') / get_option('libdir')) 7 | bin_conf.set('datadir', datadir) 8 | bin_conf.set('pkgdatadir', pkgdatadir) 9 | bin_conf.set('sourcedir', meson.project_source_root()) 10 | 11 | gjspack = find_program('../../troll/gjspack/bin/gjspack') 12 | 13 | configure_file( 14 | input: 'bin.js', 15 | output: app_id + '.cli', 16 | configuration: bin_conf, 17 | install: true, 18 | install_dir: get_option('bindir') 19 | ) 20 | custom_target('workbench-cli', 21 | input: ['main.js'], 22 | output: app_id + '.cli.src.gresource', 23 | command: [ 24 | gjspack, 25 | '--appid=' + app_id + '.cli', 26 | '--prefix', '/re/sonny/Workbench', 27 | '--project-root', meson.project_source_root(), 28 | '--resource-root', meson.project_source_root() / 'src', 29 | '--no-executable', 30 | '@INPUT0@', 31 | '@OUTDIR@', 32 | ], 33 | install: true, 34 | install_dir: datadir / app_id + '.cli', 35 | build_always_stale: true, 36 | ) 37 | -------------------------------------------------------------------------------- /src/langs/css/CssDocument.js: -------------------------------------------------------------------------------- 1 | import Document from "../../Document.js"; 2 | import { applyTextEdits } from "../../lsp/sourceview.js"; 3 | import { setup } from "./css.js"; 4 | 5 | export class CssDocument extends Document { 6 | constructor(...args) { 7 | super(...args); 8 | 9 | this.lspc = setup({ document: this }); 10 | this.code_view.lspc = this.lspc; 11 | } 12 | 13 | async format() { 14 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 15 | const text_edits = await this.lspc.request("textDocument/formatting", { 16 | textDocument: { 17 | uri: this.file.get_uri(), 18 | }, 19 | options: { 20 | tabSize: 2, 21 | insertSpaces: true, 22 | trimTrailingWhitespace: true, 23 | insertFinalNewline: true, 24 | trimFinalNewlines: true, 25 | }, 26 | }); 27 | 28 | // GTKCssLanguageServer doesn't support diff - it just returns one edit 29 | // we don't want to loose the cursor position so we use this 30 | const state = this.code_view.saveState(); 31 | applyTextEdits(text_edits, this.buffer); 32 | await this.code_view.restoreState(state); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/langs/python/PythonDocument.js: -------------------------------------------------------------------------------- 1 | import { setup } from "./python.js"; 2 | 3 | import Document from "../../Document.js"; 4 | import { applyTextEdits } from "../../lsp/sourceview.js"; 5 | 6 | export class PythonDocument extends Document { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.lspc = setup({ document: this }); 11 | this.code_view.lspc = this.lspc; 12 | } 13 | 14 | async format() { 15 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 16 | const text_edits = await this.lspc.request("textDocument/formatting", { 17 | textDocument: { 18 | uri: this.file.get_uri(), 19 | }, 20 | options: { 21 | tabSize: 4, 22 | insertSpaces: true, 23 | trimTrailingWhitespace: true, 24 | insertFinalNewline: true, 25 | trimFinalNewlines: true, 26 | }, 27 | }); 28 | 29 | // lsp Ruff doesn't support diff - it just returns one edit 30 | // we don't want to loose the cursor position so we use this 31 | const state = this.code_view.saveState(); 32 | applyTextEdits(text_edits, this.buffer); 33 | await this.code_view.restoreState(state); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/langs/javascript/JavaScriptDocument.js: -------------------------------------------------------------------------------- 1 | import { setup } from "./javascript.js"; 2 | 3 | import Document from "../../Document.js"; 4 | import { applyTextEdits } from "../../lsp/sourceview.js"; 5 | 6 | export class JavaScriptDocument extends Document { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.lspc = setup({ document: this }); 11 | this.code_view.lspc = this.lspc; 12 | } 13 | 14 | async format() { 15 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 16 | const text_edits = await this.lspc.request("textDocument/formatting", { 17 | textDocument: { 18 | uri: this.file.get_uri(), 19 | }, 20 | options: { 21 | tabSize: 2, 22 | insertSpaces: true, 23 | trimTrailingWhitespace: true, 24 | insertFinalNewline: true, 25 | trimFinalNewlines: true, 26 | }, 27 | }); 28 | 29 | // Biome doesn't support diff - it just returns one edit 30 | // we don't want to loose the cursor position so we use this 31 | const state = this.code_view.saveState(); 32 | applyTextEdits(text_edits, this.buffer); 33 | await this.code_view.restoreState(state); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/langs/vala/ValaDocument.js: -------------------------------------------------------------------------------- 1 | import { setup as setupVala } from "./vala.js"; 2 | 3 | import Document from "../../Document.js"; 4 | import { applyTextEdits } from "../../lsp/sourceview.js"; 5 | 6 | export class ValaDocument extends Document { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.lspc = setupVala({ document: this }); 11 | this.code_view.lspc = this.lspc; 12 | } 13 | 14 | async format() { 15 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 16 | const text_edits = await this.lspc.request("textDocument/formatting", { 17 | textDocument: { 18 | uri: this.file.get_uri(), 19 | }, 20 | options: { 21 | tabSize: 4, 22 | insertSpaces: true, 23 | trimTrailingWhitespace: true, 24 | insertFinalNewline: true, 25 | trimFinalNewlines: true, 26 | }, 27 | }); 28 | 29 | // vala language server doesn't support diff - it just returns one edit 30 | // we don't want to loose the cursor position so we use this 31 | const state = this.code_view.saveState(); 32 | applyTextEdits(text_edits, this.buffer); 33 | await this.code_view.restoreState(state); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-gamepad-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Extensions/Extension.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | template $Extension: ListBoxRow { 4 | activatable: false; 5 | 6 | Box { 7 | orientation: vertical; 8 | 9 | CenterBox { 10 | height-request: 55; 11 | valign: center; 12 | halign: fill; 13 | margin-start: 14; 14 | margin-end: 14; 15 | 16 | [start] 17 | Label label_title {} 18 | 19 | [end] 20 | Image image_available { 21 | icon-name: "re.sonny.Workbench-test-pass-symbolic"; 22 | 23 | styles [ 24 | "success", 25 | ] 26 | } 27 | } 28 | 29 | Box installation_guide { 30 | visible: false; 31 | margin-start: 14; 32 | margin-end: 14; 33 | margin-bottom: 14; 34 | orientation: vertical; 35 | 36 | Label { 37 | wrap: true; 38 | xalign: 0; 39 | label: _("Run the following command"); 40 | 41 | styles [ 42 | "dim-label", 43 | ] 44 | } 45 | 46 | Label label_command { 47 | margin-top: 12; 48 | use-markup: true; 49 | wrap: true; 50 | wrap-mode: word_char; 51 | selectable: true; 52 | xalign: 0; 53 | 54 | styles [ 55 | "command_snippet", 56 | ] 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cli/rust.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | import { getLanguage } from "../common.js"; 4 | 5 | import { checkFile } from "./util.js"; 6 | 7 | const languageId = "rust"; 8 | 9 | export default async function rust({ file, lspc }) { 10 | print(` ${file.get_path()}`); 11 | 12 | const uri = file.get_uri(); 13 | let version = 0; 14 | 15 | const [contents] = await file.load_contents_async(null); 16 | const text = new TextDecoder().decode(contents); 17 | 18 | await lspc._notify("textDocument/didOpen", { 19 | textDocument: { 20 | uri, 21 | languageId, 22 | version: version++, 23 | text, 24 | }, 25 | }); 26 | 27 | // FIXME: rust analyzer doesn't publish diagnostics if there are none 28 | // probably we should switch to pulling diagnostics but unknown if supported 29 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_pullDiagnostics 30 | // await diagnose({ 31 | // file, 32 | // lspc, 33 | // languageId, 34 | // }); 35 | 36 | await checkFile({ 37 | lspc, 38 | file, 39 | lang: getLanguage(languageId), 40 | uri, 41 | }); 42 | 43 | await lspc._notify("textDocument/didClose", { 44 | textDocument: { 45 | uri, 46 | }, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/lsp/LSP.js: -------------------------------------------------------------------------------- 1 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity 2 | export const diagnostic_severities = { 3 | 1: "Error", 4 | 2: "Warning", 5 | 3: "Information", 6 | 4: "Hint", 7 | }; 8 | 9 | export class LSPError extends Error { 10 | constructor({ message, code, data }) { 11 | super(message); 12 | this.name = "LSPError"; 13 | this.code = code; 14 | this.data = data; 15 | } 16 | } 17 | 18 | export function rangeEquals(start, end) { 19 | return start.line === end.line && start.character === end.character; 20 | } 21 | 22 | export const CompletionItemKind = { 23 | Text: 1, 24 | Method: 2, 25 | Function: 3, 26 | Constructor: 4, 27 | Field: 5, 28 | Variable: 6, 29 | Class: 7, 30 | Interface: 8, 31 | Module: 9, 32 | Property: 10, 33 | Unit: 11, 34 | Value: 12, 35 | Enum: 13, 36 | Keyword: 14, 37 | Snippet: 15, 38 | Color: 16, 39 | File: 17, 40 | Reference: 18, 41 | Folder: 19, 42 | EnumMember: 20, 43 | Constant: 21, 44 | Struct: 22, 45 | Event: 23, 46 | Operator: 24, 47 | TypeParameter: 25, 48 | }; 49 | 50 | export const completion_item_kinds = Object.fromEntries( 51 | Object.entries(CompletionItemKind).map(([key, value]) => { 52 | return [value, key]; 53 | }), 54 | ); 55 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .view-toggler image { 2 | margin-right: 6px; 3 | } 4 | 5 | .hoverdisplay { 6 | padding: 8px; 7 | border-radius: 10px; 8 | } 9 | 10 | .panel_header { 11 | padding-left: 12px; 12 | padding-right: 12px; 13 | } 14 | 15 | .command_snippet { 16 | color: var(--view-fg-color); 17 | background: var(--view-bg-color); 18 | font-family: monospace; 19 | border-radius: 6px; 20 | padding: 6px; 21 | border: 1px solid var(--border-color); 22 | } 23 | 24 | /* 25 | * From Builder's stylesheet: 26 | * https://gitlab.gnome.org/GNOME/gnome-builder/-/blob/45d4ec9b6f1aa2236c9bb7dfb846b35f9b959618/src/libide/gui/style.css#L33 27 | * Used to style the language tags for the demo entries in the Library 28 | */ 29 | button.pill.small { 30 | font-size: 0.83333em; 31 | border-radius: 99px; 32 | margin: 0; 33 | padding: 1px 12px; 34 | } 35 | 36 | #panel_code, 37 | #panel_style, 38 | #panel_ui { 39 | border-right: solid 1px var(--border-color); 40 | background-color: #fcfcfc; 41 | } 42 | 43 | /* Prevent dev banner on second toolbar */ 44 | #panel_code > revealer, 45 | #panel_style > revealer, 46 | #panel_ui > revealer, 47 | #panel_preview > revealer, 48 | #panel_devtools > revealer { 49 | background-image: none; 50 | } 51 | 52 | #toolbar_devtools { 53 | background-color: #fcfcfc; 54 | } 55 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-external-link-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/langs/vala/vala.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | 3 | import { isValaAvailable } from "../../Extensions/Extensions.js"; 4 | import { createLSPClient } from "../../common.js"; 5 | import { getLanguage } from "../../util.js"; 6 | 7 | export function setup({ document }) { 8 | if (!isValaAvailable()) return; 9 | 10 | const { file, buffer, code_view } = document; 11 | 12 | const api_file = Gio.File.new_for_path(pkg.pkgdatadir).get_child( 13 | "workbench.vala", 14 | ); 15 | api_file.copy( 16 | file.get_parent().get_child("workbench.vala"), 17 | Gio.FileCopyFlags.OVERWRITE, 18 | null, 19 | null, 20 | ); 21 | 22 | const lspc = createLSPClient({ 23 | lang: getLanguage("vala"), 24 | root_uri: file.get_parent().get_uri(), 25 | quiet: true, 26 | }); 27 | lspc.buffer = buffer; 28 | lspc.uri = file.get_uri(); 29 | lspc.connect( 30 | "notification::textDocument/publishDiagnostics", 31 | (_self, params) => { 32 | if (params.uri !== file.get_uri()) { 33 | return; 34 | } 35 | code_view.handleDiagnostics(params.diagnostics); 36 | }, 37 | ); 38 | 39 | lspc.start().catch(console.error); 40 | buffer.connect("modified-changed", () => { 41 | if (!buffer.get_modified()) return; 42 | lspc.didChange().catch(console.error); 43 | }); 44 | 45 | return lspc; 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@babel/core": "^7.25.7", 5 | "@babel/eslint-parser": "^7.25.7", 6 | "@babel/plugin-syntax-import-attributes": "^7.25.7", 7 | "@rollup/plugin-commonjs": "^22.0.1", 8 | "@rollup/plugin-node-resolve": "^13.3.0", 9 | "eslint": "^8.48.0", 10 | "eslint-config-prettier": "^9.0.0", 11 | "eslint-plugin-import": "^2.28.1", 12 | "eslint-plugin-prettier": "^5.0.0", 13 | "events": "^3.3.0", 14 | "husky": "^8.0.3", 15 | "lint-staged": "^14.0.1", 16 | "ltx": "git://github.com/xmppjs/ltx.git#072690a43a51254ddd17b082131a8b9115586e8a", 17 | "postcss": "^8.4.14", 18 | "prettier": "3.0.3", 19 | "rollup": "^2.76.0", 20 | "rollup-plugin-ignore": "^1.0.10", 21 | "rollup-plugin-node-polyfills": "^0.2.1" 22 | }, 23 | "type": "module", 24 | "scripts": { 25 | "prepare": "husky install" 26 | }, 27 | "lint-staged": { 28 | "*.{json,md,yaml,yml}": "prettier --write", 29 | "*.{js,cjs,mjs}": "eslint --fix", 30 | "*.css": "./build-aux/fun workbench-cli format css", 31 | "*.py": "./build-aux/fun workbench-cli format python", 32 | "*.rs": "./build-aux/fun rustfmt --edition 2021", 33 | "*.blp": "./build-aux/fun workbench-cli format blueprint", 34 | "*.vala": "./build-aux/fun workbench-cli format vala" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "javascript.preferences.importModuleSpecifierEnding": "js", 4 | "typescript.preferences.importModuleSpecifierEnding": "js", 5 | "files.watcherExclude": { 6 | "**/.git/objects/**": true, 7 | "**/.git/subtree-cache/**": true, 8 | "**/.hg/store/**": true, 9 | "**/node_modules/*/**": true, 10 | "**/.flatpak": true, 11 | "**/src/lib": true, 12 | ".flatpak": true, 13 | ".flatpak/**": true, 14 | "_build/**": true 15 | }, 16 | "editor.defaultFormatter": "esbenp.prettier-vscode", 17 | "[yaml]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[json]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "rust-analyzer.server.path": "${workspaceFolder}/.flatpak/rust-analyzer.sh", 24 | "rust-analyzer.runnables.command": "${workspaceFolder}/.flatpak/cargo.sh", 25 | "rust-analyzer.files.excludeDirs": [ 26 | ".flatpak", 27 | ".flatpak-builder", 28 | "_build", 29 | "build", 30 | "builddir" 31 | ], 32 | "vala.languageServerPath": "${workspaceFolder}/.flatpak/vala-language-server.sh", 33 | "[meson]": { 34 | "editor.defaultFormatter": "mesonbuild.mesonbuild" 35 | }, 36 | "mesonbuild.configureOnOpen": false, 37 | "mesonbuild.buildFolder": "_build", 38 | "mesonbuild.mesonPath": "${workspaceFolder}/.flatpak/meson.sh" 39 | } 40 | -------------------------------------------------------------------------------- /src/cli/typescript.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | import { getLanguage } from "../common.js"; 4 | import { checkFile, getCodeObjectIds, diagnose, Interrupt } from "./util.js"; 5 | 6 | const languageId = "typescript"; 7 | 8 | export default async function typescript({ 9 | file, 10 | lspc, 11 | blueprint_object_ids, 12 | demo_dir, 13 | application, 14 | builder, 15 | template, 16 | window, 17 | }) { 18 | print(` ${file.get_path()}`); 19 | 20 | const text = await diagnose({ file, lspc, languageId }); 21 | 22 | await checkFile({ 23 | lspc, 24 | file, 25 | lang: getLanguage(languageId), 26 | uri: file.get_uri(), 27 | }); 28 | 29 | const js_object_ids = getCodeObjectIds(text); 30 | for (const object_id of js_object_ids) { 31 | if (!blueprint_object_ids.includes(object_id)) { 32 | print(` ❌ Reference to inexistant object id "${object_id}"`); 33 | throw new Interrupt(); 34 | } 35 | } 36 | 37 | globalThis.workbench = { 38 | window, 39 | application, 40 | builder, 41 | template, 42 | resolve(path) { 43 | return demo_dir.resolve_relative_path(path).get_uri(); 44 | }, 45 | preview() {}, 46 | }; 47 | 48 | await import(`file://${file.get_path()}`); 49 | print(" ✅ runs"); 50 | 51 | await lspc._notify("textDocument/didClose", { 52 | textDocument: { 53 | uri: file.get_uri(), 54 | }, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/cli/javascript.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | import { getLanguage } from "../common.js"; 4 | import { checkFile, getCodeObjectIds, diagnose, Interrupt } from "./util.js"; 5 | 6 | const languageId = "javascript"; 7 | 8 | export default async function javascript({ 9 | file, 10 | lspc, 11 | blueprint_object_ids, 12 | demo_dir, 13 | application, 14 | builder, 15 | template, 16 | window, 17 | }) { 18 | print(` ${file.get_path()}`); 19 | 20 | const text = await diagnose({ file, lspc, languageId }); 21 | 22 | await checkFile({ 23 | lspc: lspc, 24 | file: file, 25 | lang: getLanguage("javascript"), 26 | uri: file.get_uri(), 27 | }); 28 | 29 | const js_object_ids = getCodeObjectIds(text); 30 | for (const object_id of js_object_ids) { 31 | if (!blueprint_object_ids.includes(object_id)) { 32 | print(` ❌ Reference to inexistant object id "${object_id}"`); 33 | throw new Interrupt(); 34 | } 35 | } 36 | 37 | globalThis.workbench = { 38 | window, 39 | application, 40 | builder, 41 | template, 42 | resolve(path) { 43 | return demo_dir.resolve_relative_path(path).get_uri(); 44 | }, 45 | preview() {}, 46 | }; 47 | 48 | await import(`file://${file.get_path()}`); 49 | print(" ✅ runs"); 50 | 51 | await lspc._notify("textDocument/didClose", { 52 | textDocument: { 53 | uri: file.get_uri(), 54 | }, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/langs/rust/template/lib.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | mod code; 3 | pub(crate) mod workbench; 4 | 5 | use std::ffi::{c_char, CStr}; 6 | 7 | use glib::translate::FromGlibPtrFull; 8 | use gtk::glib; 9 | use libc::{c_int, EXIT_FAILURE, EXIT_SUCCESS}; 10 | 11 | static mut BUILDER: Option = None; 12 | static mut WINDOW: Option = None; 13 | static mut URI: Option = None; 14 | 15 | #[no_mangle] 16 | extern "C" fn main() -> c_int { 17 | code::main(); 18 | EXIT_SUCCESS 19 | } 20 | 21 | #[no_mangle] 22 | extern "C" fn set_builder(builder_ptr: *mut gtk::ffi::GtkBuilder) -> c_int { 23 | unsafe { 24 | let builder = gtk::Builder::from_glib_full(builder_ptr); 25 | BUILDER = Some(builder.clone()); 26 | } 27 | EXIT_SUCCESS 28 | } 29 | 30 | #[no_mangle] 31 | extern "C" fn set_window(window_ptr: *mut adw::ffi::AdwWindow) -> c_int { 32 | unsafe { 33 | let window = adw::Window::from_glib_full(window_ptr); 34 | WINDOW = Some(window.clone()); 35 | } 36 | EXIT_SUCCESS 37 | } 38 | 39 | #[no_mangle] 40 | extern "C" fn set_base_uri(c_string: *const c_char) -> c_int { 41 | unsafe { 42 | if c_string.is_null() { 43 | return EXIT_FAILURE; 44 | } 45 | 46 | let c_str = CStr::from_ptr(c_string); 47 | if let Ok(str_slice) = c_str.to_str() { 48 | URI = Some(str_slice.to_string()); 49 | } 50 | } 51 | EXIT_SUCCESS 52 | } 53 | -------------------------------------------------------------------------------- /src/langs/typescript/types/ambient.d.ts: -------------------------------------------------------------------------------- 1 | // import Adw from "gi://Adw"; 2 | // import Gtk from "gi://Gtk?version=4.0"; 3 | // import GObject from "gi://GObject"; 4 | 5 | // additional type declarations for GJS 6 | 7 | // additional GJS log utils 8 | declare function print(...args: any[]): void; 9 | declare function log(...args: any[]): void; 10 | 11 | // GJS pkg global 12 | declare const pkg: { 13 | version: string; 14 | name: string; 15 | }; 16 | 17 | // old GJS global imports 18 | // used like: imports.format.printf("..."); 19 | declare module imports { 20 | // format import 21 | const format: { 22 | format(this: String, ...args: any[]): string; 23 | printf(fmt: string, ...args: any[]): string; 24 | vprintf(fmt: string, args: any[]): string; 25 | }; 26 | } 27 | 28 | // gettext import 29 | declare module "gettext" { 30 | export function gettext(id: string): string; 31 | export function ngettext( 32 | singular: string, 33 | plural: string, 34 | n: number, 35 | ): string; 36 | } 37 | 38 | // TODO: uncomment correct typings after we switch to `ts-for-gir` 39 | // declare const workbench: { 40 | // window: Adw.ApplicationWindow; 41 | // application: Adw.Application; 42 | // builder: Gtk.Builder; 43 | // template: string; 44 | // resolve(path: string): string; 45 | // preview(object: Gtk.Widget): void; 46 | // build(params: Record): void; 47 | // }; 48 | 49 | // global workbench object 50 | declare const workbench: any; 51 | -------------------------------------------------------------------------------- /src/Previewer/previewer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = configure_file( 2 | input: 'app.desktop', 3 | output: '@0@.desktop'.format(app_id), 4 | configuration: { 'app_id': app_id }, 5 | install_dir: get_option('datadir') / 'applications' 6 | ) 7 | 8 | desktop_utils = find_program('desktop-file-validate', required: true) 9 | test('Validate desktop file', desktop_utils, 10 | args: [desktop_file] 11 | ) 12 | 13 | configure_file( 14 | input: 'app.service', 15 | output: '@0@.service'.format(app_id), 16 | configuration: { 'app_id': app_id, 'bindir': bindir }, 17 | install_dir: get_option('datadir') / 'dbus-1/services' 18 | ) 19 | 20 | appstream_file = configure_file( 21 | input: 'app.metainfo.xml', 22 | output: '@0@.metainfo.xml'.format(app_id), 23 | configuration: { 'app_id': app_id }, 24 | install_dir: get_option('datadir') / 'metainfo' 25 | ) 26 | 27 | appstreamcli = find_program('appstreamcli', required: false) 28 | test( 29 | 'Validate appstream file', 30 | appstreamcli, 31 | args: ['validate', '--no-net', '--explain', appstream_file], 32 | ) 33 | 34 | configure_file( 35 | input: 'app.gschema.xml', 36 | output: '@0@.gschema.xml'.format(app_id), 37 | configuration: { 'app_id': app_id }, 38 | install_dir: get_option('datadir') / 'glib-2.0/schemas' 39 | ) 40 | 41 | compile_schemas = find_program('glib-compile-schemas', required: true) 42 | test('Validate schema file', compile_schemas, 43 | args: ['--strict', '--dry-run', meson.current_source_dir()] 44 | ) 45 | 46 | install_subdir('icons/hicolor', install_dir : get_option('datadir') / 'icons') 47 | -------------------------------------------------------------------------------- /src/cli/format.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | import Gtk from "gi://Gtk"; 3 | 4 | import { applyTextEdits } from "../lsp/sourceview.js"; 5 | 6 | export default async function format({ filenames, lang, lspc }) { 7 | const success = true; 8 | 9 | for await (const filename of filenames) { 10 | const file = Gio.File.new_for_path(filename); 11 | const [contents] = await file.load_contents_async(null); 12 | const text = new TextDecoder().decode(contents); 13 | const buffer = new Gtk.TextBuffer({ text }); 14 | 15 | const uri = file.get_uri(); 16 | const languageId = lang.id; 17 | let version = 0; 18 | 19 | await lspc._notify("textDocument/didOpen", { 20 | textDocument: { 21 | uri, 22 | languageId, 23 | version: version++, 24 | text: buffer.text, 25 | }, 26 | }); 27 | 28 | await formatting({ buffer, uri, lang, lspc }); 29 | 30 | await file.replace_contents_async( 31 | new TextEncoder().encode(buffer.text), 32 | null, 33 | false, 34 | Gio.FileCreateFlags.NONE, 35 | null, 36 | ); 37 | } 38 | 39 | return success; 40 | } 41 | 42 | export async function formatting({ buffer, uri, lang, lspc }) { 43 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 44 | const text_edits = await lspc._request("textDocument/formatting", { 45 | textDocument: { 46 | uri, 47 | }, 48 | options: lang.formatting_options, 49 | }); 50 | 51 | applyTextEdits(text_edits, buffer); 52 | } 53 | -------------------------------------------------------------------------------- /src/langs/blueprint/BlueprintDocument.js: -------------------------------------------------------------------------------- 1 | import Document from "../../Document.js"; 2 | import { applyTextEdits } from "../../lsp/sourceview.js"; 3 | 4 | import { setup } from "./blueprint.js"; 5 | 6 | export class BlueprintDocument extends Document { 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.lspc = setup({ document: this }); 11 | this.code_view.lspc = this.lspc; 12 | } 13 | async update() { 14 | return this.lspc.didChange(); 15 | } 16 | async compile() { 17 | await this.update(); 18 | 19 | let xml = null; 20 | 21 | try { 22 | ({ xml } = await this.lspc.request("textDocument/x-blueprint-compile", { 23 | textDocument: { 24 | uri: this.file.get_uri(), 25 | }, 26 | })); 27 | } catch (err) { 28 | console.debug(err); 29 | } 30 | 31 | return xml; 32 | } 33 | async decompile(text) { 34 | const { blp } = await this.lspc.request("x-blueprint/decompile", { 35 | text, 36 | }); 37 | return blp; 38 | } 39 | async format() { 40 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting 41 | const text_edits = await this.lspc.request("textDocument/formatting", { 42 | textDocument: { 43 | uri: this.file.get_uri(), 44 | }, 45 | options: { 46 | tabSize: 2, 47 | insertSpaces: true, 48 | trimTrailingWhitespace: true, 49 | insertFinalNewline: true, 50 | trimFinalNewlines: true, 51 | }, 52 | }); 53 | 54 | applyTextEdits(text_edits, this.buffer); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/icons/re.sonny.Workbench-network-wireless-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/langs/javascript/javascript.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | 3 | import { createLSPClient } from "../../common.js"; 4 | import { getLanguage, copy } from "../../util.js"; 5 | 6 | export function setup({ document }) { 7 | const { file, buffer, code_view } = document; 8 | 9 | const lspc = createLSPClient({ 10 | lang: getLanguage("javascript"), 11 | root_uri: file.get_parent().get_uri(), 12 | quiet: true, 13 | }); 14 | lspc.buffer = buffer; 15 | lspc.uri = file.get_uri(); 16 | lspc.connect( 17 | "notification::textDocument/publishDiagnostics", 18 | (_self, params) => { 19 | if (params.uri !== file.get_uri()) { 20 | return; 21 | } 22 | code_view.handleDiagnostics(params.diagnostics); 23 | }, 24 | ); 25 | 26 | lspc.start().catch(console.error); 27 | 28 | buffer.connect("modified-changed", () => { 29 | if (!buffer.get_modified()) return; 30 | lspc.didChange().catch(console.error); 31 | }); 32 | 33 | return lspc; 34 | } 35 | 36 | const javascript_template_dir = Gio.File.new_for_path( 37 | pkg.pkgdatadir, 38 | ).resolve_relative_path("langs/javascript/template"); 39 | 40 | export async function setupJavascriptProject(destination, document) { 41 | const destination_file = await copy( 42 | "jsconfig.json", 43 | javascript_template_dir, 44 | destination, 45 | Gio.FileCopyFlags.NONE, 46 | ); 47 | 48 | // Notify the language server that the jsconfig file was created 49 | // to initialize diagnostics and type checkings 50 | await document.lspc.notify("workspace/didCreateFile", { 51 | files: [{ uri: destination_file.get_uri() }], 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/widgets/CodeFind.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | template $CodeFind: Revealer { 4 | transition-type: slide_up; 5 | reveal-child: false; 6 | 7 | Box { 8 | orientation: vertical; 9 | 10 | Separator {} 11 | 12 | Box { 13 | styles [ 14 | "toolbar", 15 | ] 16 | 17 | halign: center; 18 | 19 | Box { 20 | valign: center; 21 | width-request: 220; 22 | css-name: "entry"; 23 | 24 | Image { 25 | icon-name: 'edit-find-symbolic'; 26 | } 27 | 28 | Text text_search_term { 29 | hexpand: true; 30 | vexpand: true; 31 | width-chars: 10; 32 | max-width-chars: 10; 33 | } 34 | 35 | Label label_info { 36 | label: ""; 37 | xalign: 1; 38 | opacity: 0.5; 39 | } 40 | } 41 | 42 | Box { 43 | valign: center; 44 | 45 | Button button_previous { 46 | icon-name: "re.sonny.Workbench-up-symbolic"; 47 | tooltip-text: _("Move to previous match (Ctrl+Shift+G)"); 48 | sensitive: false; 49 | clicked => $onSearchPrevious(); 50 | } 51 | 52 | Button button_next { 53 | icon-name: "re.sonny.Workbench-down-symbolic"; 54 | tooltip-text: _("Move to next match (Ctrl+G)"); 55 | sensitive: false; 56 | clicked => $onSearchNext(); 57 | } 58 | } 59 | 60 | Button { 61 | tooltip-text: _("Close Search"); 62 | icon-name: "window-close-symbolic"; 63 | 64 | styles [ 65 | "circular", 66 | "small", 67 | ] 68 | 69 | clicked => $onClose(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/langs/typescript/typescript.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | 3 | import { createLSPClient } from "../../common.js"; 4 | import { getLanguage, copy } from "../../util.js"; 5 | import { isTypeScriptAvailable } from "../../Extensions/Extensions.js"; 6 | 7 | export function setup({ document }) { 8 | if (!isTypeScriptAvailable()) return; 9 | 10 | const { file, buffer, code_view } = document; 11 | 12 | const lspc = createLSPClient({ 13 | lang: getLanguage("typescript"), 14 | root_uri: file.get_parent().get_uri(), 15 | quiet: true, 16 | }); 17 | lspc.buffer = buffer; 18 | lspc.uri = file.get_uri(); 19 | lspc.connect( 20 | "notification::textDocument/publishDiagnostics", 21 | (_self, params) => { 22 | if (params.uri !== file.get_uri()) { 23 | return; 24 | } 25 | code_view.handleDiagnostics(params.diagnostics); 26 | }, 27 | ); 28 | 29 | lspc.start().catch(console.error); 30 | 31 | buffer.connect("modified-changed", () => { 32 | if (!buffer.get_modified()) return; 33 | lspc.didChange().catch(console.error); 34 | }); 35 | 36 | return lspc; 37 | } 38 | 39 | const typescript_template_dir = Gio.File.new_for_path( 40 | pkg.pkgdatadir, 41 | ).resolve_relative_path("langs/typescript/template"); 42 | 43 | export async function setupTypeScriptProject(destination, document) { 44 | const destination_file = await copy( 45 | "tsconfig.json", 46 | typescript_template_dir, 47 | destination, 48 | Gio.FileCopyFlags.NONE, 49 | ); 50 | 51 | // Notify the language server that the tsconfig file was created 52 | // to initialized diagnostics and type checkings 53 | await document.lspc.notify("workspace/didCreateFile", { 54 | files: [{ uri: destination_file.get_uri() }], 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/langs/typescript/Compiler.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | import GLib from "gi://GLib"; 3 | 4 | import { buildRuntimePath, copy } from "../../util.js"; 5 | 6 | export default function Compiler({ session }) { 7 | const { file } = session; 8 | 9 | async function compile() { 10 | const tsc_launcher = new Gio.SubprocessLauncher(); 11 | tsc_launcher.set_cwd(file.get_path()); 12 | 13 | const tsc = tsc_launcher.spawnv(["tsc", "--project", file.get_path()]); 14 | await tsc.wait_async(null); 15 | 16 | const result = tsc.get_successful(); 17 | tsc_launcher.close(); 18 | return result; 19 | } 20 | 21 | async function run() { 22 | // We have to create a new file each time 23 | // because gjs doesn't appear to use etag for module caching 24 | // ?foo=Date.now() also does not work as expected 25 | // https://gitlab.gnome.org/GNOME/gjs/-/issues/618 26 | const path = buildRuntimePath(`workbench-${Date.now()}`); 27 | const compiled_dir = Gio.File.new_for_path(path); 28 | if (!compiled_dir.query_exists(null)) { 29 | await compiled_dir.make_directory_async(GLib.PRIORITY_DEFAULT, null); 30 | } 31 | await copy( 32 | "main.js", 33 | file.get_child("compiled_javascript"), 34 | compiled_dir, 35 | Gio.FileCopyFlags.NONE, 36 | ); 37 | const compiled_file = compiled_dir.get_child("main.js"); 38 | 39 | let exports; 40 | try { 41 | exports = await import(`file://${compiled_file.get_path()}`); 42 | } catch (err) { 43 | console.error(err); 44 | return false; 45 | } finally { 46 | compiled_file 47 | .delete_async(GLib.PRIORITY_DEFAULT, null) 48 | .catch(console.error); 49 | } 50 | 51 | return exports; 52 | } 53 | 54 | return { compile, run }; 55 | } 56 | -------------------------------------------------------------------------------- /src/langs/rust/rust.js: -------------------------------------------------------------------------------- 1 | import Gio from "gi://Gio"; 2 | 3 | import { createLSPClient } from "../../common.js"; 4 | import { getLanguage, copy } from "../../util.js"; 5 | import { isRustAvailable } from "../../Extensions/Extensions.js"; 6 | 7 | export function setup({ document }) { 8 | if (!isRustAvailable()) return; 9 | 10 | const { file, buffer, code_view } = document; 11 | 12 | const lspc = createLSPClient({ 13 | lang: getLanguage("rust"), 14 | root_uri: file.get_parent().get_uri(), 15 | quiet: true, 16 | }); 17 | lspc.buffer = buffer; 18 | lspc.uri = file.get_uri(); 19 | lspc.connect( 20 | "notification::textDocument/publishDiagnostics", 21 | (_self, params) => { 22 | if (params.uri !== file.get_uri()) { 23 | return; 24 | } 25 | code_view.handleDiagnostics(params.diagnostics); 26 | }, 27 | ); 28 | 29 | lspc.start().catch(console.error); 30 | 31 | buffer.connect("modified-changed", () => { 32 | if (!buffer.get_modified()) return; 33 | lspc.didChange().catch(console.error); 34 | }); 35 | 36 | return lspc; 37 | } 38 | 39 | const rust_template_dir = Gio.File.new_for_path( 40 | pkg.pkgdatadir, 41 | ).resolve_relative_path("langs/rust/template"); 42 | 43 | export async function setupRustProject(destination) { 44 | return Promise.all([ 45 | copy("Cargo.toml", rust_template_dir, destination, Gio.FileCopyFlags.NONE), 46 | copy("Cargo.lock", rust_template_dir, destination, Gio.FileCopyFlags.NONE), 47 | ]); 48 | } 49 | 50 | export async function installRustLibraries(destination) { 51 | return Promise.all([ 52 | copy("lib.rs", rust_template_dir, destination, Gio.FileCopyFlags.OVERWRITE), 53 | copy( 54 | "workbench.rs", 55 | rust_template_dir, 56 | destination, 57 | Gio.FileCopyFlags.OVERWRITE, 58 | ), 59 | ]); 60 | } 61 | -------------------------------------------------------------------------------- /src/Extensions/Extension.js: -------------------------------------------------------------------------------- 1 | import GObject from "gi://GObject"; 2 | import Gtk from "gi://Gtk"; 3 | 4 | import Template from "./Extension.blp" with { type: "uri" }; 5 | 6 | export default GObject.registerClass( 7 | { 8 | GTypeName: "Extension", 9 | Template, 10 | InternalChildren: [ 11 | "label_title", 12 | "image_available", 13 | "installation_guide", 14 | "label_command", 15 | ], 16 | Properties: { 17 | title: GObject.ParamSpec.string( 18 | "title", 19 | "", 20 | "", 21 | GObject.ParamFlags.READWRITE, 22 | "", 23 | ), 24 | available: GObject.ParamSpec.boolean( 25 | "available", 26 | "", 27 | "", 28 | GObject.ParamFlags.READWRITE, 29 | false, 30 | ), 31 | command: GObject.ParamSpec.string( 32 | "command", 33 | "", 34 | "", 35 | GObject.ParamFlags.READWRITE, 36 | "", 37 | ), 38 | }, 39 | }, 40 | class Extension extends Gtk.ListBoxRow { 41 | constructor(properties = {}) { 42 | super(properties); 43 | 44 | this.bind_property( 45 | "title", 46 | this._label_title, 47 | "label", 48 | GObject.BindingFlags.SYNC_CREATE, 49 | ); 50 | 51 | this.bind_property( 52 | "available", 53 | this._image_available, 54 | "visible", 55 | GObject.BindingFlags.SYNC_CREATE, 56 | ); 57 | 58 | this.bind_property( 59 | "available", 60 | this._installation_guide, 61 | "visible", 62 | GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN, 63 | ); 64 | 65 | this.bind_property( 66 | "command", 67 | this._label_command, 68 | "label", 69 | GObject.BindingFlags.SYNC_CREATE, 70 | ); 71 | } 72 | }, 73 | ); 74 | -------------------------------------------------------------------------------- /.github/workflows/CI.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # See also https://github.com/flatpak/flatpak-github-actions 4 | 5 | on: 6 | pull_request: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | CI: 12 | runs-on: ubuntu-24.04 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: recursive 17 | 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | cache: "npm" 22 | 23 | - name: Install host dependencies 24 | run: | 25 | sudo apt-get install flatpak mutter flatpak-builder 26 | 27 | # Restore caches 28 | - name: Restore Flatpak dependencies 29 | uses: actions/cache/restore@v3 30 | with: 31 | path: ~/.local/share/flatpak 32 | key: ${{ runner.os }}-flatpak-dependencies-${{ github.run_id }} 33 | restore-keys: | 34 | ${{ runner.os }}-flatpak-dependencies- 35 | - name: Restore .flatpak-builder 36 | uses: actions/cache/restore@v3 37 | with: 38 | path: .flatpak-builder 39 | key: ${{ runner.os }}-flatpak-builder-${{ github.run_id }} 40 | restore-keys: | 41 | ${{ runner.os }}-flatpak-builder- 42 | 43 | - run: mutter --wayland --no-x11 --headless --wayland-display=wayland-0 --virtual-monitor 1280x720 > /tmp/mutter.log 2>&1 & 44 | - run: make ci 45 | - run: cat /tmp/mutter.log 46 | 47 | # Save caches 48 | - name: Save Flatpak dependencies 49 | uses: actions/cache/save@v3 50 | if: always() 51 | with: 52 | path: ~/.local/share/flatpak 53 | key: ${{ runner.os }}-flatpak-dependencies-${{ github.run_id }} 54 | - name: Save .flatpak-builder 55 | uses: actions/cache/save@v3 56 | if: always() 57 | with: 58 | path: .flatpak-builder 59 | key: ${{ runner.os }}-flatpak-builder-${{ github.run_id }} 60 | -------------------------------------------------------------------------------- /src/cli/vala.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | import Gio from "gi://Gio"; 4 | 5 | import { getLanguage } from "../common.js"; 6 | import { checkFile, diagnose } from "./util.js"; 7 | 8 | const languageId = "vala"; 9 | 10 | export default async function vala({ file, lspc, demo_dir }) { 11 | print(` ${file.get_path()}`); 12 | 13 | const file_api = Gio.File.new_for_path(pkg.pkgdatadir).get_child( 14 | "workbench.vala", 15 | ); 16 | file_api.copy( 17 | demo_dir.get_child("workbench.vala"), 18 | Gio.FileCopyFlags.OVERWRITE, 19 | null, 20 | null, 21 | ); 22 | 23 | await diagnose({ 24 | file, 25 | lspc, 26 | languageId, 27 | filter(diagnostic) { 28 | // FIXME: deprecated features, no replacement? 29 | if (demo_dir.get_basename() === "Text Fields") { 30 | const ignore_for_text_fields = [ 31 | "`Gtk.EntryCompletion' has been deprecated since 4.10", 32 | "`Gtk.Entry.completion' has been deprecated since 4.10", 33 | "`Gtk.ListStore' has been deprecated since 4.10", 34 | "`Gtk.TreeIter' has been deprecated since 4.10", 35 | ]; 36 | return !ignore_for_text_fields.includes(diagnostic.message); 37 | // Gtk.StyleContext class is deprecated but not the following methods 38 | // gtk_style_context_add_provider_for_display 39 | // gtk_style_context_remove_provider_for_display 40 | } else if (demo_dir.get_basename() === "CSS Gradients") { 41 | return ( 42 | diagnostic.message !== 43 | "`Gtk.StyleContext' has been deprecated since 4.10" 44 | ); 45 | } 46 | return true; 47 | }, 48 | }); 49 | 50 | await checkFile({ 51 | lspc, 52 | file, 53 | lang: getLanguage("vala"), 54 | uri: file.get_uri(), 55 | }); 56 | 57 | await lspc._notify("textDocument/didClose", { 58 | textDocument: { 59 | uri: file.get_uri(), 60 | }, 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /src/overrides.js: -------------------------------------------------------------------------------- 1 | import system from "system"; 2 | import GObject from "gi://GObject"; 3 | 4 | /* 5 | * These overrides only exist to make documentation examples 6 | * work in Workbench - keep them to a minimum 7 | */ 8 | 9 | export const registerClass = GObject.registerClass; 10 | 11 | const types = Object.create(null); 12 | 13 | function increment(name) { 14 | return (types[name] || 0) + 1; 15 | } 16 | 17 | export function overrides() { 18 | // Makes the app unersponsive - by blocking the mainloop I presume. 19 | // Anyway, code shouldn't be able to exit 20 | system.exit = function exit(code) { 21 | console.log(`Intercepted exit with status "${code}"`); 22 | }; 23 | 24 | // GTypeName must be unique globally 25 | // there is no unregister equivalent to registerClass and 26 | // this is what GNOME Shell does too according to Verdre 27 | // https://github.com/workbenchdev/Workbench/issues/50 28 | // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1011#note_451228 29 | // https://gitlab.gnome.org/GNOME/glib/-/issues/282#note_662735 30 | // https://gitlab.gnome.org/GNOME/glib/-/issues/2336 31 | // https://gitlab.gnome.org/GNOME/glib/-/issues/667 32 | GObject.registerClass = function registerWorkbenchClass(...args) { 33 | let attrs; 34 | let klass; 35 | 36 | if (args.length === 1) { 37 | attrs = {}; 38 | klass = args[0]; 39 | } else { 40 | attrs = args[0]; 41 | klass = args[1]; 42 | } 43 | 44 | const GTypeName = attrs.GTypeName || klass.name; 45 | if (GTypeName) { 46 | types[GTypeName] = increment(GTypeName); 47 | attrs.GTypeName = GTypeName + types[GTypeName]; 48 | } 49 | return registerClass(attrs, klass); 50 | }; 51 | // This is used to tweak `workbench.template` in order to set the 52 | //