├── .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 |
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 | // to something that will work next time
53 | // Object.registerClass is called with the corresponding GTypeName
54 | }
55 |
56 | export function getClassNameType(name) {
57 | return name + increment(name);
58 | }
59 |
--------------------------------------------------------------------------------
/src/langs/vala/Compiler.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 | import dbus_previewer from "../../Previewer/DBusPreviewer.js";
3 | import { decode } from "../../util.js";
4 |
5 | export default function ValaCompiler({ session }) {
6 | const { file } = session;
7 |
8 | const module_file = file.get_child("libworkbenchcode.so");
9 | const file_vala = file.get_child("main.vala");
10 |
11 | async function compile() {
12 | let args;
13 |
14 | try {
15 | const [contents] = await file_vala.load_contents_async(null);
16 | const code = decode(contents);
17 | args = getValaCompilerArguments(code);
18 | } catch (error) {
19 | console.debug(error);
20 | return;
21 | }
22 |
23 | const valac_launcher = new Gio.SubprocessLauncher();
24 | valac_launcher.set_cwd(file.get_path());
25 | const valac = valac_launcher.spawnv([
26 | "valac",
27 | file_vala.get_path(),
28 | "--hide-internal",
29 | "-X",
30 | "-shared",
31 | "-X",
32 | "-fpic",
33 | "--library",
34 | "workbench",
35 | "-o",
36 | module_file.get_path(),
37 | "--vapi",
38 | "/dev/null",
39 | ...args,
40 | ]);
41 |
42 | await valac.wait_async(null);
43 |
44 | const result = valac.get_successful();
45 | valac_launcher.close();
46 | return result;
47 | }
48 |
49 | async function run() {
50 | try {
51 | const proxy = await dbus_previewer.getProxy("vala");
52 | await proxy.RunAsync(module_file.get_path(), session.file.get_uri());
53 | } catch (err) {
54 | console.error(err);
55 | return false;
56 | }
57 |
58 | return true;
59 | }
60 |
61 | return { compile, run };
62 | }
63 |
64 | // Takes a string starting with the line
65 | // #!/usr/bin/env -S vala workbench.vala --pkg gtk4 --pkg libadwaita-1
66 | // and return ["--pkg", "gtk4", "--pkg", "libadwaita-1"]
67 | // FIXME: consider using https://docs.gtk.org/glib/struct.OptionContext.html instead
68 | function getValaCompilerArguments(text) {
69 | return text.split("\n")[0]?.split("-S vala ")[1]?.split(" ") || [];
70 | }
71 |
--------------------------------------------------------------------------------
/src/langs/blueprint/blueprint.js:
--------------------------------------------------------------------------------
1 | import GLib from "gi://GLib";
2 |
3 | import { createLSPClient } from "../../common.js";
4 | import { getLanguage } from "../../util.js";
5 | import { CompletionItemKind } from "../../lsp/LSP.js";
6 |
7 | export function setup({ document }) {
8 | const { file, code_view, buffer } = document;
9 |
10 | const lspc = createLSPClient({
11 | lang: getLanguage("blueprint"),
12 | root_uri: file.get_parent().get_uri(),
13 | quiet: true,
14 | });
15 | lspc.buffer = buffer;
16 | lspc.uri = file.get_uri();
17 | lspc.connect(
18 | "notification::textDocument/publishDiagnostics",
19 | (_self, params) => {
20 | if (params.uri !== file.get_uri()) {
21 | return;
22 | }
23 | code_view.handleDiagnostics(params.diagnostics);
24 | },
25 | );
26 |
27 | lspc.start().catch(console.error);
28 |
29 | return lspc;
30 | }
31 |
32 | const SYSLOG_IDENTIFIER = pkg.name;
33 |
34 | export function logBlueprintError(err) {
35 | GLib.log_structured("Blueprint", GLib.LogLevelFlags.LEVEL_CRITICAL, {
36 | MESSAGE: `${err.message}`,
37 | SYSLOG_IDENTIFIER,
38 | });
39 | }
40 |
41 | export function logBlueprintInfo(info) {
42 | GLib.log_structured("Blueprint", GLib.LogLevelFlags.LEVEL_WARNING, {
43 | MESSAGE: `${info.line + 1}:${info.col} ${info.message}`,
44 | SYSLOG_IDENTIFIER,
45 | });
46 | }
47 |
48 | export function sortBlueprintProposals(a, b) {
49 | if (a.kind === b.kind) return 0;
50 |
51 | if (a.kind === CompletionItemKind.Property) return -1;
52 | if (b.kind === CompletionItemKind.Property) return 1;
53 | if (a.kind === CompletionItemKind.Snippet) return -1;
54 | if (b.kind === CompletionItemKind.Snippet) return 1;
55 | if (a.kind === CompletionItemKind.Keyword) return -1;
56 | if (b.kind === CompletionItemKind.Keyword) return 1;
57 | if (a.kind === CompletionItemKind.Event) return -1;
58 | if (b.kind === CompletionItemKind.Event) return 1;
59 | if (a.kind === CompletionItemKind.Class) return -1;
60 | if (b.kind === CompletionItemKind.Class) return 1;
61 |
62 | return a.sortText.localeCompare(b.sortText);
63 | }
64 |
--------------------------------------------------------------------------------
/src/langs/xml/xml.js:
--------------------------------------------------------------------------------
1 | // elint-disable-next-line import/named
2 | import {
3 | escapeXML,
4 | escapeXMLText,
5 | SaxLtx,
6 | parse,
7 | Element,
8 | createElement,
9 | } from "../../lib/ltx.js";
10 |
11 | // adapted from ltx.stringify to work without Element and ignore whitespace
12 | // and mixed content in order to use the same algo as blueprint-compiler xml_emitter.py
13 | function format(str, indent = 2) {
14 | const p = new SaxLtx();
15 | if (typeof indent === "number") indent = " ".repeat(indent);
16 |
17 | let level = 0;
18 | let current_tag;
19 | let needs_newline = false;
20 | let s = '';
21 |
22 | function _indent() {
23 | s += "\n" + indent.repeat(level);
24 | }
25 |
26 | p.on("startElement", (name, attrs) => {
27 | // console.debug("startElement", name, attrs);
28 | current_tag = name;
29 | _indent();
30 |
31 | s += `<${name}`;
32 |
33 | for (const k in attrs) {
34 | const v = attrs[k];
35 | s += ` ${k}="${escapeXML(v)}"`;
36 | }
37 |
38 | level++;
39 | needs_newline = false;
40 | });
41 | p.on("endElement", (name, self_closing) => {
42 | // console.debug("endElement", name, self_closing);
43 | if (current_tag && name !== current_tag) {
44 | throw new Error("Invalid XML document");
45 | }
46 |
47 | level--;
48 |
49 | if (needs_newline) {
50 | _indent();
51 | }
52 |
53 | if (self_closing) {
54 | s += "/>";
55 | } else {
56 | if (current_tag) {
57 | s += ">";
58 | }
59 |
60 | s += `${name}>`;
61 | }
62 |
63 | current_tag = null;
64 | needs_newline = true;
65 | });
66 | p.on("text", (str) => {
67 | // console.debug("text", str);
68 | if (current_tag) {
69 | s += ">";
70 | current_tag = null;
71 | }
72 |
73 | str = str.trim();
74 | if (!str) return;
75 |
76 | s += escapeXMLText(str);
77 | needs_newline = false;
78 | });
79 |
80 | p.write(str);
81 |
82 | if (level !== 0) {
83 | throw new Error("Invalid XML document");
84 | }
85 |
86 | return s;
87 | }
88 |
89 | export { parse, Element, format, createElement };
90 |
--------------------------------------------------------------------------------
/src/Permissions/Permissions.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 | import Gtk from "gi://Gtk";
3 |
4 | import { build } from "../../troll/src/main.js";
5 |
6 | import Interface from "./Permissions.blp" with { type: "uri" };
7 |
8 | import illustration from "./permissions.svg";
9 |
10 | import {
11 | getFlatpakId,
12 | getFlatpakInfo,
13 | isDeviceInputOverrideAvailable,
14 | } from "../flatpak.js";
15 |
16 | const device = isDeviceInputOverrideAvailable() ? "input" : "all";
17 |
18 | const action_permissions = new Gio.SimpleAction({
19 | name: "permissions",
20 | parameter_type: null,
21 | });
22 |
23 | export function Permissions({ window }) {
24 | const {
25 | dialog,
26 | picture_illustration,
27 | label_command,
28 | button_info,
29 | action_row_device,
30 | } = build(Interface);
31 |
32 | picture_illustration.set_resource(illustration);
33 |
34 | label_command.label = `flatpak override --user --share=network --socket=pulseaudio --device=${device} ${getFlatpakId()}`;
35 | action_row_device.title = `--input=${device}`;
36 |
37 | button_info.connect("clicked", () => {
38 | new Gtk.UriLauncher({
39 | uri: "https://docs.flatpak.org/en/latest/sandbox-permissions.html",
40 | })
41 | .launch(window, null)
42 | .catch(console.error);
43 | });
44 |
45 | action_permissions.connect("activate", () => {
46 | dialog.present(window);
47 | });
48 |
49 | window.add_action(action_permissions);
50 | }
51 |
52 | const missing_permissions = (() => {
53 | const flatpak_info = getFlatpakInfo();
54 | const shared = flatpak_info.get_string_list("Context", "shared");
55 | const sockets = flatpak_info.get_string_list("Context", "sockets");
56 | const devices = flatpak_info.get_string_list("Context", "devices");
57 |
58 | return (
59 | !shared.includes("network") ||
60 | !sockets.includes("pulseaudio") ||
61 | !devices.includes(device)
62 | );
63 | })();
64 |
65 | export function needsAdditionalPermissions({ demo }) {
66 | if (!demo["flatpak-finish-args"]) return false;
67 | return missing_permissions;
68 | }
69 |
70 | export function showPermissionsDialog({ window }) {
71 | window.activate_action("permissions", null);
72 | }
73 |
--------------------------------------------------------------------------------
/test/isDiagnosticInRange.test.js:
--------------------------------------------------------------------------------
1 | import "../src/init.js";
2 |
3 | import tst, { assert } from "../troll/tst/tst.js";
4 |
5 | import { isDiagnosticInRange } from "../src/WorkbenchHoverProvider.js";
6 |
7 | const test = tst("isDiagnosticInRange");
8 |
9 | test("in range", () => {
10 | assert.equal(
11 | isDiagnosticInRange(
12 | {
13 | range: {
14 | start: {
15 | line: 5,
16 | character: 10,
17 | },
18 | end: {
19 | line: 5,
20 | character: 14,
21 | },
22 | },
23 | },
24 | {
25 | line: 5,
26 | character: 12,
27 | },
28 | ),
29 | true,
30 | );
31 | });
32 |
33 | test("same line", () => {
34 | assert.equal(
35 | isDiagnosticInRange(
36 | {
37 | range: {
38 | start: {
39 | line: 5,
40 | character: 10,
41 | },
42 | end: {
43 | line: 5,
44 | character: 14,
45 | },
46 | },
47 | },
48 | {
49 | line: 5,
50 | character: 15,
51 | },
52 | ),
53 | true,
54 | );
55 | });
56 |
57 | test("between lines", () => {
58 | assert.equal(
59 | isDiagnosticInRange(
60 | {
61 | range: {
62 | start: {
63 | line: 2,
64 | character: 10,
65 | },
66 | end: {
67 | line: 4,
68 | character: 14,
69 | },
70 | },
71 | },
72 | {
73 | line: 3,
74 | character: 9,
75 | },
76 | ),
77 | true,
78 | );
79 | });
80 |
81 | test("not in range", () => {
82 | assert.equal(
83 | isDiagnosticInRange(
84 | {
85 | range: {
86 | start: {
87 | line: 7,
88 | character: 33,
89 | },
90 | end: {
91 | line: 7,
92 | character: 42,
93 | },
94 | },
95 | },
96 | {
97 | line: 5,
98 | character: 12,
99 | },
100 | ),
101 | false,
102 | );
103 | });
104 |
105 | export default test;
106 |
--------------------------------------------------------------------------------
/src/Document.js:
--------------------------------------------------------------------------------
1 | import Source from "gi://GtkSource";
2 | import Gio from "gi://Gio";
3 | import GLib from "gi://GLib";
4 |
5 | export default class Document {
6 | handler_id = null;
7 |
8 | constructor({ session, code_view, lang }) {
9 | this.code_view = code_view;
10 | this.buffer = code_view.buffer;
11 | this.session = session;
12 | this.source_view = code_view.source_view;
13 |
14 | const file = session.file.get_child(lang.default_file);
15 | this.file = file;
16 | this.source_file = new Source.File({
17 | location: file,
18 | });
19 |
20 | this.start();
21 | }
22 |
23 | save() {
24 | const { source_file, buffer, session } = this;
25 | saveSourceBuffer({ source_file, buffer })
26 | .catch(console.error)
27 | .finally(() => {
28 | try {
29 | session.settings.set_boolean("edited", true);
30 | } catch (err) {
31 | console.error(err);
32 | }
33 | });
34 | }
35 |
36 | start() {
37 | this.stop();
38 | this.handler_id = this.buffer.connect("modified-changed", () => {
39 | if (!this.buffer.get_modified()) return;
40 | this.save();
41 | });
42 | }
43 |
44 | stop() {
45 | if (this.handler_id !== null) {
46 | this.buffer.disconnect(this.handler_id);
47 | this.handler_id = null;
48 | }
49 | }
50 |
51 | load() {
52 | const { source_file, buffer } = this;
53 | return loadSourceBuffer({ source_file, buffer });
54 | }
55 |
56 | format() {}
57 | }
58 |
59 | async function saveSourceBuffer({ source_file, buffer }) {
60 | const file_saver = new Source.FileSaver({
61 | buffer,
62 | file: source_file,
63 | });
64 | const success = await file_saver.save_async(
65 | GLib.PRIORITY_DEFAULT,
66 | null,
67 | null,
68 | );
69 |
70 | if (success) {
71 | buffer.set_modified(false);
72 | }
73 | }
74 |
75 | async function loadSourceBuffer({ source_file, buffer }) {
76 | const file_loader = new Source.FileLoader({
77 | buffer,
78 | file: source_file,
79 | });
80 | try {
81 | await file_loader.load_async(GLib.PRIORITY_DEFAULT, null, null);
82 | } catch (err) {
83 | if (!err.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
84 | throw err;
85 | }
86 | }
87 |
88 | buffer.set_modified(false);
89 | }
90 |
--------------------------------------------------------------------------------
/src/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 | bin_conf.set('command', 'SHELL=/bin/sh script --flush --quiet --return $XDG_RUNTIME_DIR/$FLATPAK_ID/typescript --command "' + app_id + ' $@"')
11 |
12 | meson.add_install_script('../build-aux/library.js', pkgdatadir)
13 |
14 | subdir('langs/javascript')
15 | subdir('langs/rust/template')
16 | subdir('langs/typescript')
17 |
18 | configure_file(
19 | input: 'bin.js',
20 | output: app_id,
21 | configuration: bin_conf,
22 | install: true,
23 | install_dir: get_option('bindir')
24 | )
25 |
26 | configure_file(
27 | input: 'workbench',
28 | output: 'workbench',
29 | configuration: bin_conf,
30 | install: true,
31 | install_dir: get_option('bindir')
32 | )
33 |
34 | clibin_conf = configuration_data()
35 | clibin_conf.merge_from(bin_conf)
36 | clibin_conf.set('command', app_id + '.cli "$@"')
37 | configure_file(
38 | input: 'workbench',
39 | output: 'workbench-cli',
40 | configuration: clibin_conf,
41 | install: true,
42 | install_dir: get_option('bindir')
43 | )
44 |
45 | install_data('langs/vala/workbench.vala', install_dir: pkgdatadir)
46 | install_data('langs/javascript/biome.json', install_dir: pkgdatadir)
47 | install_data('project-readme.md', install_dir: pkgdatadir)
48 | subdir('libworkbench')
49 | subdir('Previewer')
50 | subdir('langs/python')
51 | subdir('langs/css')
52 |
53 | gjspack = find_program('../troll/gjspack/bin/gjspack')
54 | custom_target('workbench',
55 | input: ['main.js'],
56 | output: app_id + '.src.gresource',
57 | command: [
58 | gjspack,
59 | '--appid=' + app_id,
60 | '--prefix', '/re/sonny/Workbench',
61 | '--project-root', meson.project_source_root(),
62 | '--resource-root', meson.project_source_root() / 'src',
63 | '--no-executable',
64 | '@INPUT0@',
65 | '@OUTDIR@',
66 | ],
67 | install: true,
68 | install_dir: pkgdatadir,
69 | build_always_stale: true,
70 | )
71 |
72 | subdir('cli')
73 |
--------------------------------------------------------------------------------
/src/libworkbench/workbench-preview-window.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "workbench-preview-window.h"
5 |
6 | struct _WorkbenchPreviewWindow
7 | {
8 | AdwWindow parent_instance;
9 |
10 | AdwToolbarView *toolbar_view;
11 | };
12 |
13 | G_DEFINE_FINAL_TYPE (WorkbenchPreviewWindow, workbench_preview_window, ADW_TYPE_WINDOW)
14 |
15 | static void
16 | workbench_preview_window_init (WorkbenchPreviewWindow *self)
17 | {
18 | gtk_widget_init_template (GTK_WIDGET (self));
19 | }
20 |
21 | static void
22 | workbench_preview_window_dispose (GObject *object)
23 | {
24 | gtk_widget_dispose_template (GTK_WIDGET (object), WORKBENCH_TYPE_PREVIEW_WINDOW);
25 |
26 | G_OBJECT_CLASS (workbench_preview_window_parent_class)->dispose (object);
27 | }
28 |
29 | static void
30 | workbench_preview_window_class_init (WorkbenchPreviewWindowClass *klass)
31 | {
32 | GObjectClass *object_class = G_OBJECT_CLASS (klass);
33 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
34 |
35 | object_class->dispose = workbench_preview_window_dispose;
36 |
37 | gtk_widget_class_set_template_from_resource (widget_class, "/re/sonny/Workbench/libworkbench/workbench-preview-window.ui");
38 | gtk_widget_class_bind_template_child (widget_class, WorkbenchPreviewWindow, toolbar_view);
39 | }
40 |
41 | /**
42 | * workbench_preview_window_new: (constructor)
43 | *
44 | * Returns: (transfer full): Returns a PreviewWindow
45 | */
46 | WorkbenchPreviewWindow *
47 | workbench_preview_window_new (void)
48 | {
49 | return g_object_new (WORKBENCH_TYPE_PREVIEW_WINDOW, NULL);
50 | }
51 |
52 | /**
53 | * workbench_preview_window_get_content:
54 | *
55 | * Returns: (transfer none)
56 | */
57 | GtkWidget *
58 | workbench_preview_window_get_content (WorkbenchPreviewWindow *self)
59 | {
60 | g_return_val_if_fail (WORKBENCH_IS_PREVIEW_WINDOW (self), NULL);
61 |
62 | return adw_toolbar_view_get_content (self->toolbar_view);
63 | }
64 |
65 | /**
66 | * workbench_preview_window_set_content:
67 | * @content: (transfer full)
68 | */
69 | void
70 | workbench_preview_window_set_content (WorkbenchPreviewWindow *self,
71 | GtkWidget *content)
72 | {
73 | g_return_if_fail (WORKBENCH_IS_PREVIEW_WINDOW (self));
74 |
75 | adw_toolbar_view_set_content (self->toolbar_view, content);
76 | }
77 |
--------------------------------------------------------------------------------
/src/Extensions/Extensions.blp:
--------------------------------------------------------------------------------
1 | using Gtk 4.0;
2 | using Adw 1;
3 |
4 | Adw.Dialog dialog {
5 | content-height: 750;
6 | content-width: 600;
7 | title: _("Extensions");
8 |
9 | Adw.ToolbarView {
10 | [top]
11 | Adw.HeaderBar {}
12 |
13 | content: ScrolledWindow {
14 | hscrollbar-policy: never;
15 |
16 | Adw.Clamp {
17 | maximum-size: 600;
18 | tightening-threshold: 400;
19 | margin-start: 14;
20 | margin-end: 14;
21 | margin-top: 30;
22 | margin-bottom: 30;
23 |
24 | Box {
25 | orientation: vertical;
26 | halign: fill;
27 | // Gtk.Picture needs to be wrapped in a box to behave properly
28 | Box {
29 | halign: center;
30 |
31 | Picture picture_illustration {
32 | can-shrink: false;
33 | margin-bottom: 30;
34 | }
35 | }
36 |
37 | ListBox {
38 | selection-mode: none;
39 |
40 | styles [
41 | "boxed-list",
42 | ]
43 |
44 | $Extension {
45 | title: _("JavaScript");
46 | available: true;
47 | }
48 |
49 | $Extension {
50 | title: _("Python");
51 | available: true;
52 | }
53 |
54 | $Extension extension_rust {
55 | title: _("Rust");
56 | }
57 |
58 | $Extension extension_vala {
59 | title: _("Vala");
60 | }
61 |
62 | $Extension extension_typescript {
63 | title: _("TypeScript");
64 | }
65 | }
66 |
67 | Label restart_hint {
68 | label: "To apply changes, restart Workbench once\nthe commands have completed";
69 | visible: false;
70 | margin-top: 30;
71 | justify: center;
72 | wrap: true;
73 |
74 | styles [
75 | "dim-label",
76 | ]
77 | }
78 |
79 | Label all_set_hint {
80 | label: "You’re all set!";
81 | margin-top: 30;
82 | justify: center;
83 | wrap: true;
84 |
85 | styles [
86 | "dim-label",
87 | ]
88 | }
89 | }
90 | }
91 | };
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/PanelCode.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 | import GObject from "gi://GObject";
3 |
4 | import { makeDropdownFlat, settings as global_settings } from "./util.js";
5 | import { setupRustProject } from "./langs/rust/rust.js";
6 | import { setupTypeScriptProject } from "./langs/typescript/typescript.js";
7 | import { setupJavascriptProject } from "./langs/javascript/javascript.js";
8 |
9 | export default function PanelCode({
10 | builder,
11 | previewer,
12 | session: { settings, file },
13 | langs,
14 | }) {
15 | const panel_code = builder.get_object("panel_code");
16 | const button_code = builder.get_object("button_code");
17 | const stack_code = builder.get_object("stack_code");
18 |
19 | const dropdown_code_lang = builder.get_object("dropdown_code_lang");
20 | makeDropdownFlat(dropdown_code_lang);
21 |
22 | settings.bind(
23 | "show-code",
24 | button_code,
25 | "active",
26 | Gio.SettingsBindFlags.DEFAULT,
27 | );
28 | button_code.bind_property(
29 | "active",
30 | panel_code,
31 | "visible",
32 | GObject.BindingFlags.SYNC_CREATE,
33 | );
34 |
35 | settings.bind(
36 | "code-language",
37 | dropdown_code_lang,
38 | "selected",
39 | Gio.SettingsBindFlags.DEFAULT,
40 | );
41 | dropdown_code_lang.connect("notify::selected-item", switchLanguage);
42 |
43 | settings.connect("changed::code-language", () => {
44 | global_settings.set_int(
45 | "recent-code-language",
46 | settings.get_int("code-language"),
47 | );
48 | });
49 |
50 | const panel = {
51 | panel: panel_code,
52 | };
53 |
54 | function switchLanguage() {
55 | panel.language = dropdown_code_lang.selected_item?.string;
56 | stack_code.visible_child_name = panel.language;
57 | previewer.useInternal().catch(console.error);
58 |
59 | if (panel.language.toLowerCase() === "javascript") {
60 | setupJavascriptProject(file, langs.javascript.document).catch(
61 | console.error,
62 | );
63 | }
64 |
65 | if (panel.language.toLowerCase() === "rust") {
66 | setupRustProject(file).catch(console.error);
67 | }
68 |
69 | if (panel.language.toLowerCase() === "typescript") {
70 | setupTypeScriptProject(file, langs.typescript.document).catch(
71 | console.error,
72 | );
73 | }
74 | }
75 | switchLanguage();
76 |
77 | return panel;
78 | }
79 |
--------------------------------------------------------------------------------
/src/Library/EntryRow.js:
--------------------------------------------------------------------------------
1 | import Adw from "gi://Adw";
2 | import Gtk from "gi://Gtk";
3 | import Gio from "gi://Gio";
4 | import GObject from "gi://GObject";
5 |
6 | import { getLanguage } from "../util.js";
7 | import Template from "./EntryRow.blp" with { type: "uri" };
8 | import { isTypeScriptEnabled } from "../Extensions/Extensions.js";
9 |
10 | class EntryRow extends Adw.ActionRow {
11 | constructor({ demo, ...params } = {}) {
12 | super(params);
13 |
14 | this._title_label.label = demo.name;
15 | this._description_label.label = demo.description;
16 |
17 | this.#createLanguageTags(demo);
18 |
19 | const action_group = new Gio.SimpleActionGroup();
20 | const activate_action = new Gio.SimpleAction({
21 | name: "activate",
22 | parameter_type: null,
23 | });
24 |
25 | activate_action.connect("activate", () => {
26 | this.emit("triggered", null);
27 | });
28 | action_group.add_action(activate_action);
29 |
30 | this.insert_action_group("demo-row", action_group);
31 | this.action_name = "demo-row.activate";
32 | }
33 |
34 | #createLanguageTags(demo) {
35 | demo.languages.forEach((id) => {
36 | if (id === "typescript" && !isTypeScriptEnabled()) return;
37 | const language = getLanguage(id);
38 | if (!language) return;
39 | const language_tag = this.#createLanguageTag(language);
40 | this._languages_box.append(language_tag);
41 | });
42 | }
43 |
44 | #createLanguageTag(language) {
45 | const button = new Gtk.Button({
46 | label: language.name,
47 | valign: Gtk.Align.CENTER,
48 | css_classes: ["pill", "small"],
49 | });
50 |
51 | button.connect("clicked", () => {
52 | this.emit("triggered", language);
53 | });
54 |
55 | return button;
56 | }
57 | }
58 |
59 | export default GObject.registerClass(
60 | {
61 | GTypeName: "EntryRow",
62 | Template,
63 | Properties: {
64 | demo: GObject.ParamSpec.jsobject(
65 | "demo",
66 | "",
67 | "",
68 | GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
69 | null,
70 | ),
71 | },
72 | Signals: {
73 | triggered: {
74 | param_types: [GObject.TYPE_JSOBJECT],
75 | },
76 | },
77 | InternalChildren: ["title_label", "description_label", "languages_box"],
78 | },
79 | EntryRow,
80 | );
81 |
--------------------------------------------------------------------------------
/data/app.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | 0
11 |
12 |
13 | 0
14 |
15 |
16 | []
17 |
18 |
19 | true
20 |
21 |
22 | false
23 |
24 |
25 |
26 |
27 | false
28 |
29 |
30 | false
31 |
32 |
33 | ''
34 |
35 |
36 | true
37 |
38 |
39 | true
40 |
41 |
42 | true
43 |
44 |
45 | 0
46 |
47 |
48 | 'blueprint'
49 |
50 |
51 | 0
52 |
53 |
54 | true
55 |
56 |
57 | false
58 |
59 |
60 | false
61 |
62 |
63 | 0
64 |
65 |
66 | 0
67 |
68 |
69 | false
70 |
71 |
72 | false
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/cli/lint.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 | import Gtk from "gi://Gtk";
3 | import Gio from "gi://Gio";
4 |
5 | import { formatting } from "./format.js";
6 | import { diagnostic_severities } from "../lsp/LSP.js";
7 | import { waitForDiagnostics } from "./util.js";
8 |
9 | export default async function lint({ filenames, lang, lspc, ci }) {
10 | let success = true;
11 |
12 | for await (const filename of filenames) {
13 | const file = Gio.File.new_for_path(filename);
14 | const [contents] = await file.load_contents_async(null);
15 | const text = new TextDecoder().decode(contents);
16 | const buffer = new Gtk.TextBuffer({ text });
17 |
18 | const uri = file.get_uri();
19 | const languageId = lang.id;
20 | let version = 0;
21 |
22 | await lspc._notify("textDocument/didOpen", {
23 | textDocument: {
24 | uri,
25 | languageId,
26 | version: version++,
27 | text: buffer.text,
28 | },
29 | });
30 |
31 | const diagnostics = await waitForDiagnostics({ uri, lspc });
32 |
33 | if (diagnostics.length > 0) {
34 | printerr(serializeDiagnostics({ file, diagnostics }));
35 | success = false;
36 | }
37 |
38 | if (ci) {
39 | const buffer_tmp = new Gtk.TextBuffer({ text: buffer.text });
40 | await formatting({ buffer: buffer_tmp, uri, lang, lspc });
41 | if (buffer_tmp.text === buffer.text) continue;
42 |
43 | printerr(`\n${file.get_path()}\nFormatting differs\n`);
44 | success = false;
45 | }
46 | }
47 |
48 | return success;
49 | }
50 |
51 | function serializeDiagnostics({ file, diagnostics }) {
52 | return (
53 | `\n${file.get_path()}\n` +
54 | diagnostics
55 | .map(({ severity, range, message }) => {
56 | return (
57 | diagnostic_severities[severity] +
58 | " " +
59 | range.start.line +
60 | ":" +
61 | range.start.character +
62 | " " +
63 | message.split("\n")[0]
64 | );
65 | })
66 | .join("\n") +
67 | "\n"
68 | );
69 | }
70 |
71 | // Vala Language Server does not support this
72 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_pullDiagnostics
73 | // const res = await lspc._request("textDocument/diagnostic", {
74 | // textDocument: {
75 | // uri: file.get_uri(),
76 | // },
77 | // });
78 |
--------------------------------------------------------------------------------
/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | root: true
2 | env:
3 | es2023: true
4 | parser: "@babel/eslint-parser"
5 | parserOptions:
6 | sourceType: module
7 | requireConfigFile: false
8 | babelOptions:
9 | plugins:
10 | - "@babel/plugin-syntax-import-attributes"
11 | extends:
12 | - eslint:recommended
13 | - plugin:import/errors
14 | - plugin:import/warnings
15 | - plugin:prettier/recommended
16 | globals:
17 | __DEV__: readonly
18 | pkg: readonly
19 | # gjs
20 | ARGV: readonly
21 | Debugger: readonly
22 | GIRepositoryGType: readonly
23 | globalThis: readonly
24 | imports: readonly
25 | Intl: readonly
26 | log: readonly
27 | logError: readonly
28 | print: readonly
29 | printerr: readonly
30 | window: readonly
31 | TextEncoder: readonly
32 | TextDecoder: readonly
33 | console: readonly
34 | setTimeout: readonly
35 | setInterval: readonly
36 | clearTimeout: readonly
37 | clearInterval: readonly
38 | rules:
39 | # Possible Problems
40 | # https://eslint.org/docs/latest/rules/#possible-problems
41 | array-callback-return: [error] # https://eslint.org/docs/latest/rules/array-callback-return
42 | no-duplicate-imports: [error] # https://eslint.org/docs/latest/rules/no-duplicate-imports
43 | no-new-native-nonconstructor: [error] # https://eslint.org/docs/latest/rules/no-new-native-nonconstructor
44 | no-restricted-globals: # https://eslint.org/docs/rules/no-restricted-globals
45 | - error
46 | - window
47 | - printerr
48 | - print
49 | - imports
50 | - logError
51 | - log
52 | no-unused-vars: # https://eslint.org/docs/latest/rules/no-unused-vars
53 | - error
54 | - vars: all
55 | args: all
56 | argsIgnorePattern: "^_"
57 |
58 | # Suggestions
59 | # https://eslint.org/docs/latest/rules/#suggestions
60 | eqeqeq: [error, always] # https://eslint.org/docs/rules/eqeqeq
61 | no-implicit-globals: [error] # https://eslint.org/docs/latest/rules/no-implicit-globals
62 | no-var: [error] # https://eslint.org/docs/rules/no-var
63 | prefer-arrow-callback: [
64 | error,
65 | { allowNamedFunctions: true, allowUnboundThis: true },
66 | ] # https://eslint.org/docs/rules/prefer-arrow-callback
67 | prefer-const: [error] # https://eslint.org/docs/rules/prefer-const
68 |
69 | # eslint-plugin-import
70 | # https://github.com/benmosher/eslint-plugin-import/
71 | import/extensions: ["error", "ignorePackages"]
72 | import/no-unresolved:
73 | [2, { ignore: ["gi://*", "resource://*", "cairo", "gettext", "system"] }]
74 |
--------------------------------------------------------------------------------
/src/WorkbenchHoverProvider.js:
--------------------------------------------------------------------------------
1 | import GObject from "gi://GObject";
2 | import Gtk from "gi://Gtk";
3 | import Source from "gi://GtkSource";
4 |
5 | import { rangeEquals } from "./lsp/LSP.js";
6 | import { registerClass } from "./overrides.js";
7 |
8 | class WorkbenchHoverProvider extends GObject.Object {
9 | constructor() {
10 | super();
11 | this.diagnostics = [];
12 | }
13 |
14 | findDiagnostics(context) {
15 | const [, iter] = context.get_iter();
16 |
17 | const line = iter.get_line();
18 | // Looks like line_offset starts at 0
19 | // Blueprint starts at 1
20 | const character = iter.get_line_offset() + 1;
21 |
22 | return findDiagnostics(this.diagnostics, { line, character });
23 | }
24 |
25 | showDiagnostics(display, diagnostics) {
26 | const container = new Gtk.Box({
27 | orientation: Gtk.Orientation.VERTICAL,
28 | spacing: 4,
29 | css_classes: ["hoverdisplay", "osd", "frame"],
30 | });
31 |
32 | for (const { message } of diagnostics) {
33 | const label = new Gtk.Label({
34 | halign: Gtk.Align.START,
35 | label: `${message}`,
36 | css_classes: ["body"],
37 | });
38 | container.append(label);
39 | }
40 |
41 | display.append(container);
42 | }
43 |
44 | vfunc_populate(context, display) {
45 | try {
46 | const diagnostics = this.findDiagnostics(context);
47 | if (diagnostics.length < 1) return [false, null];
48 | this.showDiagnostics(display, diagnostics);
49 | } catch (err) {
50 | console.error(err);
51 | return [false, null];
52 | }
53 |
54 | return [true, null];
55 | }
56 | }
57 |
58 | function findDiagnostics(diagnostics, position) {
59 | return diagnostics.filter((diagnostic) => {
60 | return isDiagnosticInRange(diagnostic, position);
61 | });
62 | }
63 |
64 | export function isDiagnosticInRange(diagnostic, { line, character }) {
65 | const { start, end } = diagnostic.range;
66 |
67 | // The tag is applied on the whole line
68 | // when diagnostic start and end ranges are equals
69 | if (rangeEquals(start, end) && line === start.line) return true;
70 |
71 | if (line < start.line) return false;
72 | if (line > end.line) return false;
73 |
74 | return (
75 | (line >= start.line && character >= start.character - 1) ||
76 | (line <= end.line && character <= end.character + 1)
77 | );
78 | }
79 |
80 | export default registerClass(
81 | {
82 | GTypeName: "WorkbenchHoverProvider",
83 | Implements: [Source.HoverProvider],
84 | },
85 | WorkbenchHoverProvider,
86 | );
87 |
--------------------------------------------------------------------------------
/Workbench.doap:
--------------------------------------------------------------------------------
1 |
2 |
8 | Workbench
9 | Learn and prototype with GNOME technologies
10 | Workbench goal is to let you experiment with GNOME technologies, no matter if tinkering for the first time or building and testing a custom GTK widget.
11 |
12 |
13 |
14 | JavaScript
15 | Vala
16 | C
17 | Rust
18 | Python
19 |
20 | GTK 4
21 | Libadwaita
22 |
23 |
24 |
25 | Sonny Piers
26 |
27 | sonnyp
28 |
29 |
30 |
31 | sonnyp
32 |
33 |
34 |
35 |
36 |
37 | sonny
38 |
39 |
40 |
41 |
42 |
43 |
44 | Julian Hofer
45 |
46 | julianhofer
47 |
48 |
49 |
50 | Hofer-Julian
51 |
52 |
53 |
54 |
55 |
56 | Hofer-Julian
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/lsp/sourceview.js:
--------------------------------------------------------------------------------
1 | import { rangeEquals } from "./LSP.js";
2 |
3 | // Inspired by
4 | // https://gitlab.gnome.org/GNOME/gnome-builder/-/blob/cbcf02bf9ac957a004fa32a17a7586f32e899a48/src/libide/code/ide-buffer-manager.c#L899
5 | export function applyTextEdits(text_edits, buffer) {
6 | if (!text_edits) return;
7 |
8 | buffer.begin_user_action();
9 |
10 | // Stage TextMarks
11 | for (const text_edit of text_edits) {
12 | prepareTextEdit(text_edit, buffer);
13 | }
14 |
15 | // Perform the edits
16 | for (const text_edit of text_edits) {
17 | applyTextEdit(text_edit, buffer);
18 | }
19 |
20 | buffer.end_user_action();
21 | }
22 |
23 | function prepareTextEdit(text_edit, buffer) {
24 | const {
25 | range: { start, end },
26 | } = text_edit;
27 | const [, start_iter] = buffer.get_iter_at_line_offset(
28 | start.line,
29 | start.character,
30 | );
31 | const [, end_iter] = buffer.get_iter_at_line_offset(end.line, end.character);
32 |
33 | const begin_mark = buffer.create_mark(
34 | null, // name
35 | start_iter, // where
36 | true, // left gravity
37 | );
38 | const end_mark = buffer.create_mark(
39 | null, // name
40 | end_iter, // where
41 | false, // left gravity
42 | );
43 |
44 | text_edit.begin_mark = begin_mark;
45 | text_edit.end_mark = end_mark;
46 | }
47 |
48 | function applyTextEdit(text_edit, buffer) {
49 | const { newText, begin_mark, end_mark } = text_edit;
50 |
51 | let start_iter = buffer.get_iter_at_mark(begin_mark);
52 | const end_iter = buffer.get_iter_at_mark(end_mark);
53 |
54 | buffer.delete(start_iter, end_iter);
55 |
56 | start_iter = buffer.get_iter_at_mark(begin_mark);
57 | buffer.insert(start_iter, newText, -1);
58 |
59 | buffer.delete_mark(begin_mark);
60 | buffer.delete_mark(end_mark);
61 | }
62 |
63 | export function getItersAtRange(buffer, { start, end }) {
64 | let start_iter;
65 | let end_iter;
66 |
67 | // Apply the tag on the whole line
68 | // if diagnostic start and end are equals such as
69 | // Blueprint-Error 13:12 to 13:12 Could not determine what kind of syntax is meant here
70 | if (rangeEquals(start, end)) {
71 | [, start_iter] = buffer.get_iter_at_line(start.line);
72 | [, end_iter] = buffer.get_iter_at_line(end.line);
73 | end_iter.forward_to_line_end();
74 | start_iter.forward_find_char((char) => char !== "", end_iter);
75 | } else {
76 | [, start_iter] = buffer.get_iter_at_line_offset(
77 | start.line,
78 | start.character,
79 | );
80 | [, end_iter] = buffer.get_iter_at_line_offset(end.line, end.character);
81 | }
82 |
83 | return [start_iter, end_iter];
84 | }
85 |
--------------------------------------------------------------------------------
/src/libworkbench/workbench-completion-request.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 | G_BEGIN_DECLS
15 |
16 | /**
17 | * WorkbenchRequestState:
18 | * @WORKBENCH_REQUEST_STATE_UNKNOWN: the request state is unknown
19 | * @WORKBENCH_REQUEST_STATE_CANCELLED: the request was cancelled
20 | * @WORKBENCH_REQUEST_STATE_COMPLETE: the request is complete
21 | *
22 | * Enumeration of request states.
23 | */
24 | typedef enum
25 | {
26 | WORKBENCH_REQUEST_STATE_UNKNOWN,
27 | WORKBENCH_REQUEST_STATE_CANCELLED,
28 | WORKBENCH_REQUEST_STATE_COMPLETE,
29 | } WorkbenchRequestState;
30 |
31 |
32 | #define WORKBENCH_TYPE_COMPLETION_REQUEST (workbench_completion_request_get_type())
33 |
34 | G_DECLARE_FINAL_TYPE (WorkbenchCompletionRequest, workbench_completion_request, WORKBENCH, COMPLETION_REQUEST, GObject)
35 |
36 | GCancellable * workbench_completion_request_get_cancellable (WorkbenchCompletionRequest *request);
37 | GtkSourceCompletionContext * workbench_completion_request_get_context (WorkbenchCompletionRequest *request);
38 | GtkSourceCompletionProvider * workbench_completion_request_get_provider (WorkbenchCompletionRequest *request);
39 | WorkbenchRequestState workbench_completion_request_get_state (WorkbenchCompletionRequest *request);
40 | void workbench_completion_request_add (WorkbenchCompletionRequest *request,
41 | GtkSourceCompletionProposal *proposal);
42 | void workbench_completion_request_splice (WorkbenchCompletionRequest *request,
43 | unsigned int position,
44 | unsigned int n_removals,
45 | gpointer *additions,
46 | unsigned int n_additions);
47 | void workbench_completion_request_state_changed (WorkbenchCompletionRequest *request,
48 | WorkbenchRequestState state);
49 |
50 | G_END_DECLS
51 |
--------------------------------------------------------------------------------
/src/init.js:
--------------------------------------------------------------------------------
1 | import "gi://GIRepository?version=3.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import Source from "gi://GtkSource";
4 | import Adw from "gi://Adw";
5 | import Vte from "gi://Vte";
6 | import GObject from "gi://GObject";
7 | import Gio from "gi://Gio";
8 | import Xdp from "gi://Xdp";
9 |
10 | Adw.init();
11 | GObject.type_ensure(Vte.Terminal);
12 |
13 | Gio._promisify(Adw.AlertDialog.prototype, "choose", "choose_finish");
14 | Gio._promisify(Xdp.Portal.prototype, "trash_file", "trash_file_finish");
15 | Gio._promisify(Xdp.Portal.prototype, "open_uri", "open_uri_finish");
16 | Gio._promisify(Xdp.Portal.prototype, "open_file", "open_file_finish");
17 | Gio._promisify(Xdp.Portal.prototype, "open_directory", "open_directory_finish");
18 | Gio._promisify(Xdp.Portal.prototype, "save_files", "save_files_finish");
19 |
20 | Gio._promisify(
21 | Gtk.FileDialog.prototype,
22 | "select_folder",
23 | "select_folder_finish",
24 | );
25 | Gio._promisify(Gtk.UriLauncher.prototype, "launch", "launch_finish");
26 | Gio._promisify(Gtk.FileLauncher.prototype, "launch", "launch_finish");
27 |
28 | Gio._promisify(
29 | Gio.InputStream.prototype,
30 | "read_bytes_async",
31 | "read_bytes_finish",
32 | );
33 | Gio._promisify(Gio.InputStream.prototype, "read_all_async", "read_all_finish");
34 | Gio._promisify(Gio.InputStream.prototype, "close_async", "close_finish");
35 | Gio._promisify(
36 | Gio.DataInputStream.prototype,
37 | "read_line_async",
38 | "read_line_finish",
39 | );
40 |
41 | Gio._promisify(Gio.OutputStream.prototype, "close_async", "close_finish");
42 | Gio._promisify(
43 | Gio.OutputStream.prototype,
44 | "write_all_async",
45 | "write_all_finish",
46 | );
47 |
48 | Gio._promisify(Gio.Subprocess.prototype, "wait_async", "wait_finish");
49 | Gio._promisify(
50 | Gio.Subprocess.prototype,
51 | "wait_check_async",
52 | "wait_check_finish",
53 | );
54 |
55 | Gio._promisify(
56 | Gio.File.prototype,
57 | "replace_contents_async",
58 | "replace_contents_finish",
59 | );
60 | Gio._promisify(
61 | Gio.File.prototype,
62 | "make_directory_async",
63 | "make_directory_finish",
64 | );
65 | Gio._promisify(Gio.File.prototype, "delete_async", "delete_finish");
66 | Gio._promisify(Gio.File.prototype, "move_async", "move_finish");
67 |
68 | Gio._promisify(Source.FileSaver.prototype, "save_async", "save_finish");
69 | Gio._promisify(Source.FileLoader.prototype, "load_async", "load_finish");
70 |
71 | Gio._promisify(Gio.DBusProxy, "new", "new_finish");
72 | Gio._promisify(Gio.DBusConnection.prototype, "close", "close_finish");
73 |
74 | Gio._promisify(
75 | Gio.File.prototype,
76 | "enumerate_children_async",
77 | "enumerate_children_finish",
78 | );
79 |
80 | Gio._promisify(
81 | Gio.File.prototype,
82 | "load_contents_async",
83 | "load_contents_finish",
84 | );
85 |
86 | Gio._promisify(
87 | Gio.FileEnumerator.prototype,
88 | "next_files_async",
89 | "next_files_finish",
90 | );
91 |
92 | Gio._promisify(Gio.File.prototype, "copy_async", "copy_finish");
93 |
--------------------------------------------------------------------------------
/src/cli/blueprint.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | import Gtk from "gi://Gtk";
4 |
5 | import { getLanguage } from "../common.js";
6 | import { parse } from "../langs/xml/xml.js";
7 | import { LSPError } from "../lsp/LSP.js";
8 | import { checkFile, diagnose } from "./util.js";
9 |
10 | const languageId = "blueprint";
11 |
12 | export default async function blueprint({ file, lspc }) {
13 | print(` ${file.get_path()}`);
14 |
15 | await diagnose({
16 | file,
17 | lspc,
18 | languageId,
19 | filter(diagnostic) {
20 | // No replacements yet
21 | return ![
22 | "Gtk.ShortcutsShortcut is deprecated\nhint: This widget will be removed in GTK 5",
23 | "Gtk.ShortcutLabel is deprecated\nhint: This widget will be removed in GTK 5",
24 | "Gtk.ShortcutsWindow is deprecated\nhint: This widget will be removed in GTK 5",
25 | "Gtk.ShortcutsGroup is deprecated\nhint: This widget will be removed in GTK 5",
26 | "Gtk.ShortcutsSection is deprecated\nhint: This widget will be removed in GTK 5",
27 | ].includes(diagnostic.message);
28 | },
29 | });
30 |
31 | const { xml } = await lspc._request("textDocument/x-blueprint-compile", {
32 | textDocument: {
33 | uri: file.get_uri(),
34 | },
35 | });
36 |
37 | print(` ✅ compiles`);
38 |
39 | try {
40 | await lspc._request("x-blueprint/decompile", {
41 | text: xml,
42 | });
43 | print(" ✅ decompiles");
44 | } catch (err) {
45 | if (!(err instanceof LSPError)) throw err;
46 | if (
47 | ![
48 | // https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/128
49 | "unsupported XML tag: ",
50 | // https://gitlab.gnome.org/jwestman/blueprint-compiler/-/issues/139
51 | "unsupported XML tag: ",
52 | ].includes(err.message)
53 | ) {
54 | throw err;
55 | }
56 | }
57 |
58 | await checkFile({
59 | lspc,
60 | file,
61 | lang: getLanguage(languageId),
62 | uri: file.get_uri(),
63 | });
64 |
65 | await lspc._notify("textDocument/didClose", {
66 | textDocument: {
67 | uri: file.get_uri(),
68 | },
69 | });
70 |
71 | const tree = parse(xml);
72 | const template_el = tree.getChild("template");
73 |
74 | let template;
75 | const builder = new Gtk.Builder();
76 | const blueprint_object_ids = [];
77 |
78 | if (template_el) {
79 | template = tree.toString();
80 | } else {
81 | builder.add_from_string(xml, -1);
82 | print(` ✅ instantiates`);
83 | getXMLObjectIds(tree, blueprint_object_ids);
84 | }
85 |
86 | return { template, builder, blueprint_object_ids };
87 | }
88 |
89 | function getXMLObjectIds(tree, object_ids) {
90 | for (const object of tree.getChildren("object")) {
91 | if (object.attrs.id) object_ids.push(object.attrs.id);
92 | // or
93 | for (const child of object.getChildElements()) {
94 | getXMLObjectIds(child, object_ids);
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | If you are interested in contributing to the Library/demos, please head over to https://github.com/workbenchdev/demos instead.
4 |
5 | Either way, don't hesitate to [get in touch](https://matrix.to/#/%23workbench:gnome.org).
6 |
7 | ## Getting started
8 |
9 | The following is the recommended setup:
10 |
11 | 1. Install [GNOME Builder from Flathub](https://flathub.org/apps/details/org.gnome.Builder)
12 | 2. Open Builder and select "Clone Repository..."
13 | 3. Clone `https://github.com/workbenchdev/Workbench.git` (or your fork)
14 | 4. Press the Run ▶ button
15 |
16 | Make sure that you're building the development target `re.sonny.Workbench.Devel`.
17 |
18 | If you know what you are doing you can also use VSCode with the extensions recommended in this workspace or anything else you are comfortable with. Don't forget to fetch the submodules.
19 |
20 | ## Setup
21 |
22 | We provide a couple of tools to make the development process pleasant.
23 |
24 | - Code formatter that runs automatically on git commit
25 | - Single command to run all the tests locally
26 |
27 | ```sh
28 | # Ubuntu requirements
29 | # sudo apt install flatpak flatpak-builder nodejs make gcc g++
30 |
31 | # Fedora requirements
32 | # sudo dnf install flatpak flatpak-builder nodejs make gcc gcc-c++
33 |
34 | cd Workbench
35 | make
36 | ```
37 |
38 | Before submitting a PR, we recommend running tests locally with
39 |
40 | ```sh
41 | make test
42 | ```
43 |
44 | ## Submitting a contribution
45 |
46 | - Unless you don't want too - add your name to [the list of contributors](./src/about.js)
47 | - Open a pull request
48 | - Make sure to review your own changes
49 | - Commits are squashed into a single commit on merge
50 |
51 | ## Debugging
52 |
53 | To view debug logs, use the following command in [`src/workbench`](../src/workbench).
54 |
55 | ```sh
56 | --command "G_MESSAGES_DEBUG=\"@app_id@\" @app_id@ $@"
57 | ```
58 |
59 | See also
60 |
61 | - [GJS Logging](https://gitlab.gnome.org/GNOME/gjs/-/blob/master/doc/Logging.md)
62 | - [Flatpak Debugging](https://docs.flatpak.org/en/latest/debugging.html)
63 |
64 | ## Translation
65 |
66 | Workbench doesn't currently support translations for its user interface. GNOME documentation is only available in English and we do not want to mislead non-English speakers.
67 |
68 |
77 |
78 | ## Troubleshooting
79 |
80 | ### The app won't build/run anymore - even on clean `main`
81 |
82 | Clean the build directory. On GNOME Builder, open the search palette with `Ctrl+Enter` and search/select `Clean`.
83 |
84 | If that doesn't solve it - remove the GNOME Builder cache directory
85 |
86 | ```sh
87 | rm -r ~/.var/app/org.gnome.Builder/cache/
88 | ```
89 |
--------------------------------------------------------------------------------
/src/shortcutsWindow.blp:
--------------------------------------------------------------------------------
1 | using Gtk 4.0;
2 |
3 | ShortcutsWindow window {
4 | hide-on-close: true;
5 |
6 | ShortcutsSection {
7 | ShortcutsGroup {
8 | title: _("Application");
9 |
10 | ShortcutsShortcut {
11 | accelerator: "Return";
12 | title: _("Run Code");
13 | }
14 |
15 | ShortcutsShortcut {
16 | accelerator: "Return";
17 | title: _("Format");
18 | }
19 |
20 | ShortcutsShortcut {
21 | accelerator: "N";
22 | title: _("New Project");
23 | }
24 |
25 | ShortcutsShortcut {
26 | accelerator: "O";
27 | title: _("Open Project");
28 | }
29 |
30 | ShortcutsShortcut {
31 | accelerator: "O";
32 | title: _("Open Library");
33 | }
34 |
35 | ShortcutsShortcut {
36 | accelerator: "I";
37 | title: _("Inspector");
38 | }
39 |
40 | ShortcutsShortcut {
41 | accelerator: "W";
42 | title: _("Close Window");
43 | }
44 |
45 | ShortcutsShortcut {
46 | accelerator: "M";
47 | title: _("Reveal in Files");
48 | }
49 |
50 | ShortcutsShortcut {
51 | accelerator: "question";
52 | title: _("Keyboard Shortcuts");
53 | }
54 |
55 | ShortcutsShortcut {
56 | accelerator: "Q";
57 | title: _("Quit");
58 | }
59 | }
60 |
61 | ShortcutsGroup {
62 | title: _("Editor");
63 |
64 | ShortcutsShortcut {
65 | accelerator: "X";
66 | title: _("Cut");
67 | }
68 |
69 | ShortcutsShortcut {
70 | accelerator: "C";
71 | title: _("Copy");
72 | }
73 |
74 | ShortcutsShortcut {
75 | accelerator: "V";
76 | title: _("Paste");
77 | }
78 |
79 | ShortcutsShortcut {
80 | accelerator: "F";
81 | title: _("Find");
82 | }
83 |
84 | ShortcutsShortcut {
85 | accelerator: "Z";
86 | title: _("Undo");
87 | }
88 |
89 | ShortcutsShortcut {
90 | accelerator: "space";
91 | title: _("Show code suggestions");
92 | }
93 |
94 | ShortcutsShortcut {
95 | accelerator: "Z";
96 | title: _("Redo");
97 | }
98 | }
99 |
100 | ShortcutsGroup {
101 | title: _("Console");
102 |
103 | ShortcutsShortcut {
104 | accelerator: "K";
105 | title: _("Toggle Console");
106 | }
107 |
108 | ShortcutsShortcut {
109 | accelerator: "C";
110 | title: _("Copy");
111 | }
112 |
113 | ShortcutsShortcut {
114 | accelerator: "A";
115 | title: _("Select All");
116 | }
117 |
118 | ShortcutsShortcut {
119 | accelerator: "K";
120 | title: _("Clear");
121 | }
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/cli/util.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-globals */
2 |
3 | import Gtk from "gi://Gtk";
4 |
5 | import { diagnostic_severities } from "../lsp/LSP.js";
6 | import { formatting } from "./format.js";
7 |
8 | export class Interrupt extends Error {
9 | constructor(...args) {
10 | super(...args);
11 | Error.captureStackTrace?.(this, Interrupt);
12 | }
13 | }
14 |
15 | export async function diagnose({
16 | file,
17 | lspc,
18 | languageId,
19 | filter = (_diagnostic) => {
20 | return true;
21 | },
22 | }) {
23 | const [contents] = await file.load_contents_async(null);
24 | const text = new TextDecoder().decode(contents);
25 |
26 | const uri = file.get_uri();
27 | let version = 0;
28 |
29 | await lspc._notify("textDocument/didOpen", {
30 | textDocument: {
31 | uri,
32 | languageId,
33 | version: version++,
34 | text,
35 | },
36 | });
37 |
38 | let diagnostics = await waitForDiagnostics({
39 | uri,
40 | lspc,
41 | });
42 | diagnostics = diagnostics.filter(filter);
43 | if (diagnostics.length > 0) {
44 | printerr(serializeDiagnostics({ diagnostics }));
45 | throw new Interrupt();
46 | }
47 |
48 | print(` ✅ lints`);
49 |
50 | return text;
51 | }
52 |
53 | export function serializeDiagnostics({ diagnostics }) {
54 | return (
55 | diagnostics
56 | .map(({ severity, range, message }) => {
57 | return (
58 | " ❌ " +
59 | diagnostic_severities[severity] +
60 | " " +
61 | range.start.line +
62 | ":" +
63 | range.start.character +
64 | " " +
65 | message.split("\n")[0]
66 | );
67 | })
68 | .join("\n") + "\n"
69 | );
70 | }
71 |
72 | export async function checkFile({ lspc, file, lang, uri }) {
73 | const [contents] = await file.load_contents_async(null);
74 | const text = new TextDecoder().decode(contents);
75 | const buffer = new Gtk.TextBuffer({ text });
76 |
77 | const buffer_tmp = new Gtk.TextBuffer({ text: buffer.text });
78 | await formatting({ buffer: buffer_tmp, uri, lang, lspc });
79 |
80 | if (buffer_tmp.text === buffer.text) {
81 | print(` ✅ checks`);
82 | } else {
83 | printerr(
84 | ` ❌ formatting differs - open and run ${file
85 | .get_parent()
86 | .get_basename()} with Workbench to fix`,
87 | );
88 | throw new Interrupt();
89 | }
90 | }
91 |
92 | export function getCodeObjectIds(text) {
93 | const object_ids = [];
94 | for (const match of text.matchAll(/get_object\("(.+)"\)/g)) {
95 | object_ids.push(match[1]);
96 | }
97 | return object_ids;
98 | }
99 |
100 | export function waitForDiagnostics({ uri, lspc }) {
101 | return new Promise((resolve) => {
102 | const handler_id = lspc.connect(
103 | "notification::textDocument/publishDiagnostics",
104 | (_self, params) => {
105 | if (uri !== params.uri) return;
106 | lspc.disconnect(handler_id);
107 | resolve(params.diagnostics);
108 | },
109 | );
110 | });
111 | }
112 |
--------------------------------------------------------------------------------
/src/icons/re.sonny.Workbench-preview-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
64 |
--------------------------------------------------------------------------------
/src/Extensions/Extensions.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 |
3 | import { build } from "../../troll/src/main.js";
4 |
5 | import Interface from "./Extensions.blp" with { type: "uri" };
6 | import illustration from "./extensions.svg";
7 |
8 | import "./Extension.js";
9 | import { settings } from "../util.js";
10 | import { getFlatpakInfo } from "../flatpak.js";
11 |
12 | export const action_extensions = new Gio.SimpleAction({
13 | name: "extensions",
14 | parameter_type: null,
15 | });
16 |
17 | export function Extensions({ window }) {
18 | const {
19 | dialog,
20 | picture_illustration,
21 | extension_rust,
22 | extension_vala,
23 | extension_typescript,
24 | restart_hint,
25 | all_set_hint,
26 | } = build(Interface);
27 |
28 | picture_illustration.set_resource(illustration);
29 |
30 | extension_rust.available = isRustAvailable();
31 | extension_rust.command = `flatpak install flathub org.freedesktop.Sdk.Extension.rust-stable//${freedesktop_version} org.freedesktop.Sdk.Extension.${llvm}//${freedesktop_version}`;
32 |
33 | extension_vala.available = isValaAvailable();
34 | extension_vala.command = `flatpak install flathub org.freedesktop.Sdk.Extension.vala//${freedesktop_version}`;
35 |
36 | extension_typescript.available = isTypeScriptAvailable();
37 | extension_typescript.command = `flatpak install flathub org.freedesktop.Sdk.Extension.${node}//${freedesktop_version} org.freedesktop.Sdk.Extension.typescript//${freedesktop_version}`;
38 | extension_typescript.visible = isTypeScriptEnabled();
39 |
40 | for (const extension of [
41 | extension_rust,
42 | extension_vala,
43 | extension_typescript,
44 | ]) {
45 | if (!extension.available) {
46 | all_set_hint.set_visible(false);
47 | restart_hint.set_visible(true);
48 | }
49 | }
50 |
51 | action_extensions.connect("activate", () => {
52 | dialog.present(window);
53 | });
54 |
55 | window.add_action(action_extensions);
56 | }
57 |
58 | let rust_available = null;
59 | export function isRustAvailable() {
60 | rust_available ??=
61 | Gio.File.new_for_path("/usr/lib/sdk/rust-stable").query_exists(null) &&
62 | Gio.File.new_for_path(`/usr/lib/sdk/${llvm}`).query_exists(null);
63 | return rust_available;
64 | }
65 |
66 | let vala_available = null;
67 | export function isValaAvailable() {
68 | vala_available ??=
69 | Gio.File.new_for_path("/usr/lib/sdk/vala").query_exists(null);
70 | return vala_available;
71 | }
72 |
73 | let typescript_available = null;
74 | export function isTypeScriptAvailable() {
75 | typescript_available ??=
76 | isTypeScriptEnabled() &&
77 | Gio.File.new_for_path("/usr/lib/sdk/typescript").query_exists(null) &&
78 | Gio.File.new_for_path(`/usr/lib/sdk/${node}`).query_exists(null);
79 | return typescript_available;
80 | }
81 |
82 | // FIXME: read from manifest
83 | const llvm = "llvm21";
84 | const node = "node24";
85 | const runtime = getFlatpakInfo().get_string("Application", "runtime");
86 | const freedesktop_version = runtime.endsWith("master") ? "25.08" : "25.08";
87 |
88 | export function isTypeScriptEnabled() {
89 | return settings.get_boolean("typescript");
90 | }
91 |
--------------------------------------------------------------------------------
/data/icons/hicolor/symbolic/apps/re.sonny.Workbench-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
51 |
--------------------------------------------------------------------------------
/src/Devtools.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 |
3 | import TermConsole from "./TermConsole.js";
4 |
5 | export default function Devtools({ application, window, builder, settings }) {
6 | const button_console = builder.get_object("button_console");
7 | const terminal = builder.get_object("terminal");
8 | const paned = builder.get_object("paned");
9 | const toolbar_devtools = builder.get_object("toolbar_devtools");
10 | const devtools = builder.get_object("devtools");
11 |
12 | // For some reasons those don't work
13 | // as builder properties
14 | paned.set_shrink_start_child(false);
15 | paned.set_shrink_end_child(true);
16 | paned.set_resize_start_child(true);
17 | paned.set_resize_end_child(true);
18 | paned.get_start_child().set_size_request(-1, 200);
19 |
20 | settings.bind(
21 | "show-console",
22 | button_console,
23 | "active",
24 | Gio.SettingsBindFlags.DEFAULT,
25 | );
26 |
27 | let position;
28 |
29 | function uncollapse() {
30 | terminal.visible = true;
31 | settings.set_boolean("show-console", true);
32 | }
33 |
34 | function collapse() {
35 | const { height: toolbar_height } = toolbar_devtools.get_allocation();
36 | const { height: paned_height } = paned.get_allocation();
37 |
38 | terminal.visible = false;
39 | settings.set_boolean("show-console", false);
40 | paned.position = paned_height - toolbar_height;
41 | }
42 |
43 | function isCollapsed() {
44 | const { height: paned_height } = paned.get_allocation();
45 | const { height: toolbar_height } = toolbar_devtools.get_allocation();
46 | return paned_height <= paned.position + toolbar_height;
47 | }
48 |
49 | paned.connect_after("notify::position", () => {
50 | const { height: toolbar_height } = toolbar_devtools.get_allocation();
51 | const { height: paned_height } = paned.get_allocation();
52 |
53 | if (paned.position + toolbar_height > paned_height - 50) {
54 | collapse();
55 | } else {
56 | uncollapse();
57 | }
58 | });
59 |
60 | function setupPaned() {
61 | const { height: paned_height } = paned.get_allocation();
62 | const { height: toolbar_height } = toolbar_devtools.get_allocation();
63 |
64 | if (button_console.active) {
65 | terminal.visible = true;
66 | if (isCollapsed()) {
67 | devtools.set_size_request(-1, 200);
68 | paned.position =
69 | position >= paned_height - toolbar_height - 50
70 | ? paned_height - 200
71 | : position;
72 | }
73 | } else {
74 | position = paned.position;
75 | const { height: toolbar_height } = toolbar_devtools.get_allocation();
76 | paned.position = paned_height - toolbar_height;
77 | terminal.visible = false;
78 | devtools.set_size_request(-1, toolbar_height);
79 | }
80 | }
81 | button_console.connect_after("notify::active", setupPaned);
82 |
83 | const action_console = new Gio.SimpleAction({
84 | name: "console",
85 | parameter_type: null,
86 | });
87 | action_console.connect("activate", () => {
88 | settings.set_boolean("show-console", !settings.get_boolean("show-console"));
89 | });
90 | window.add_action(action_console);
91 | application.set_accels_for_action("win.console", ["K"]);
92 |
93 | return {
94 | term_console: TermConsole({ builder, window, application, settings }),
95 | };
96 | }
97 |
--------------------------------------------------------------------------------
/src/langs/rust/Compiler.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 | import GLib from "gi://GLib";
3 | import dbus_previewer from "../../Previewer/DBusPreviewer.js";
4 | import { decode, encode } from "../../util.js";
5 | import { installRustLibraries } from "./rust.js";
6 |
7 | export default function Compiler({ session }) {
8 | const { file } = session;
9 | const cacheDir = GLib.get_user_cache_dir();
10 | const targetPath = `${cacheDir}/rust_build_cache`;
11 | const rustcVersionFile = Gio.File.new_for_path(
12 | `${targetPath}/rustc_version.txt`,
13 | );
14 |
15 | let rustcVersion;
16 | let savedRustcVersion;
17 |
18 | async function compile() {
19 | await installRustLibraries(file);
20 |
21 | rustcVersion ||= await getRustcVersion();
22 | savedRustcVersion ||= await getSavedRustcVersion({ rustcVersionFile });
23 |
24 | if (rustcVersion !== savedRustcVersion) {
25 | await cargoClean({ file, targetPath });
26 | await saveRustcVersion({ targetPath, rustcVersion, rustcVersionFile });
27 | }
28 |
29 | const cargo_launcher = new Gio.SubprocessLauncher();
30 | cargo_launcher.set_cwd(file.get_path());
31 |
32 | const cargo = cargo_launcher.spawnv([
33 | "cargo",
34 | "build",
35 | "--locked",
36 | "--target-dir",
37 | targetPath,
38 | ]);
39 | await cargo.wait_async(null);
40 |
41 | const result = cargo.get_successful();
42 | cargo_launcher.close();
43 | return result;
44 | }
45 |
46 | async function run() {
47 | try {
48 | const proxy = await dbus_previewer.getProxy("vala"); // rust uses the Vala previewer.
49 | const sharedLibrary = `${targetPath}/debug/libdemo.so`;
50 | await proxy.RunAsync(sharedLibrary, session.file.get_uri());
51 | } catch (err) {
52 | console.error(err);
53 | return false;
54 | }
55 |
56 | return true;
57 | }
58 |
59 | return { compile, run };
60 | }
61 |
62 | async function getRustcVersion() {
63 | const cargo_launcher = Gio.SubprocessLauncher.new(
64 | Gio.SubprocessFlags.STDOUT_PIPE,
65 | );
66 | const rustcVersionProcess = cargo_launcher.spawnv(["rustc", "--version"]);
67 | const stdout = rustcVersionProcess.communicate_utf8(null, null)[1];
68 | return stdout;
69 | }
70 |
71 | async function saveRustcVersion({
72 | targetPath,
73 | rustcVersionFile,
74 | rustcVersion,
75 | }) {
76 | GLib.mkdir_with_parents(targetPath, 0o755);
77 | await rustcVersionFile.replace_contents_async(
78 | encode(rustcVersion),
79 | null,
80 | false,
81 | Gio.FileCreateFlags.NONE,
82 | null,
83 | );
84 | }
85 |
86 | async function getSavedRustcVersion({ rustcVersionFile }) {
87 | try {
88 | const [contents] = await rustcVersionFile.load_contents_async(null);
89 | return decode(contents);
90 | } catch (err) {
91 | if (!err.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
92 | throw err;
93 | }
94 | return null;
95 | }
96 | }
97 |
98 | async function cargoClean({ file, targetPath }) {
99 | const cargo_launcher = new Gio.SubprocessLauncher();
100 | cargo_launcher.set_cwd(file.get_path());
101 | const cargoCleanProcess = cargo_launcher.spawnv([
102 | "cargo",
103 | "clean",
104 | "--target-dir",
105 | targetPath,
106 | ]);
107 | await cargoCleanProcess.wait_async(null);
108 | }
109 |
--------------------------------------------------------------------------------
/src/Previewer/External.js:
--------------------------------------------------------------------------------
1 | import Adw from "gi://Adw";
2 | import dbus_previewer from "./DBusPreviewer.js";
3 |
4 | export default function External({ output, builder, onWindowChange }) {
5 | const stack = builder.get_object("stack_preview");
6 | let dbus_proxy;
7 |
8 | dbus_previewer.onWindowOpen = ([open]) => {
9 | onWindowChange(open);
10 | };
11 |
12 | dbus_previewer.onCssParserError = (error) => {
13 | builder
14 | .get_object("code_view_css")
15 | .handleDiagnostics([getCssDiagnostic(error)]);
16 | };
17 |
18 | async function start(language) {
19 | if (language === "rust") {
20 | language = "vala"; // Rust uses the Vala previewer.
21 | }
22 | try {
23 | dbus_proxy = await dbus_previewer.getProxy(language);
24 | } catch (err) {
25 | console.error(err);
26 | }
27 | }
28 |
29 | async function open() {
30 | updateColorScheme();
31 | stack.set_visible_child_name("close_window");
32 | try {
33 | await dbus_proxy.OpenWindowAsync(output.get_width(), output.get_height());
34 | } catch (err) {
35 | console.debug(err);
36 | }
37 | }
38 |
39 | async function close() {
40 | try {
41 | await dbus_proxy.CloseWindowAsync();
42 | } catch (err) {
43 | console.debug(err);
44 | return;
45 | }
46 | stack.set_visible_child_name("open_window");
47 | }
48 |
49 | function stop() {
50 | close()
51 | .then(() => {
52 | return dbus_previewer.stop();
53 | })
54 | .catch(console.error);
55 | }
56 |
57 | async function updateXML({ xml, target_id, original_id }) {
58 | try {
59 | await dbus_proxy.UpdateUiAsync(xml, target_id, original_id || "");
60 | } catch (err) {
61 | console.debug(err);
62 | }
63 | }
64 |
65 | async function openInspector() {
66 | try {
67 | await dbus_proxy.EnableInspectorAsync(true);
68 | } catch (err) {
69 | console.debug(err);
70 | }
71 | }
72 |
73 | async function closeInspector() {
74 | try {
75 | await dbus_proxy.EnableInspectorAsync(false);
76 | } catch (err) {
77 | console.debug(err);
78 | }
79 | }
80 |
81 | const style_manager = Adw.StyleManager.get_default();
82 | function updateColorScheme() {
83 | try {
84 | dbus_proxy.ColorScheme = style_manager.color_scheme;
85 | } catch (err) {
86 | console.debug(err);
87 | }
88 | }
89 | style_manager.connect("notify::color-scheme", updateColorScheme);
90 |
91 | return {
92 | start,
93 | stop,
94 | open,
95 | close,
96 | openInspector,
97 | closeInspector,
98 | updateXML,
99 | async updateCSS(css) {
100 | try {
101 | await dbus_proxy.UpdateCssAsync(css);
102 | } catch (err) {
103 | console.debug(err);
104 | }
105 | },
106 | async screenshot({ path }) {
107 | return dbus_proxy.ScreenshotAsync(path);
108 | },
109 | };
110 | }
111 |
112 | // Converts a CssParserError to an LSP diagnostic object
113 | function getCssDiagnostic([
114 | message,
115 | start_line,
116 | start_char,
117 | end_line,
118 | end_char,
119 | ]) {
120 | return {
121 | message,
122 | range: {
123 | start: {
124 | line: start_line,
125 | character: start_char,
126 | },
127 | end: {
128 | line: end_line,
129 | character: end_char,
130 | },
131 | },
132 | severity: 1,
133 | };
134 | }
135 |
--------------------------------------------------------------------------------
/src/Library/Library.blp:
--------------------------------------------------------------------------------
1 | using Gtk 4.0;
2 | using Adw 1;
3 |
4 | Adw.Window window {
5 | hide-on-close: true;
6 | modal: false;
7 | title: _("Workbench — Library");
8 | default-height: 700;
9 | default-width: 700;
10 |
11 | Adw.ToolbarView toolbar_view {
12 | [top]
13 | Adw.HeaderBar header_bar {
14 | title-widget: Adw.WindowTitle {
15 | title: _("Workbench — Library");
16 | };
17 | }
18 |
19 | content: ScrolledWindow scrolled_window {
20 | hscrollbar-policy: never;
21 |
22 | child: Adw.Clamp {
23 | maximum-size: 576;
24 | margin-end: 12;
25 | margin-start: 12;
26 |
27 | child: Box {
28 | orientation: vertical;
29 | spacing: 12;
30 |
31 | Box {
32 | orientation: vertical;
33 |
34 | Box {
35 | halign: center;
36 | vexpand: false;
37 |
38 | Picture picture_illustration {
39 | can-shrink: false;
40 | margin-bottom: 32;
41 | margin-top: 24;
42 | }
43 | }
44 |
45 | Label {
46 | label: _("Learn, Test, Remix");
47 |
48 | styles [
49 | "title-1",
50 | ]
51 | }
52 |
53 | Box {
54 | spacing: 6;
55 |
56 | SearchEntry search_entry {
57 | search-delay: 100;
58 | placeholder-text: _("Search demos");
59 | activates-default: true;
60 | hexpand: true;
61 | margin-top: 32;
62 | }
63 |
64 | DropDown dropdown_language {
65 | valign: end;
66 |
67 | model: Gtk.StringList {};
68 | }
69 |
70 | DropDown dropdown_category {
71 | valign: end;
72 |
73 | model: Gtk.StringList {};
74 | }
75 | }
76 | }
77 |
78 | ListBox listbox {
79 | selection-mode: none;
80 |
81 | styles [
82 | "boxed-list",
83 | ]
84 | }
85 |
86 | Box {
87 | halign: center;
88 | margin-bottom: 24;
89 | margin-top: 12;
90 | orientation: vertical;
91 |
92 | Box results_empty {
93 | orientation: vertical;
94 | visible: false;
95 | margin-top: 46;
96 | margin-bottom: 70;
97 | spacing: 6;
98 |
99 | Label {
100 | label: _("No results");
101 |
102 | styles [
103 | "title-3",
104 | ]
105 | }
106 |
107 | Button button_reset {
108 | label: _("Reset filters");
109 | halign: center;
110 |
111 | styles [
112 | "pill",
113 | ]
114 | }
115 | }
116 |
117 | Label {
118 | label: _("All examples are dedicated to the public domain\nand can be used freely under the terms of CC0 1.0");
119 | use-markup: true;
120 |
121 | styles [
122 | "caption",
123 | ]
124 | }
125 | }
126 | };
127 | };
128 | };
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/Previewer/DBusPreviewer.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 |
3 | import previewer_xml from "./previewer.xml" with { type: "string" };
4 | import { buildRuntimePath } from "../util.js";
5 |
6 | const PREVIEWER_TYPE_VALA = "vala";
7 | const PREVIEWER_TYPE_PYTHON = "python";
8 |
9 | const nodeInfo = Gio.DBusNodeInfo.new_for_xml(previewer_xml);
10 | const interface_info = nodeInfo.interfaces[0];
11 |
12 | const guid = Gio.dbus_generate_guid();
13 | const path = buildRuntimePath(`workbench_preview_dbus_socket_${Date.now()}`);
14 | const server = Gio.DBusServer.new_sync(
15 | `unix:path=${path}`,
16 | Gio.DBusServerFlags.AUTHENTICATION_REQUIRE_SAME_USER,
17 | guid,
18 | null,
19 | null,
20 | );
21 |
22 | server.start();
23 |
24 | let current_proxy = null;
25 | let current_sub_process = null;
26 | let current_type = null;
27 |
28 | async function startProcess(type) {
29 | let executable_name;
30 | switch (type) {
31 | case PREVIEWER_TYPE_VALA:
32 | executable_name = "workbench-previewer-module";
33 | break;
34 | case PREVIEWER_TYPE_PYTHON:
35 | executable_name = "workbench-python-previewer";
36 | break;
37 | default:
38 | throw Error(`invalid dbus previewer type: ${type}`);
39 | }
40 |
41 | current_sub_process = Gio.Subprocess.new(
42 | [executable_name, server.get_client_address()],
43 | Gio.SubprocessFlags.NONE,
44 | );
45 | current_type = type;
46 |
47 | const connection = await new Promise((resolve) => {
48 | const _handler_id = server.connect(
49 | "new-connection",
50 | (_self, connection) => {
51 | server.disconnect(_handler_id);
52 | // FIXME: Just because the connection is established does not mean the Previewer has had time yet
53 | // to expose the object. Add a better way to detect if the object exists yet.
54 | setTimeout(() => resolve(connection), 100);
55 | return true;
56 | },
57 | );
58 | });
59 |
60 | console.debug(
61 | "new-connection",
62 | connection.get_peer_credentials().to_string(),
63 | );
64 |
65 | connection.connect("closed", (_self, remote_peer_vanished, error) => {
66 | current_proxy = null;
67 | current_type = null;
68 | console.debug(
69 | "connection closed",
70 | connection.get_peer_credentials().to_string(),
71 | remote_peer_vanished,
72 | );
73 | if (error) console.error(error);
74 | });
75 |
76 | const proxy = await Gio.DBusProxy.new(
77 | connection,
78 | Gio.DBusProxyFlags.NONE,
79 | interface_info,
80 | null,
81 | "/re/sonny/workbench/previewer_module", // object path
82 | "re.sonny.Workbench.previewer_module", // interface name
83 | null,
84 | );
85 |
86 | proxy.connectSignal("CssParserError", (_proxy, _name_owner, ...args) => {
87 | dbus_previewer.onCssParserError?.(...args);
88 | });
89 |
90 | proxy.connectSignal("WindowOpen", (_proxy, _name_owner, ...args) => {
91 | dbus_previewer.onWindowOpen?.(...args);
92 | });
93 |
94 | return proxy;
95 | }
96 |
97 | const dbus_previewer = {
98 | onCssParserError: null, // set in External.js
99 | onWindowOpen: null, // set in External.js
100 | async getProxy(type) {
101 | if (current_type !== type) {
102 | await this.stop();
103 | current_proxy = startProcess(type);
104 | }
105 | return current_proxy;
106 | },
107 |
108 | async stop() {
109 | const connection = current_proxy?.["g-connection"];
110 |
111 | if (connection) {
112 | await connection.close(null);
113 | }
114 |
115 | if (current_sub_process) {
116 | // The vala process is set to exit when the connection close
117 | // but let's send a SIGTERM anyway just to be safe
118 | current_sub_process.send_signal(15);
119 | await current_sub_process.wait_async(null);
120 | }
121 |
122 | current_sub_process = null;
123 | },
124 | };
125 |
126 | export default dbus_previewer;
127 |
--------------------------------------------------------------------------------
/src/about.js:
--------------------------------------------------------------------------------
1 | import Gtk from "gi://Gtk";
2 | import { gettext as _ } from "gettext";
3 | import GLib from "gi://GLib";
4 | import Adw from "gi://Adw";
5 |
6 | import {
7 | getGIRepositoryVersion,
8 | getGjsVersion,
9 | getGLibVersion,
10 | } from "../troll/src/util.js";
11 | import { getFlatpakInfo } from "./flatpak.js";
12 |
13 | export default function About({ application }) {
14 | const flatpak_info = getFlatpakInfo();
15 |
16 | const debug_info = `
17 | ${pkg.name} ${pkg.version}
18 | ${GLib.get_os_info("ID")} ${GLib.get_os_info("VERSION_ID")}
19 |
20 | GJS ${getGjsVersion()}
21 | Adw ${getGIRepositoryVersion(Adw)}
22 | GTK ${getGIRepositoryVersion(Gtk)}
23 | GLib ${getGLibVersion()}
24 | Flatpak ${flatpak_info.get_string("Instance", "flatpak-version")}
25 | ${getValaVersion()}
26 | `.trim();
27 |
28 | const dialog = new Adw.AboutDialog({
29 | application_name: "Workbench",
30 | developer_name: "Sonny Piers",
31 | copyright: "© 2022 Sonny Piers",
32 | license_type: Gtk.License.GPL_3_0_ONLY,
33 | version: pkg.version,
34 | website: "https://workbench.sonny.re",
35 | application_icon: pkg.name,
36 | issue_url: "https://workbench.sonny.re/feedback",
37 | debug_info,
38 | developers: [
39 | "Sonny Piers https://sonny.re",
40 | "Lorenz Wildberg https://gitlab.gnome.org/lwildberg",
41 | "Andy Holmes https://gitlab.gnome.org/andyholmes",
42 | "Julian Hofer https://julianhofer.eu/",
43 | "Marco Köpcke https://github.com/theCapypara",
44 | ],
45 | designers: [
46 | "Sonny Piers https://sonny.re",
47 | "Tobias Bernard ",
48 | "Brage Fuglseth https://bragefuglseth.dev",
49 | ],
50 | artists: [
51 | "Tobias Bernard ",
52 | "Jakub Steiner https://jimmac.eu",
53 | "Brage Fuglseth https://bragefuglseth.dev",
54 | ],
55 | });
56 |
57 | dialog.add_credit_section(_("Contributors"), [
58 | "Akshay Warrier https://github.com/AkshayWarrier",
59 | "Ben Foote http://www.bengineeri.ng",
60 | "Brage Fuglseth https://bragefuglseth.dev",
61 | "Hari Rana (TheEvilSkeleton) https://theevilskeleton.gitlab.io",
62 | "Sriyansh Shivam https://linktr.ee/sonic_here",
63 | "Angelo Verlain https://www.vixalien.com",
64 | "bazylevnik0 https://github.com/bazylevnik0",
65 | "Felipe Kinoshita https://mastodon.social/@fkinoshita",
66 | "Karol Lademan https://github.com/karl0d",
67 | "Nasah Kuma https://www.mantohnasah.com/",
68 | "Jose Hunter https://github.com/halfmexican/",
69 | "Akunne Pascal https://github.com/Kodecheff",
70 | "JCWasmx86 https://github.com/JCWasmx86",
71 | "Alex (PaladinDev) https://github.com/SpikedPaladin",
72 | "Diego Iván M.E https://github.com/Diego-Ivan",
73 | "Rasmus Thomsen ",
74 | "Marvin W https://github.com/mar-v-in",
75 | "Saad Khan https://github.com/saadulkh",
76 | "Adeel Ahmed Qureshi https://github.com/itsAdee",
77 | "Muhammad Bilal https://github.com/mbilal234",
78 | "Onkar https://github.com/onkarrai06",
79 | "Sabrina Meindlhumer https://github.com/m-sabrina",
80 | "Urtsi Santsi ",
81 | "Roland Lötscher https://github.com/rolandlo",
82 | "Gregor Niehl https://fosstodon.org/@gregorni",
83 | "Jamie Gravendeel https://jamie.garden",
84 | "Bharat Tyagi https://github.com/BharatAtbrat",
85 | "Jan Fooken https://git.janvhs.com",
86 | "Vladimir Vaskov https://github.com/Rirusha",
87 | "Nokse https://github.com/Nokse22",
88 | // Add yourself as
89 | // "John Doe",
90 | // or
91 | // "John Doe ",
92 | // or
93 | // "John Doe https://john.com",
94 | ]);
95 | dialog.present(application.active_window);
96 |
97 | return { dialog };
98 | }
99 |
100 | function getValaVersion() {
101 | const [, data] = GLib.spawn_command_line_sync("valac --version");
102 | return new TextDecoder().decode(data).trim();
103 | }
104 |
--------------------------------------------------------------------------------
/src/icons/re.sonny.Workbench-ui-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
67 |
--------------------------------------------------------------------------------
/src/Permissions/Permissions.blp:
--------------------------------------------------------------------------------
1 | using Gtk 4.0;
2 | using Adw 1;
3 |
4 | Adw.Dialog dialog {
5 | content-height: 750;
6 | content-width: 600;
7 |
8 | Adw.ToolbarView {
9 | [top]
10 | Adw.HeaderBar {}
11 |
12 | content: ScrolledWindow {
13 | hscrollbar-policy: never;
14 |
15 | Adw.Clamp {
16 | maximum-size: 520;
17 | tightening-threshold: 400;
18 | margin-start: 12;
19 | margin-end: 12;
20 | margin-bottom: 24;
21 |
22 | Box {
23 | orientation: vertical;
24 | halign: fill;
25 | spacing: 24;
26 | // Gtk.Picture needs to be wrapped in a box to behave properly
27 | Box {
28 | halign: center;
29 |
30 | Picture picture_illustration {
31 | can-shrink: false;
32 | margin-bottom: 24;
33 | }
34 | }
35 |
36 | Label {
37 | label: _("Permissions Needed");
38 |
39 | styles [
40 | "title-1",
41 | ]
42 | }
43 |
44 | Label {
45 | label: _("Workbench needs additional permissions. Please run the following command in a terminal and restart Workbench.");
46 | wrap: true;
47 | justify: center;
48 | }
49 |
50 | Label label_command {
51 | use-markup: true;
52 | wrap: true;
53 | wrap-mode: word_char;
54 | selectable: true;
55 | xalign: 0;
56 |
57 | styles [
58 | "command_snippet",
59 | ]
60 | }
61 |
62 | Box {
63 | orientation: vertical;
64 |
65 | Box {
66 | margin-bottom: 6;
67 |
68 | Label {
69 | label: _("What it does");
70 | halign: start;
71 | hexpand: true;
72 |
73 | styles [
74 | "heading",
75 | ]
76 | }
77 |
78 | Button button_info {
79 | icon-name: "re.sonny.Workbench-external-link-symbolic";
80 |
81 | styles [
82 | "flat",
83 | ]
84 | }
85 | }
86 |
87 | ListBox {
88 | selection-mode: none;
89 |
90 | styles [
91 | "boxed-list",
92 | ]
93 |
94 | Adw.ActionRow {
95 | [prefix]
96 | Image {
97 | icon-name: "re.sonny.Workbench-person-symbolic";
98 | }
99 |
100 | title: _("--user");
101 | subtitle: _("Grant for your account only");
102 |
103 | styles [
104 | "property",
105 | ]
106 | }
107 |
108 | Adw.ActionRow {
109 | [prefix]
110 | Image {
111 | icon-name: "re.sonny.Workbench-network-wireless-symbolic";
112 | }
113 |
114 | title: _("--share-network");
115 | subtitle: _("Network access");
116 |
117 | styles [
118 | "property",
119 | ]
120 | }
121 |
122 | Adw.ActionRow {
123 | [prefix]
124 | Image {
125 | icon-name: "re.sonny.Workbench-speakers-symbolic";
126 | }
127 |
128 | title: _("--socket=pulseaudio");
129 | subtitle: _("Record and play audio");
130 |
131 | styles [
132 | "property",
133 | ]
134 | }
135 |
136 | Adw.ActionRow action_row_device {
137 | [prefix]
138 | Image {
139 | icon-name: "re.sonny.Workbench-gamepad-symbolic";
140 | }
141 |
142 | // title: _("--device=input");
143 | subtitle: _("Access to input device such as gamepads");
144 |
145 | styles [
146 | "property",
147 | ]
148 | }
149 | }
150 | }
151 | }
152 | }
153 | };
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/libworkbench/meson.build:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: GPL-3.0-only
2 | # SPDX-FileCopyrightText: Workbench Contributors
3 | # SPDX-FileContributor: Andy Holmes
4 |
5 | prefix = get_option('prefix')
6 | datadir = prefix / get_option('datadir')
7 | includedir = prefix / get_option('includedir')
8 | libdir = prefix / get_option('libdir')
9 |
10 | girdir = datadir / 'gir-1.0'
11 | typelibdir = libdir / 'girepository-1.0'
12 | vapidir = datadir / 'vala' / 'vapi'
13 |
14 | libworkbench_api_version = '0'
15 | libworkbench_api_name = 'libworkbench-@0@'.format(libworkbench_api_version)
16 |
17 |
18 | #
19 | # Shared Library
20 | #
21 | libworkbench_c_args = [
22 | '-Wall',
23 | '-Wextra',
24 | '-Wfloat-equal',
25 | '-Wformat=2',
26 | '-Wincompatible-pointer-types',
27 | '-Wint-conversion',
28 | '-Wint-to-pointer-cast',
29 | '-Wmissing-include-dirs',
30 | '-Woverflow',
31 | '-Wpointer-arith',
32 | '-Wpointer-to-int-cast',
33 | '-Wredundant-decls',
34 | '-Wshadow',
35 | '-Wstrict-prototypes',
36 | '-Wundef',
37 |
38 | '-Wno-discarded-array-qualifiers',
39 | '-Wno-missing-field-initializers',
40 | '-Wno-unused-parameter',
41 | '-Wno-missing-declarations',
42 |
43 | '-DWORKBENCH_COMPILATION',
44 | ]
45 | libworkbench_link_args = []
46 | libworkbench_deps = [
47 | dependency('gio-2.0', version: '>= 2.76.0'),
48 | dependency('gtk4', version: '>= 4.10.0'),
49 | dependency('gtksourceview-5', version: '>= 5.8.0'),
50 | dependency('libadwaita-1', version: '>= 1.5')
51 | ]
52 |
53 | if get_option('profile') != 'development'
54 | libworkbench_c_args += [
55 | '-DG_DISABLE_ASSERT',
56 | '-DG_DISABLE_CAST_CHECKS',
57 | ]
58 | endif
59 |
60 | libworkbench_headers = files([
61 | 'workbench.h',
62 | 'workbench-completion-provider.h',
63 | 'workbench-completion-request.h',
64 | 'workbench-preview-window.h'
65 | ])
66 |
67 | libworkbench_sources = files([
68 | 'workbench-completion-provider.c',
69 | 'workbench-completion-request.c',
70 | 'workbench-preview-window.c'
71 | ])
72 |
73 | install_headers(libworkbench_headers,
74 | subdir: includedir / 'libworkbench',
75 | )
76 |
77 | libworkbench_enums = gnome.mkenums_simple('libworkbench-enums',
78 | sources: libworkbench_headers,
79 | install_header: true,
80 | install_dir: includedir / 'libworkbench',
81 | )
82 |
83 | libworkbench = shared_library('workbench-@0@'.format(libworkbench_api_version),
84 | libworkbench_headers,
85 | libworkbench_sources,
86 | libworkbench_enums,
87 | include_directories: [include_directories('.')],
88 | dependencies: libworkbench_deps,
89 | c_args: libworkbench_c_args,
90 | link_args: libworkbench_link_args,
91 | soversion: meson.project_version(),
92 | version: libworkbench_api_version,
93 | install: true,
94 | )
95 |
96 | # GObject Introspection
97 | libworkbench_gir = gnome.generate_gir(libworkbench,
98 | identifier_prefix: meson.project_name(),
99 | namespace: meson.project_name(),
100 | nsversion: libworkbench_api_version,
101 | symbol_prefix: meson.project_name().to_lower(),
102 | sources: [libworkbench_headers, libworkbench_sources, libworkbench_enums],
103 | extra_args: ['--c-include=workbench.h'],
104 | includes: ['Gio-2.0', 'Gtk-4.0', 'GtkSource-5'],
105 | install_dir_gir: girdir,
106 | install_dir_typelib: typelibdir,
107 | install: true,
108 | )
109 |
110 | # Vala API Bindings
111 | libworkbench_vapi = gnome.generate_vapi(libworkbench_api_name,
112 | sources: libworkbench_gir[0],
113 | packages: ['gio-2.0', 'gtk4', 'gtksourceview-5'],
114 | install: true,
115 | install_dir: vapidir,
116 | )
117 |
118 | blueprints = custom_target('blueprints',
119 | input: files(
120 | 'workbench-preview-window.blp'
121 | ),
122 | output: '.',
123 | command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'],
124 | )
125 |
126 | gnome.compile_resources(
127 | 're.sonny.Workbench.libworkbench',
128 | 're.sonny.Workbench.libworkbench.gresource.xml',
129 | gresource_bundle: true,
130 | install: true,
131 | install_dir: pkgdatadir,
132 | dependencies: blueprints,
133 | )
134 |
--------------------------------------------------------------------------------
/src/actions.js:
--------------------------------------------------------------------------------
1 | import Gtk from "gi://Gtk";
2 | import Gio from "gi://Gio";
3 | import GLib from "gi://GLib";
4 | import Xdp from "gi://Xdp";
5 | import XdpGtk from "gi://XdpGtk4";
6 |
7 | import About from "./about.js";
8 | import Window from "./window.js";
9 | import { portal, settings } from "./util.js";
10 | import { createSession } from "./sessions.js";
11 |
12 | export default function Actions({ application }) {
13 | const quit = new Gio.SimpleAction({
14 | name: "quit",
15 | parameter_type: null,
16 | });
17 | quit.connect("activate", () => {
18 | application.quit();
19 | });
20 | application.add_action(quit);
21 | application.set_accels_for_action("app.quit", ["Q"]);
22 |
23 | const showAboutDialog = new Gio.SimpleAction({
24 | name: "about",
25 | parameter_type: null,
26 | });
27 | showAboutDialog.connect("activate", () => {
28 | About({ application });
29 | });
30 | application.add_action(showAboutDialog);
31 |
32 | const action_open_uri = new Gio.SimpleAction({
33 | name: "open_uri",
34 | parameter_type: new GLib.VariantType("s"),
35 | });
36 | action_open_uri.connect("activate", (_self, target) => {
37 | new Gtk.UriLauncher({
38 | uri: target.unpack(),
39 | })
40 | .launch(application.get_active_window(), null)
41 | .catch(console.error);
42 | });
43 | application.add_action(action_open_uri);
44 |
45 | const action_platform_tools = new Gio.SimpleAction({
46 | name: "platform_tools",
47 | parameter_type: new GLib.VariantType("s"),
48 | });
49 | action_platform_tools.connect("activate", (_self, target) => {
50 | const name = target.unpack();
51 |
52 | if (
53 | !["adwaita-1-demo", "gtk4-demo", "gtk4-widget-factory"].includes(name)
54 | ) {
55 | return;
56 | }
57 |
58 | try {
59 | GLib.spawn_command_line_async(`sh -c "/bin/${name} > /dev/null 2>&1"`);
60 | } catch (err) {
61 | console.error(err);
62 | }
63 | });
64 | application.add_action(action_platform_tools);
65 |
66 | application.add_action(settings.create_action("color-scheme"));
67 | // application.add_action(settings.create_action("safe-mode"));
68 | // application.add_action(settings.create_action("auto-preview"));
69 |
70 | const action_new_project = new Gio.SimpleAction({
71 | name: "new",
72 | });
73 | action_new_project.connect("activate", (_self, _target) => {
74 | newProject({ application }).catch(console.error);
75 | });
76 | application.add_action(action_new_project);
77 | application.set_accels_for_action("app.new", ["N"]);
78 |
79 | const action_open_file = new Gio.SimpleAction({
80 | name: "open",
81 | parameter_type: new GLib.VariantType("s"),
82 | });
83 | action_open_file.connect("activate", (_self, target) => {
84 | const hint = target.unpack();
85 | open({ application, hint }).catch(console.error);
86 | });
87 | application.add_action(action_open_file);
88 | application.set_accels_for_action("app.open('project')", ["O"]);
89 |
90 | const action_show_screenshot = new Gio.SimpleAction({
91 | name: "show-screenshot",
92 | parameter_type: new GLib.VariantType("s"),
93 | });
94 | action_show_screenshot.connect("activate", (_self, target) => {
95 | const uri = target.unpack();
96 | showScreenshot({ application, uri }).catch(console.error);
97 | });
98 | application.add_action(action_show_screenshot);
99 | }
100 |
101 | async function showScreenshot({ application, uri }) {
102 | const parent = XdpGtk.parent_new_gtk(application.get_active_window());
103 | await portal.open_directory(
104 | parent,
105 | uri,
106 | Xdp.OpenUriFlags.NONE,
107 | null, // cancellable
108 | );
109 | }
110 |
111 | async function newProject({ application }) {
112 | const session = createSession();
113 | const { load } = Window({ application, session });
114 | await load();
115 | }
116 |
117 | async function open({ application, hint }) {
118 | const file_dialog = new Gtk.FileDialog();
119 |
120 | let file;
121 | try {
122 | file = await file_dialog.select_folder(
123 | application.get_active_window(),
124 | null,
125 | );
126 | } catch (err) {
127 | if (!err.matches(Gtk.DialogError, Gtk.DialogError.DISMISSED)) {
128 | throw err;
129 | }
130 | return;
131 | }
132 |
133 | application.open([file], hint);
134 | }
135 |
--------------------------------------------------------------------------------
/src/log_handler.js:
--------------------------------------------------------------------------------
1 | import GLib from "gi://GLib";
2 |
3 | // Does not wok for some reason
4 | // const all_log_levels =
5 | // GLib.LogLevelFlags.LEVEL_MASK &
6 | // GLib.LogLevelFlags.FLAG_FATAL &
7 | // GLib.LogLevelFlags.FLAG_RECURSION;
8 |
9 | const all_log_levels =
10 | GLib.LogLevelFlags.FLAG_FATAL |
11 | GLib.LogLevelFlags.FLAG_RECURSION |
12 | GLib.LogLevelFlags.LEVEL_CRITICAL |
13 | GLib.LogLevelFlags.LEVEL_DEBUG |
14 | GLib.LogLevelFlags.LEVEL_ERROR |
15 | GLib.LogLevelFlags.LEVEL_INFO |
16 | // GLib.LogLevelFlags.LEVEL_MASK |
17 | GLib.LogLevelFlags.LEVEL_MESSAGE |
18 | GLib.LogLevelFlags.LEVEL_WARNING;
19 |
20 | /* Cannot use GLib.log_set_writer_func because it is not safe to use https://gitlab.gnome.org/GNOME/gjs/-/issues/481 */
21 | // const decoder = new TextDecoder();
22 | // GLib.log_set_writer_func((level, fields) => {
23 | // const domain = decoder.decode(fields.GLIB_DOMAIN);
24 | // const message = decoder.decode(fields.MESSAGE);
25 | // log_handler(domain, level, message);
26 | // return GLib.LogWriterOutput.HANDLED;
27 | // });
28 |
29 | // Not working - Gjs-Console uses structured logging
30 | // GLib.log_set_handler("Gjs-Console", all_log_levels, log_handler);
31 | GLib.log_set_handler("Gdk", all_log_levels, log_handler);
32 | GLib.log_set_handler("Adwaita", all_log_levels, log_handler);
33 | GLib.log_set_handler("GVFS", all_log_levels, log_handler);
34 | GLib.log_set_handler("Workbench", all_log_levels, log_handler);
35 | // Not working - Gtk is probably using structured logging
36 | // GLib.log_set_handler("Gtk", all_log_levels, log_handler);
37 |
38 | // https://docs.gtk.org/glib/flags.LogLevelFlags.html
39 | // https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
40 | function get_log_level_name(log_level, domain) {
41 | switch (log_level) {
42 | case GLib.LogLevelFlags.FLAG_RECURSION:
43 | return "\x1b[1;31mRecursion\x1b[0m";
44 | case GLib.LogLevelFlags.FLAG_FATAL:
45 | return "\x1b[1;31mFatal\x1b[0m";
46 | case GLib.LogLevelFlags.LEVEL_CRITICAL:
47 | // This is what console.error use
48 | return domain === "Gjs-Console"
49 | ? "\x1b[1;31mError\x1b[0m"
50 | : "\x1b[1;35mCritical\x1b[0m";
51 | case GLib.LogLevelFlags.LEVEL_ERROR:
52 | return "\x1b[1;31mError\x1b[0m";
53 | case GLib.LogLevelFlags.LEVEL_WARNING:
54 | return "\x1b[1;33mWarning\x1b[0m";
55 | // case GLib.LogLevelFlags.LEVEL_MESSAGE:
56 | // case GLib.LogLevelFlags.LEVEL_INFO:
57 | // case GLib.LogLevelFlags.LEVEL_DEBUG:
58 | default:
59 | return "";
60 | }
61 | }
62 |
63 | function log_handler(domain, level, message) {
64 | // if (level === GLib.LogLevelFlags.LEVEL_DEBUG) {
65 | // return GLib.LogWriterOutput.HANDLED;
66 | // }
67 |
68 | if (
69 | domain === "Gdk" &&
70 | level === GLib.LogLevelFlags.LEVEL_CRITICAL &&
71 | [
72 | "gdk_scroll_event_get_direction: assertion 'GDK_IS_EVENT_TYPE (event, GDK_SCROLL)' failed",
73 | "gdk_scroll_event_get_direction: assertion 'GDK_IS_EVENT (event)' failed",
74 | ].includes(message)
75 | ) {
76 | return GLib.LogWriterOutput.HANDLED;
77 | }
78 |
79 | if (
80 | domain === "Gtk" &&
81 | level === GLib.LogLevelFlags.LEVEL_CRITICAL &&
82 | message ===
83 | "Unable to connect to the accessibility bus at 'unix:path=/run/flatpak/at-spi-bus': Could not connect: No such file or directory"
84 | ) {
85 | return GLib.LogWriterOutput.HANDLED;
86 | }
87 |
88 | if (
89 | domain === "Adwaita" &&
90 | level === GLib.LogLevelFlags.LEVEL_WARNING &&
91 | message ===
92 | "Using GtkSettings:gtk-application-prefer-dark-theme with libadwaita is unsupported. Please use AdwStyleManager:color-scheme instead."
93 | ) {
94 | return GLib.LogWriterOutput.HANDLED;
95 | }
96 |
97 | if (
98 | domain === "GVFS" &&
99 | level === GLib.LogLevelFlags.LEVEL_WARNING &&
100 | message ===
101 | "The peer-to-peer connection failed: Error when getting information for file “/run/user/1000/gvfsd”: No such file or directory. Falling back to the session bus. Your application is probably missing --filesystem=xdg-run/gvfsd privileges."
102 | ) {
103 | return GLib.LogWriterOutput.HANDLED;
104 | }
105 |
106 | let str = "\n";
107 |
108 | if (!["Gjs", "Gjs-Console"].includes(domain)) {
109 | str += `${domain}-`;
110 | }
111 |
112 | const level_name = get_log_level_name(level, domain);
113 | str += level_name ? `${level_name}: ` : "";
114 | str += message;
115 | str += "\n";
116 |
117 | // console.terminal.fork_command(`echo ${str}`);
118 | // eslint-disable-next-line no-restricted-globals
119 | print(str);
120 | }
121 |
--------------------------------------------------------------------------------
/src/common.js:
--------------------------------------------------------------------------------
1 | import LSPClient from "./lsp/LSPClient.js";
2 |
3 | const formatting_options = {
4 | insertSpaces: true,
5 | trimTrailingWhitespace: true,
6 | insertFinalNewline: true,
7 | trimFinalNewlines: true,
8 | };
9 |
10 | // See dropdown_code_lang for index
11 | export const languages = [
12 | {
13 | id: "blueprint",
14 | name: "Blueprint",
15 | panel: "ui",
16 | extensions: [".blp"],
17 | types: [],
18 | document: null,
19 | default_file: "main.blp",
20 | // language_server: [
21 | // "/home/sonny/Projects/GNOME/blueprint-compiler/blueprint-compiler.py",
22 | // "lsp",
23 | // ],
24 | language_server: ["blueprint-compiler", "lsp"],
25 | formatting_options: {
26 | ...formatting_options,
27 | tabSize: 2,
28 | },
29 | },
30 | {
31 | id: "xml",
32 | name: "GTK Builder",
33 | panel: "ui",
34 | extensions: [".ui"],
35 | types: ["application/x-gtk-builder"],
36 | document: null,
37 | default_file: "main.ui",
38 | },
39 | {
40 | id: "javascript",
41 | name: "JavaScript",
42 | panel: "code",
43 | extensions: [".js", ".mjs"],
44 | types: ["text/javascript", "application/javascript"],
45 | document: null,
46 | default_file: "main.js",
47 | index: 0,
48 | // language_server: ["typescript-language-server", "--stdio"],
49 | language_server: [
50 | "biome",
51 | "lsp-proxy",
52 | // src/meson.build installs biome.json there
53 | `--config-path=${pkg.pkgdatadir}`,
54 | ],
55 | formatting_options: {
56 | ...formatting_options,
57 | tabSize: 2,
58 | },
59 | },
60 | {
61 | id: "css",
62 | name: "CSS",
63 | panel: "style",
64 | extensions: [".css"],
65 | types: ["text/css"],
66 | document: null,
67 | default_file: "main.css",
68 | language_server: ["gtkcsslanguageserver"],
69 | formatting_options: {
70 | ...formatting_options,
71 | tabSize: 2,
72 | },
73 | },
74 | {
75 | id: "vala",
76 | name: "Vala",
77 | panel: "code",
78 | extensions: [".vala"],
79 | types: ["text/x-vala"],
80 | document: null,
81 | default_file: "main.vala",
82 | index: 1,
83 | language_server: ["vala-language-server"],
84 | formatting_options: {
85 | ...formatting_options,
86 | tabSize: 4,
87 | },
88 | },
89 | {
90 | id: "rust",
91 | name: "Rust",
92 | panel: "code",
93 | extensions: [".rs"],
94 | types: ["text/x-rust"],
95 | document: null,
96 | default_file: "code.rs",
97 | index: 2,
98 | language_server: ["rust-analyzer"],
99 | formatting_options: {
100 | ...formatting_options,
101 | tabSize: 4,
102 | },
103 | },
104 | {
105 | id: "python",
106 | name: "Python",
107 | panel: "code",
108 | extensions: [".py"],
109 | types: ["text/x-python"],
110 | document: null,
111 | default_file: "main.py",
112 | index: 3,
113 | language_server: ["pylsp", "-v"],
114 | formatting_options: {
115 | ...formatting_options,
116 | tabSize: 4,
117 | },
118 | },
119 | {
120 | id: "typescript",
121 | name: "TypeScript",
122 | panel: "code",
123 | extensions: [".ts", ".mts"],
124 | types: [],
125 | document: null,
126 | default_file: "main.ts",
127 | index: 4,
128 | language_server: ["typescript-language-server", "--stdio"],
129 | formatting_options: {
130 | ...formatting_options,
131 | tabSize: 2,
132 | },
133 | },
134 | ];
135 |
136 | export function getLanguage(id) {
137 | return languages.find(
138 | (language) => language.id.toLowerCase() === id.toLowerCase(),
139 | );
140 | }
141 |
142 | export function createLSPClient({ lang, root_uri, quiet = true }) {
143 | const language_id = lang.id;
144 |
145 | const lspc = new LSPClient(lang.language_server, {
146 | rootUri: root_uri,
147 | languageId: language_id,
148 | quiet,
149 | });
150 |
151 | if (quiet === false) {
152 | lspc.connect("exit", () => {
153 | console.log(`${language_id} language server exit`);
154 | });
155 | lspc.connect("output", (_self, message) => {
156 | console.log(
157 | `${language_id} language server OUT:\n${JSON.stringify(
158 | message,
159 | null,
160 | 2,
161 | )}`,
162 | );
163 | });
164 | lspc.connect("input", (_self, message) => {
165 | console.log(
166 | `${language_id} language server IN:\n${JSON.stringify(
167 | message,
168 | null,
169 | 2,
170 | )}`,
171 | );
172 | });
173 | }
174 |
175 | return lspc;
176 | }
177 |
178 | export const PYTHON_LSP_CONFIG = {
179 | pylsp: {
180 | configurationSources: ["ruff"],
181 | plugins: {
182 | ruff: {
183 | enabled: true,
184 | formatEnabled: true,
185 | executable: `${pkg.prefix}/bin/ruff`,
186 | config: `${pkg.pkgdatadir}/ruff.toml`,
187 | },
188 | },
189 | },
190 | };
191 |
--------------------------------------------------------------------------------