Init commit

This commit is contained in:
Raphaël Forment
2023-07-28 01:23:38 +02:00
committed by GitHub
parent c34ee20306
commit aff5e643ac
20 changed files with 3848 additions and 0 deletions

124
src/API.ts Normal file
View File

@ -0,0 +1,124 @@
import { Editor } from "./main";
import { tryEvaluate } from "./Evaluator";
import { ZZFX, zzfx } from "zzfx";
export class UserAPI {
variables: { [key: string]: any } = {}
globalGain: GainNode
audioNodes: AudioNode[] = []
constructor(public app: Editor) {
this.globalGain = this.app.audioContext.createGain()
// Give default parameters to the reverb
this.globalGain.gain.value = 0.2;
this.globalGain.connect(this.app.audioContext.destination)
}
private registerNode<T>(node: T): T{
this.audioNodes.push(node)
return node
}
killAll() {
this.audioNodes.forEach(node => {
node.disconnect()
})
}
var(name: string, value: any) {
this.variables[name] = value
}
get(name: string) { return this.variables[name] }
pick<T>(array: T[]): T { return array[Math.floor(Math.random() * array.length)] }
almostNever() { return Math.random() > 0.9 }
sometimes() { return Math.random() > 0.5 }
rarely() { return Math.random() > 0.75 }
often() { return Math.random() > 0.25 }
almostAlways() { return Math.random() > 0.1 }
randInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min }
// Iterators
get i() { return this.app.universes[this.app.selected_universe].global.evaluations }
e(index:number) { return this.app.universes[this.app.selected_universe].locals[index].evaluations }
// Script launcher: can launch any number of scripts
script(...args: number[]): void {
args.forEach(arg => { tryEvaluate(this.app, this.app.universes[this.app.selected_universe].locals[arg]) })
}
// Small ZZFX interface for playing with this synth
zzfx(...thing: number[]) {
zzfx(...thing);
}
on(beat: number = 1, pulse: number = 1): boolean {
return this.app.clock.time_position.beat === beat && this.app.clock.time_position.pulse === pulse
}
pulse(pulse: number) {
return this.app.clock.time_position.pulse === pulse
}
modPulse(pulse: number) {
return this.app.clock.time_position.pulse % pulse === 0
}
mute() {
this.globalGain.gain.value = 0
}
volume(volume: number) {
this.globalGain.gain.value = volume
}
vol = this.volume
beep(
frequency: number = 400, duration: number = 0.2,
type: OscillatorType = "sine", filter: BiquadFilterType = "lowpass",
cutoff: number = 10000, resonance: number = 1,
) {
const oscillator = this.registerNode(this.app.audioContext.createOscillator());
const gainNode = this.registerNode(this.app.audioContext.createGain());
const limiterNode = this.registerNode(this.app.audioContext.createDynamicsCompressor());
const filterNode = this.registerNode(this.app.audioContext.createBiquadFilter());
// All this for the limiter
limiterNode.threshold.setValueAtTime(-5.0, this.app.audioContext.currentTime);
limiterNode.knee.setValueAtTime(0, this.app.audioContext.currentTime);
limiterNode.ratio.setValueAtTime(20.0, this.app.audioContext.currentTime);
limiterNode.attack.setValueAtTime(0.001, this.app.audioContext.currentTime);
limiterNode.release.setValueAtTime(0.05, this.app.audioContext.currentTime);
// Filter
filterNode.type = filter;
filterNode.frequency.value = cutoff;
filterNode.Q.value = resonance;
oscillator.type = type;
oscillator.frequency.value = frequency || 400;
gainNode.gain.value = 0.25;
oscillator
.connect(filterNode)
.connect(gainNode)
.connect(limiterNode)
.connect(this.globalGain)
oscillator.start();
gainNode.gain.exponentialRampToValueAtTime(0.00001, this.app.audioContext.currentTime + duration);
oscillator.stop(this.app.audioContext.currentTime + duration);
// Clean everything after a node has been played
oscillator.onended = () => {
oscillator.disconnect();
gainNode.disconnect();
filterNode.disconnect();
limiterNode.disconnect();
}
}
}

94
src/AppSettings.ts Normal file
View File

