Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/repl/app/components/compiler.gts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ interface Signature {

export default class Compiler extends Component<Signature> {
<template>
{{#let (Compiled this.editor.text this.format this.flavor) as |compiled|}}
{{#let (Compiled this.editor.renderInput.text this.format this.flavor) as |compiled|}}
{{yield compiled}}
{{/let}}
</template>

@service declare editor: EditorService;

get formatQP() {
return this.editor.format;
return this.editor.renderInput.format;
}

get formatQPParts() {
Expand Down
4 changes: 4 additions & 0 deletions apps/repl/app/controllers/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export default class ApplicationController extends Controller {
// or force it to not be rendered in to a shadow-dom if falsey value is passed
'shadowdom',

// When falsey ("off"), the output only compiles when the "Render" button is clicked,
// instead of on every edit.
'autorender',

// Disable shiki highlighting on page load
// this is primarily an optimization for tests
'nohighlight',
Expand Down
53 changes: 53 additions & 0 deletions apps/repl/app/services/editor.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { tracked } from '@glimmer/tracking';
import { registerDestructor } from '@ember/destroyable';
import Service, { service } from '@ember/service';

import { castToBoolean } from 'ember-primitives/qp';
import { link } from 'reactiveweb/link';

import { FileURIComponent } from 'limber/utils/editor-text';

import type { DemoEntry } from '../snippets';
import type Owner from '@ember/owner';
import type RouterService from '@ember/routing/router-service';
import type { FormatQP } from '#app/languages.gts';

interface RenderInput {
text: EditorService['text'];
format: FormatQP;
}

export default class EditorService extends Service {
@service declare router: RouterService;

Expand Down Expand Up @@ -38,6 +46,51 @@ export default class EditorService extends Service {
return this.fileURIComponent.format;
}

/**
* The file in the output pane while `?autorender=off` pauses compiling.
* Rendering is automatic while this is undefined.
*
* A getter on the router would compile again on each keystroke,
* because every edit writes the URL.
*/
@tracked pausedFile: RenderInput | undefined;

constructor(owner: Owner) {
super(owner);

this.router.on('routeDidChange', this.#syncAutoRender);
registerDestructor(this, () => this.router.off('routeDidChange', this.#syncAutoRender));
}

#syncAutoRender = () => {
const value = this.router.currentRoute?.queryParams?.['autorender'];
const autoRender = typeof value === 'string' ? castToBoolean(value) : true;

// Every edit writes the URL, so a new pausedFile here would compile each keystroke.
if (autoRender === this.autoRender) return;

this.pausedFile = autoRender ? undefined : this.#file;
};

get autoRender() {
return this.pausedFile === undefined;
}

render = () => {
this.pausedFile = this.#file;
};

get #file(): RenderInput {
return { text: this.text, format: this.format };
}

/**
* The text and format for the output pane.
*/
get renderInput(): RenderInput {
return this.pausedFile ?? this.#file;
}

get nohighlight() {
return (this.router.currentRoute?.queryParams ?? {}).nohighlight;
}
Expand Down
46 changes: 46 additions & 0 deletions apps/repl/app/templates/edit/layout/controls/auto-render.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';

import FaIcon from '@fortawesome/ember-fontawesome/components/fa-icon';
import { faForwardStep, faPause, faPlay } from '@fortawesome/free-solid-svg-icons';

import { Button } from './button.gts';

import type RouterService from '@ember/routing/router-service';
import type EditorService from 'limber/services/editor';

export class AutoRender extends Component {
@service declare editor: EditorService;
@service declare router: RouterService;

toggle = () => {
const url = new URL(this.router.currentURL ?? '/edit', location.origin);

if (this.editor.autoRender) {
url.searchParams.set('autorender', 'off');
} else {
url.searchParams.delete('autorender');
}

this.router.replaceWith(url.pathname + url.search);
};

<template>
<Button
title={{if this.editor.autoRender "Pause automatic rendering" "Resume automatic rendering"}}
data-test-auto-render
{{on "click" this.toggle}}
>
{{#if this.editor.autoRender}}
<FaIcon @icon={{faPause}} />
{{else}}
<FaIcon @icon={{faPlay}} />
{{/if}}
</Button>
{{#unless this.editor.autoRender}}
<Button title="Render" data-test-render {{on "click" this.editor.render}}>
<FaIcon @icon={{faForwardStep}} />
</Button>
{{/unless}}
</template>
}
2 changes: 2 additions & 0 deletions apps/repl/app/templates/edit/layout/controls/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { castToBoolean, qp, withQP } from 'ember-primitives/qp';

import currentURL from 'limber/helpers/current-url';

import { AutoRender } from './auto-render.gts';
import { Button } from './button.gts';
import { FormatDocument } from './format-document.gts';

Expand Down Expand Up @@ -70,6 +71,7 @@ export const Controls: TOC<{
{{/if}}
</Button>
<FormatDocument />
<AutoRender />
<Button
title="Rotate Editor/Output orientation"
disabled={{or @isMaximized @isMinimized}}
Expand Down
127 changes: 127 additions & 0 deletions apps/repl/tests/application/auto-render-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { click, currentURL, find, settled, visit } from '@ember/test-helpers';
import { module, test } from 'qunit';

import LZString from 'lz-string';

import { Output } from '#components/output.gts';
import { AutoRender } from '#edit/layout/controls/auto-render.gts';
import { setupApplicationCompilerTest } from '#tests/helpers.ts';

import { getService } from '../helpers';
import { Page } from './-page';

const { compressToEncodedURIComponent } = LZString;

const toggle = '[data-test-auto-render]';
const render = '[data-test-render]';
const out = '[data-test-compiled-output] [data-test-text]';

const doc = (text: string) => `<template><p data-test-text>${text}</p></template>`;

async function type(text: string) {
getService('editor').updateText(doc(text));
await settled();
}

module('Editor > Auto render', function (hooks) {
setupApplicationCompilerTest(hooks);

const page = new Page();

hooks.beforeEach(function () {
this.owner.lookup('service:editor').setCodemirrorState = () => {};

this.owner.register(
'template:edit',
<template>
<AutoRender />
<Output @shadow={{false}} />
</template>
);
});

test('typing re-renders by default', async function (assert) {
await page.visitEdit('gjs', doc('one'));

assert.dom(out).hasText('one');
assert.dom(render).doesNotExist();

await type('two');

assert.dom(out).hasText('two');
});

test('while paused, typing does not compile until render is clicked', async function (assert) {
await page.visitEdit('gjs', doc('one'));
await click(toggle);

assert.true(currentURL().includes('autorender=off'), 'the URL has the paused state');

const before = find(out);

await type('two');

assert.dom(out).hasText('one');
assert.strictEqual(find(out), before, 'the output did not compile again');
assert.true(currentURL().includes('autorender=off'), 'typing keeps the paused state');

await click(render);

assert.dom(out).hasText('two');

await click(toggle);

assert.false(currentURL().includes('autorender'), 'the URL no longer has the paused state');
assert.dom(render).doesNotExist();

await type('three');

assert.dom(out).hasText('three');
});

test('render compiles again when the text did not change', async function (assert) {
await page.visitEdit('gjs', doc('one'));
await click(toggle);

const before = find(out);

await click(render);

assert.dom(out).hasText('one');
assert.notStrictEqual(find(out), before, 'the output compiled again');
});

test('after render is clicked, typing does not compile again', async function (assert) {
await page.visitEdit('gjs', doc('one'));
await click(toggle);
await type('two');
await click(render);

const before = find(out);

await type('three');

assert.dom(out).hasText('two');
assert.strictEqual(find(out), before, 'the output did not compile again');
});

test('a paused page renders once on load', async function (assert) {
await visit(`/edit?format=gjs&autorender=off&c=${compressToEncodedURIComponent(doc('one'))}`);

assert.dom(out).hasText('one');
assert.dom(render).exists();

await type('two');

assert.dom(out).hasText('one');
});

test('resuming renders the latest text', async function (assert) {
await visit(`/edit?format=gjs&autorender=off&c=${compressToEncodedURIComponent(doc('one'))}`);

await type('two');
await click(toggle);

assert.dom(out).hasText('two');
});
});
Loading