Init commit
This commit is contained in:
124
src/API.ts
Normal file
124
src/API.ts
Normal 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
94
src/AppSettings.ts
Normal 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
57
src/Clock.ts
Normal 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
113
src/EditorSetup.ts
Normal 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
31
src/Evaluator.ts
Normal 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
11
src/Time.ts
Normal 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
49
src/TransportNode.js
Normal 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
37
src/TransportProcessor.js
Normal 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
114
src/highlightSelection.ts
Normal 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
452
src/main.ts
Normal 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
1405
src/output.css
Normal file
File diff suppressed because it is too large
Load Diff
3
src/style.css
Normal file
3
src/style.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
209
src/themes/materialDark.ts
Normal file
209
src/themes/materialDark.ts
Normal 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
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user