@ -0,0 +1,94 @@
export type Universes = { [key: string]: Universe }
export interface Universe {
global: File
locals: { [key: number]: File }
init: File
}
export interface File {
candidate: string
committed: string
evaluations: number
}
export interface Settings {
vimMode: boolean
theme: string
font: string
universes: Universes
}
export const template_universe = {
global: { candidate: "", committed: "", evaluations: 0 },
locals: {
1: { candidate: "", committed: "", evaluations: 0},
2: { candidate: "", committed: "", evaluations: 0},
3: { candidate: "", committed: "", evaluations: 0},
4: { candidate: "", committed: "", evaluations: 0},
5: { candidate: "", committed: "", evaluations: 0},
6: { candidate: "", committed: "", evaluations: 0},
7: { candidate: "", committed: "", evaluations: 0},
8: { candidate: "", committed: "", evaluations: 0},
9: { candidate: "", committed: "", evaluations: 0},
},
init: { candidate: "", committed: "", evaluations: 0 }
}
export const template_universes = {
"Default": {
global: { candidate: "", committed: "", evaluations: 0 },
locals: {
1: { candidate: "", committed: "", evaluations: 0},
2: { candidate: "", committed: "", evaluations: 0},
3: { candidate: "", committed: "", evaluations: 0},
4: { candidate: "", committed: "", evaluations: 0},
5: { candidate: "", committed: "", evaluations: 0},
6: { candidate: "", committed: "", evaluations: 0},
7: { candidate: "", committed: "", evaluations: 0},
8: { candidate: "", committed: "", evaluations: 0},
9: { candidate: "", committed: "", evaluations: 0},
},
init: { candidate: "", committed: "", evaluations: 0 }
}
}
export class AppSettings {
public vimMode: boolean = false
public theme: string = "materialDark"
public font: string = "SpaceMono"
public universes: Universes
constructor() {
const settingsFromStorage = JSON.parse(localStorage.getItem('topos') || "{}");
if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) {
// let settings = JSON.parse(localStorage.getItem("topos") as string)
this.vimMode = settingsFromStorage.vimMode
this.theme = settingsFromStorage.theme
this.font = settingsFromStorage.font
this.universes = settingsFromStorage.universes
} else {
this.universes = template_universes
}
}
get data(): Settings {
return {
vimMode: this.vimMode,
theme: this.theme,
font: this.font,
universes: this.universes
}
}
saveApplicationToLocalStorage(universes: Universes): void{
this.universes = universes;
localStorage.setItem('topos', JSON.stringify(this.data))
}
}

57
src/Clock.ts Normal file
View File

@ -0,0 +1,57 @@
// @ts-ignore
import { TransportNode } from './TransportNode';
import { Editor } from './main';
export interface TimePosition {
bar: number
beat: number
pulse: number
}
export class Clock {
evaluations: number
transportNode: TransportNode
bpm: number
time_signature: number[]
time_position: TimePosition
ppqn: number
constructor(public app: Editor, ctx: AudioContext) {
this.time_position = { bar: 0, beat: 0, pulse: 0 }
this.bpm = 120;
this.time_signature = [4, 4];
this.ppqn = 48;
this.evaluations = 0;
ctx.audioWorklet.addModule('src/TransportProcessor.js').then((e) => {
this.transportNode = new TransportNode(ctx, {}, this.app);
this.transportNode.connect(ctx.destination);
return e
})
.catch((e) => {
console.log('Error loading TransportProcessor.js:', e);
})
}
start(): void {
// Check if the clock is already running
if (this.transportNode?.state === 'running') {
console.log('Already started')
} else {
this.app.audioContext.resume()
this.transportNode?.start();
}
}
pause(): void {
this.transportNode?.pause();
}
stop(): void {
this.transportNode?.stop();
}
// Public methods
public toString(): string { return `` }
}

113
src/EditorSetup.ts Normal file
View File

@ -0,0 +1,113 @@
import { javascript } from "@codemirror/lang-javascript"
import {
keymap,
highlightSpecialChars,
drawSelection,
highlightActiveLine,
dropCursor,
rectangularSelection,
lineNumbers,
crosshairCursor,
highlightActiveLineGutter
} from "@codemirror/view"
import {
Extension,
EditorState
} from "@codemirror/state"
import {
defaultHighlightStyle,
syntaxHighlighting,
indentOnInput,
bracketMatching,
foldKeymap
} from "@codemirror/language"
import {
defaultKeymap,
historyKeymap,
history,
} from "@codemirror/commands"
import {
searchKeymap,
highlightSelectionMatches
} from "@codemirror/search"
import {
autocompletion,
completionKeymap,
closeBrackets,
closeBracketsKeymap
} from "@codemirror/autocomplete"
import {
lintKeymap
} from "@codemirror/lint"
import { materialDark } from "./themes/materialDark"
// (The superfluous function calls around the list of extensions work
// around current limitations in tree-shaking software.)
/// This is an extension value that just pulls together a number of
/// extensions that you might want in a basic editor. It is meant as a
/// convenient helper to quickly set up CodeMirror without installing
/// and importing a lot of separate packages.
///
/// Specifically, it includes...
///
/// - [the default command bindings](#commands.defaultKeymap)
/// - [line numbers](#view.lineNumbers)
/// - [special character highlighting](#view.highlightSpecialChars)
/// - [the undo history](#commands.history)
/// - [a fold gutter](#language.foldGutter)
/// - [custom selection drawing](#view.drawSelection)
/// - [drop cursor](#view.dropCursor)
/// - [multiple selections](#state.EditorState^allowMultipleSelections)
/// - [reindentation on input](#language.indentOnInput)
/// - [the default highlight style](#language.defaultHighlightStyle) (as fallback)
/// - [bracket matching](#language.bracketMatching)
/// - [bracket closing](#autocomplete.closeBrackets)
/// - [autocompletion](#autocomplete.autocompletion)
/// - [rectangular selection](#view.rectangularSelection) and [crosshair cursor](#view.crosshairCursor)
/// - [active line highlighting](#view.highlightActiveLine)
/// - [active line gutter highlighting](#view.highlightActiveLineGutter)
/// - [selection match highlighting](#search.highlightSelectionMatches)
/// - [search](#search.searchKeymap)
/// - [linting](#lint.lintKeymap)
///
/// (You'll probably want to add some language package to your setup
/// too.)
///
/// This extension does not allow customization. The idea is that,
/// once you decide you want to configure your editor more precisely,
/// you take this package's source (which is just a bunch of imports
/// and an array literal), copy it into your own code, and adjust it
/// as desired.
export const editorSetup: Extension = (() => [
materialDark,
lineNumbers(),
javascript(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
// foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
])
])()

31
src/Evaluator.ts Normal file
View File

@ -0,0 +1,31 @@
import type { Editor } from './main';
import type { File } from './AppSettings';
/* This mode of evaluation can only work if the whole buffer is evaluated at once */
export const tryEvaluate = (application: Editor, code: File): void => {
let isValidCode: boolean;
try {
Function(`with (this) {try{${code.candidate}} catch (e) {console.log(e)}};`).call(application.api)
code.evaluations++;
isValidCode = true;
} catch (error) {
Function(`with (this) {try{${code.committed}} catch (e) {console.log(e)}};`).call(application.api)
code.evaluations++;
isValidCode = false;
}
if (isValidCode) {
code.committed = code.candidate;
} else {
evaluate(application, code);
}
}
export const evaluate = (application: Editor, code: File): void => {
Function(`with (this) {try{${code.committed}} catch (e) {console.log(e)}};`).call(application.api)
code.evaluations++;
}
export const evaluateCommand = (application: Editor, command: string): void => {
Function(`with (this) {try{${command}} catch (e) {console.log(e)}};`).call(application.api)
}

11
src/Time.ts Normal file
View File

@ -0,0 +1,11 @@
class Ligne {
public start: number
public end: number
constructor(start: number, end: number)) {
this.start = start
this.end = end
}
}

49
src/TransportNode.js Normal file
View File

@ -0,0 +1,49 @@
import { evaluate, tryEvaluate, evaluateCommand } from "./Evaluator";
export class TransportNode extends AudioWorkletNode {
constructor(context, options, application) {
super(context, "transport", options);
this.app = application
this.port.addEventListener("message", this.handleMessage);
this.port.start();
/** @type {HTMLSpanElement} */
this.$clock = document.getElementById("clockviewer");
this.offset_time = 0;
}
/** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */
handleMessage = (message) => {
if (message.data === "bang") {
let info = this.convertTimeToBarsBeats(this.context.currentTime);
this.$clock.innerHTML = `${info.bar} / ${info.beat} / ${info.ppqn}`
this.app.clock.time_position = { bar: info.bar, beat: info.beat, pulse: info.ppqn }
tryEvaluate( this.app, this.app.global_buffer );
}
};
start() {
this.port.postMessage("start");
}
pause() {
this.port.postMessage("pause");
}
convertTimeToBarsBeats(currentTime) {
// Calculate the duration of one beat in seconds
const beatDuration = 60 / this.app.clock.bpm;
// Calculate the beat number
const beatNumber = (currentTime - this.offset_time) / beatDuration;
// Calculate the bar and beat numbers
const beatsPerBar = this.app.clock.time_signature[0];
const barNumber = Math.floor(beatNumber / beatsPerBar) + 1; // Adding 1 to make it 1-indexed
const beatWithinBar = Math.floor(beatNumber % beatsPerBar) + 1; // Adding 1 to make it 1-indexed
// Calculate the PPQN position
const ppqnPosition = Math.floor((beatNumber % 1) * this.app.clock.ppqn);
return { bar: barNumber, beat: beatWithinBar, ppqn: ppqnPosition };
}
}

37
src/TransportProcessor.js Normal file
View File

@ -0,0 +1,37 @@
class TransportProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.port.addEventListener("message", this.handleMessage);
this.port.start();
this.interval = 0.001;
this.origin = currentTime;
this.next = this.origin + this.interval;
}
handleMessage = (message) => {
if (message.data === "start") {
this.origin = currentTime;
this.next = this.origin + this.interval;
} else if (message.data === "pause") {
this.next = Infinity;
} else if (message.data === "stop") {
this.origin = currentTime;
this.next = Infinity;
}
};
process(inputs, outputs, parameters) {
if (currentTime >= this.next) {
while (this.next < currentTime)
this.next += this.interval;
this.port.postMessage("bang");
}
return true;
}
}
registerProcessor(
"transport",
TransportProcessor
);

114
src/highlightSelection.ts Normal file
View File

@ -0,0 +1,114 @@
import { Decoration, DecorationSet } from "@codemirror/view"
import { StateField, StateEffect, ChangeDesc } from "@codemirror/state"
import { EditorView } from "@codemirror/view"
import { invertedEffects } from "@codemirror/commands"
import { Extension } from "@codemirror/state"
function mapRange(range: {from: number, to: number}, change: ChangeDesc) {
let from = change.mapPos(range.from), to = change.mapPos(range.to)
return from < to ? {from, to} : undefined
}
const addHighlight = StateEffect.define<{from: number, to: number}>({
map: mapRange
})
const removeHighlight = StateEffect.define<{from: number, to: number}>({
map: mapRange
})
const highlight = Decoration.mark({
attributes: {style: `background-color: #ffad42`}
})
const highlightedRanges = StateField.define({
create() {
return Decoration.none
},
update(ranges, tr) {
ranges = ranges.map(tr.changes)
for (let e of tr.effects) {
if (e.is(addHighlight))
ranges = addRange(ranges, e.value)
else if (e.is(removeHighlight))
ranges = cutRange(ranges, e.value)
}
return ranges
},
provide: field => EditorView.decorations.from(field)
})
function cutRange(ranges: DecorationSet, r: {from: number, to: number}) {
let leftover: any[] = []
ranges.between(r.from, r.to, (from, to, deco) => {
if (from < r.from) leftover.push(deco.range(from, r.from))
if (to > r.to) leftover.push(deco.range(r.to, to))
})
return ranges.update({
filterFrom: r.from,
filterTo: r.to,
filter: () => false,
add: leftover
})
}
function addRange(ranges: DecorationSet, r: {from: number, to: number}) {
ranges.between(r.from, r.to, (from, to) => {
if (from < r.from) r = {from, to: r.to}
if (to > r.to) r = {from: r.from, to}
})
return ranges.update({
filterFrom: r.from,
filterTo: r.to,
filter: () => false,
add: [highlight.range(r.from, r.to)]
})
}
const invertHighlight = invertedEffects.of(tr => {
let found = []
for (let e of tr.effects) {
if (e.is(addHighlight)) found.push(removeHighlight.of(e.value))
else if (e.is(removeHighlight)) found.push(addHighlight.of(e.value))
}
let ranges = tr.startState.field(highlightedRanges)
tr.changes.iterChangedRanges((chFrom, chTo) => {
ranges.between(chFrom, chTo, (rFrom, rTo) => {
if (rFrom >= chFrom || rTo <= chTo) {
let from = Math.max(chFrom, rFrom), to = Math.min(chTo, rTo)
if (from < to) found.push(addHighlight.of({from, to}))
}
})
})
return found
})
export function highlightSelection(view: EditorView) {
view.dispatch({
effects: view.state.selection.ranges.filter(r => !r.empty)
.map(r => addHighlight.of(r))
})
return true
}
export function unhighlightSelection(view: EditorView) {
let highlighted = view.state.field(highlightedRanges)
let effects: any[] = []
for (let sel of view.state.selection.ranges) {
highlighted.between(sel.from, sel.to, (rFrom, rTo) => {
let from = Math.max(sel.from, rFrom), to = Math.min(sel.to, rTo)
if (from < to) effects.push(removeHighlight.of({from, to}))
})
}
view.dispatch({effects})
return true
}
export function rangeHighlighting(): Extension {
return [
highlightedRanges,
invertHighlight,
]
}

452
src/main.ts Normal file
View File

@ -0,0 +1,452 @@
import './style.css'
import { EditorView } from "codemirror";
import { editorSetup } from './EditorSetup';
import { EditorState, Compartment } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";
import { Clock } from './Clock'
import { vim } from "@replit/codemirror-vim";
import { AppSettings } from './AppSettings';
import { ViewUpdate } from '@codemirror/view';
import {
highlightSelection,
unhighlightSelection,
rangeHighlighting
} from "./highlightSelection";
import { UserAPI } from './API';
import { Extension } from '@codemirror/state';
import { Universes, File, template_universe } from './AppSettings';
import { tryEvaluate } from './Evaluator';
export class Editor {
// Data structures for editor text management
universes: Universes
selected_universe: string
local_index: number = 1
editor_mode: 'global' | 'local' | 'init' = 'local'
settings = new AppSettings()
editorExtensions: Extension[] = []
userPlugins: Extension[] = []
state: EditorState
api: UserAPI
// Audio stuff
audioContext: AudioContext
view: EditorView
clock: Clock
// Transport elements
play_button: HTMLButtonElement = document.getElementById('play-button') as HTMLButtonElement
pause_button: HTMLButtonElement = document.getElementById('pause-button') as HTMLButtonElement
clear_button: HTMLButtonElement = document.getElementById('clear-button') as HTMLButtonElement
// Script selection elements
local_button: HTMLButtonElement = document.getElementById('local-button') as HTMLButtonElement
global_button: HTMLButtonElement = document.getElementById('global-button') as HTMLButtonElement
init_button: HTMLButtonElement = document.getElementById('init-button') as HTMLButtonElement
universe_viewer: HTMLDivElement = document.getElementById('universe-viewer') as HTMLDivElement
// Buffer modal
buffer_modal: HTMLDivElement = document.getElementById('modal-buffers') as HTMLDivElement
buffer_search: HTMLInputElement = document.getElementById('buffer-search') as HTMLInputElement
settings_modal: HTMLDivElement = document.getElementById('modal-settings') as HTMLDivElement
// Local script tabs
local_script_tabs: HTMLDivElement = document.getElementById('local-script-tabs') as HTMLDivElement
constructor() {
// ================================================================================
// Loading the universe from local storage
// ================================================================================
this.selected_universe = "Default";
this.universe_viewer.innerHTML = `Topos: ${this.selected_universe}`
this.universes = this.settings.universes
// ================================================================================
// Audio context and clock
// ================================================================================
this.audioContext = new AudioContext({ sampleRate: 44100, latencyHint: 0.000001});
this.clock = new Clock(this, this.audioContext);
// ================================================================================
// User API
// ================================================================================
this.api = new UserAPI(this);
// ================================================================================
// CodeMirror Management
// ================================================================================
this.editorExtensions = [
editorSetup,
rangeHighlighting(),
javascript(),
EditorView.updateListener.of((v:ViewUpdate) => {
// This is the event listener for the editor
}),
...this.userPlugins
]
let dynamicPlugins = new Compartment;
this.state = EditorState.create({
extensions: [
...this.editorExtensions,
EditorView.lineWrapping,
dynamicPlugins.of(this.userPlugins)
],
doc: this.universes[this.selected_universe].locals[this.local_index].candidate
})
this.view = new EditorView({
parent: document.getElementById('editor') as HTMLElement,
state: this.state
});
// ================================================================================
// Application event listeners
// ================================================================================
document.addEventListener('keydown', (event: KeyboardEvent) => {
// TAB should do nothing
if (event.key === 'Tab') {
event.preventDefault();
}
// Ctrl + Shift + V: Vim Mode
if ((event.key === 'v' || event.key === 'V') && event.ctrlKey && event.shiftKey) {
this.settings.vimMode = !this.settings.vimMode
event.preventDefault();
this.userPlugins = this.settings.vimMode ? [] : [vim()]
this.view.dispatch({
effects: dynamicPlugins.reconfigure(this.userPlugins)
})
}
// Ctrl + Enter or Return: Evaluate the hovered code block
if ((event.key === 'Enter' || event.key === 'Return') && event.ctrlKey) {
event.preventDefault();
const code = this.getCodeBlock();
this.currentFile.candidate = this.view.state.doc.toString()
tryEvaluate(this, this.currentFile)
}
// Shift + Enter or Ctrl + E: evaluate the line
if ((event.key === 'Enter' && event.shiftKey) || (event.key === 'e' && event.ctrlKey)) {
event.preventDefault(); // Prevents the addition of a new line
this.currentFile.candidate = this.view.state.doc.toString()
const code = this.getSelectedLines();
}
// This is the modal to switch between universes
if (event.metaKey && event.key === "b") {
this.openBuffersModal()
}
// This is the modal that opens up the settings
if (event.shiftKey && event.key === "Escape") {
this.openSettingsModal()
}
});
// ================================================================================
// Interface buttons
// ================================================================================
let tabs = document.querySelectorAll('[id^="tab-"]');
// Iterate over the tabs with an index
for (let i = 0; i < tabs.length; i++) {
tabs[i].addEventListener('click', (event) => {
// Updating the CSS accordingly
tabs[i].classList.add('bg-orange-300')
for (let j = 0; j < tabs.length; j++) {
if (j != i) tabs[j].classList.remove('bg-orange-300')
}
this.currentFile.candidate = this.view.state.doc.toString()
let tab = event.target as HTMLElement
let tab_id = tab.id.split('-')[1]
this.local_index = parseInt(tab_id)
this.updateEditorView()
})
}
this.play_button.addEventListener('click', () => {
this.play_button.children[0].classList.add('fill-orange-300')
this.pause_button.children[0].classList.remove('fill-orange-300')
this.clock.start()
})
this.clear_button.addEventListener('click', () => {
// Reset the current universe to a template
if (confirm('Do you want to reset the current universe?')) {
this.universes[this.selected_universe] = template_universe
this.updateEditorView()
}
});
this.pause_button.addEventListener('click', () => {
// Change the color of the button
this.play_button.children[0].classList.remove('fill-orange-300')
this.pause_button.children[0].classList.add('fill-orange-300')
this.clock.pause()
})
this.local_button.addEventListener('click', () => this.changeModeFromInterface('local'))
this.global_button.addEventListener('click', () => this.changeModeFromInterface('global'))
this.init_button.addEventListener('click', () => this.changeModeFromInterface('init'))
this.buffer_search.addEventListener('keydown', (event) => {
if (event.key === "Enter") {
let query = this.buffer_search.value
if (query.length > 2 && query.length < 20) {
this.loadUniverse(query)
this.buffer_search.value = ""
this.closeBuffersModal()
}
}
})
}
get global_buffer() {
return this.universes[this.selected_universe.toString()].global
}
get init_buffer() {
return this.universes[this.selected_universe.toString()].init
}
changeModeFromInterface(mode: 'global' | 'local' | 'init') {
const interface_buttons = [this.local_button, this.global_button, this.init_button]
let changeColor = (button: HTMLElement) => {
interface_buttons.forEach(button => {
// Get the child svg element of each button
let svg = button.children[0] as HTMLElement
if (svg.classList.contains('text-orange-300')) {
svg.classList.remove('text-orange-300')
svg.classList.add('text-white')
}
})
button.children[0].classList.add('text-orange-300')
}
if (mode === this.editor_mode) return
switch (mode) {
case 'local':
if (this.local_script_tabs.classList.contains('hidden')) {
this.local_script_tabs.classList.remove('hidden')
}
this.currentFile.candidate = this.view.state.doc.toString()
changeColor(this.local_button)
this.editor_mode = 'local';
break;
case 'global':
if (!this.local_script_tabs.classList.contains('hidden')) {
this.local_script_tabs.classList.add('hidden')
}
this.currentFile.candidate = this.view.state.doc.toString()
changeColor(this.global_button)
this.editor_mode = 'global';
break;
case 'init':
if (!this.local_script_tabs.classList.contains('hidden')) {
this.local_script_tabs.classList.add('hidden')
}
this.currentFile.candidate = this.view.state.doc.toString()
changeColor(this.init_button)
this.editor_mode = 'init';
break;
}
this.updateEditorView();
}
updateEditorView():void {
// Remove everything from the editor
this.view.dispatch({
changes: {
from: 0,
to: this.view.state.doc.toString().length,
insert:''
}
})
// Insert something
this.view.dispatch({
changes: {
from: 0,
insert: this.currentFile.candidate
}
});
}
get currentFile(): File {
switch (this.editor_mode) {
case 'global': return this.global_buffer;
case 'local': return this.universes[this.selected_universe].locals[this.local_index];
case 'init': return this.init_buffer;
}
}
loadUniverse(universeName: string) {
this.currentFile.candidate = this.view.state.doc.toString()
let editor = this;
function whichBuffer(editor: Editor): File {
switch (editor.editor_mode) {
case 'global': return editor.global_buffer
case 'local': return editor.universes[
editor.selected_universe].locals[editor.local_index]
case 'init': return editor.init_buffer
}
}
let selectedUniverse = universeName.trim()
if (this.universes[selectedUniverse] === undefined) {
this.universes[selectedUniverse] = template_universe
}
this.selected_universe = selectedUniverse
this.universe_viewer.innerHTML = `Topos: ${selectedUniverse}`
this.global_buffer = this.universes[this.selected_universe.toString()].global
this.init_buffer = this.universes[this.selected_universe.toString()].init
// We should also update the editor accordingly
this.view.dispatch({
changes: { from: 0, to: this.view.state.doc.toString().length, insert:'' }
})
this.view.dispatch({
changes: { from: 0, insert: this.currentFile.candidate }
});
}
getCodeBlock(): string {
// Capture the position of the cursor
let cursor = this.view.state.selection.main.head
const state = this.view.state;
const { head } = state.selection.main;
const currentLine = state.doc.lineAt(head);
let startLine = currentLine;
while (startLine.number > 1 && !/^\s*$/.test(state.doc.line(startLine.number - 1).text)) {
startLine = state.doc.line(startLine.number - 1);
}
let endLine = currentLine;
while (
endLine.number < state.doc.lines && !/^\s*$/.test(state.doc.line(endLine.number + 1).text)) {
endLine = state.doc.line(endLine.number + 1);
}
this.view.dispatch({selection: {anchor: 0 + startLine.from, head: endLine.to}});
highlightSelection(this.view);
setTimeout(() => {
unhighlightSelection(this.view)
this.view.dispatch({selection: {anchor: cursor, head: cursor}});
}, 200);
let result_string = state.doc.sliceString(startLine.from, endLine.to);
result_string = result_string.split('\n').map((line, index, lines) => {
const trimmedLine = line.trim();
if (index === lines.length - 1 || /^\s/.test(lines[index + 1]) || trimmedLine.startsWith('@')) {
return line;
} else {
return line + ';\\';
}
}).join('\n');
return result_string
}
getSelectedLines = (): string => {
const state = this.view.state;
const { from, to } = state.selection.main;
const fromLine = state.doc.lineAt(from);
const toLine = state.doc.lineAt(to);
this.view.dispatch({selection: {anchor: 0 + fromLine.from, head: toLine.to}});
// Release the selection and get the cursor back to its original position
// Blink the text!
highlightSelection(this.view);
setTimeout(() => {
unhighlightSelection(this.view)
this.view.dispatch({selection: {anchor: from, head: from}});
}, 200);
return state.doc.sliceString(fromLine.from, toLine.to);
}
openSettingsModal() {
// If the modal is hidden, unhide it and hide the editor
if (document.getElementById('modal-settings')!.classList.contains('invisible')) {
document.getElementById('editor')!.classList.add('invisible')
document.getElementById('modal-settings')!.classList.remove('invisible')
} else {
this.closeSettingsModal();
}
}
closeSettingsModal() {
document.getElementById('editor')!.classList.remove('invisible')
document.getElementById('modal-settings')!.classList.add('invisible')
}
openBuffersModal() {
// If the modal is hidden, unhide it and hide the editor
if (document.getElementById('modal-buffers')!.classList.contains('invisible')) {
document.getElementById('editor')!.classList.add('invisible')
document.getElementById('modal-buffers')!.classList.remove('invisible')
document.getElementById('buffer-search')!.focus()
} else {
this.closeBuffersModal();
}
}
closeBuffersModal() {
// @ts-ignore
document.getElementById('buffer-search')!.value = ''
document.getElementById('editor')!.classList.remove('invisible')
document.getElementById('modal-buffers')!.classList.add('invisible')
document.getElementById('modal')!.classList.add('invisible')
}
}
const app = new Editor()
function startClock() {
document.getElementById('editor')!.classList.remove('invisible')
document.getElementById('modal')!.classList.add('hidden')
document.getElementById('start-button')!.removeEventListener('click', startClock);
document.removeEventListener('keydown', startOnEnter)
app.clock.start()
app.view.focus()
// Change the play button svg color to orange
document.getElementById('pause-button')!.children[0].classList.remove('fill-orange-300')
document.getElementById('play-button')!.children[0].classList.add('fill-orange-300')
}
function startOnEnter(e: KeyboardEvent) {
if (e.code === 'Enter' || e.code === "Space") startClock()
}
document.addEventListener('keydown', startOnEnter)
document.getElementById('start-button')!.addEventListener(
'click', startClock);
// When the user leaves the page, all the universes should be saved in the localStorage
window.addEventListener('beforeunload', () => {
// Iterate over all local files and set the candidate to the committed
app.currentFile.candidate = app.view.state.doc.toString()
app.currentFile.committed = app.view.state.doc.toString()
app.settings.saveApplicationToLocalStorage(app.universes)
return null;
});

1405
src/output.css Normal file

File diff suppressed because it is too large Load Diff

3
src/style.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

209
src/themes/materialDark.ts Normal file
View File

@ -0,0 +1,209 @@
import { EditorView } from '@codemirror/view'
import { Extension } from '@codemirror/state'
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { tags as t } from '@lezer/highlight'
const base00 = 'black', base01 = '#505d64',
base02 = 'white', base03 = '#707d8b',
base04 = '#a0a4ae', base05 = '#bdbdbd',
base06 = '#e0e0e0', base07 = '#fdf6e3',
base_red = '#ff5f52', base_deeporange = '#ff6e40',
base_pink = '#fa5788', base_yellow = '#facf4e',
base_orange = '#ffad42', base_cyan = '#1E6AE1',
base_indigo = '#7186f0', base_purple = '#D09EBF',
base_green = '#82d47c', base_lightgreen = '#82d47c',
base_teal = '#4ebaaa'
const invalid = base_red,
darkBackground = '#fdf6e3',
highlightBackground = '#545b61',
background = base00,
tooltipBackground = base01,
selection = base07,
cursor = base04
/// The editor theme styles for Material Dark.
export const materialDarkTheme = EditorView.theme(
{
'&': {
color: base05,
backgroundColor: background
},
'.cm-content': {
caretColor: cursor
},
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
{ backgroundColor: selection, border: `0.5px solid ${base_teal}` },
'.cm-panels': { backgroundColor: darkBackground, color: base03 },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
'.cm-searchMatch': {
outline: `1px solid ${base_yellow}`,
backgroundColor: 'transparent'
},
'.cm-searchMatch.cm-searchMatch-selected': {
backgroundColor: highlightBackground
},
'.cm-activeLine': { backgroundColor: highlightBackground },
'.cm-selectionMatch': {
backgroundColor: darkBackground,
outline: `1px solid ${base_teal}`
},
'&.cm-focused .cm-matchingBracket': {
color: base06,
outline: `1px solid ${base_teal}`
},
'&.cm-focused .cm-nonmatchingBracket': {
color: base_red
},
'.cm-gutters': {
backgroundColor: base00,
borderRight: `1px solid ${base07}`,
color: base02
},
'.cm-activeLineGutter': {
backgroundColor: highlightBackground,
color: base07
},
'.cm-foldPlaceholder': {
backgroundColor: 'transparent',
border: 'none',
color: `${base07}`,
},
'.cm-tooltip': {
border: 'none',
backgroundColor: tooltipBackground
},
'.cm-tooltip .cm-tooltip-arrow:before': {
borderTopColor: 'transparent',
borderBottomColor: 'transparent'
},
'.cm-tooltip .cm-tooltip-arrow:after': {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground
},
'.cm-tooltip-autocomplete': {
'& > ul > li[aria-selected]': {
backgroundColor: highlightBackground,
color: base03
}
}
},
{ dark: true }
)
/// The highlighting style for code in the Material Dark theme.
export const materialDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: base_purple },
{
tag: [t.name, t.deleted, t.character, t.macroName],
color: base_cyan
},
{ tag: [t.propertyName], color: base_yellow },
{ tag: [t.variableName], color: base05 },
{ tag: [t.function(t.variableName)], color: base_cyan },
{ tag: [t.labelName], color: base_purple },
{
tag: [t.color, t.constant(t.name), t.standard(t.name)],
color: base_yellow
},
{ tag: [t.definition(t.name), t.separator], color: base_pink },
{ tag: [t.brace], color: base_purple },
{
tag: [t.annotation],
color: invalid
},
{
tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
color: base_orange
},
{
tag: [t.typeName, t.className],
color: base_orange
},
{
tag: [t.operator, t.operatorKeyword],
color: base_indigo
},
{
tag: [t.tagName],
color: base_deeporange
},
{
tag: [t.squareBracket],
color: base_red
},
{
tag: [t.angleBracket],
color: base02
},
{
tag: [t.attributeName],
color: base05
},
{
tag: [t.regexp],
color: invalid
},
{
tag: [t.quote],
color: base_green
},
{ tag: [t.string], color: base_lightgreen },
{
tag: t.link,
color: base_cyan,
textDecoration: 'underline',
textUnderlinePosition: 'under'
},
{
tag: [t.url, t.escape, t.special(t.string)],
color: base_yellow
},
{ tag: [t.meta], color: base03 },
{ tag: [t.comment], color: base02, fontStyle: 'italic' },
{ tag: t.monospace, color: base05 },
{ tag: t.strong, fontWeight: 'bold', color: base_red },
{ tag: t.emphasis, fontStyle: 'italic', color: base_lightgreen },
{ tag: t.strikethrough, textDecoration: 'line-through' },
{ tag: t.heading, fontWeight: 'bold', color: base_yellow },
{ tag: t.heading1, fontWeight: 'bold', color: base_yellow },
{
tag: [t.heading2, t.heading3, t.heading4],
fontWeight: 'bold',
color: base_yellow
},
{
tag: [t.heading5, t.heading6],
color: base_yellow
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: base_cyan },
{
tag: [t.processingInstruction, t.inserted],
color: base_red
},
{
tag: [t.contentSeparator],
color: base_cyan
},
{ tag: t.invalid, color: base02, borderBottom: `1px dotted ${base_red}` }
])
/// Extension to enable the Material Dark theme (both the editor theme and
/// the highlight style).
export const materialDark: Extension = [
materialDarkTheme,
syntaxHighlighting(materialDarkHighlightStyle)
]

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />