1import { RangeSetBuilder, StateEffect, StateField, Prec } from '@codemirror/state'; 2import { Decoration, EditorView } from '@codemirror/view'; 3import { lineHighlightExtension, lineHighlights } from './lineHighlights.mjs'; 4 5export const setMiniLocations = StateEffect.define(); 6export const showMiniLocations = StateEffect.define(); 7export const displayMiniLocations = StateEffect.define(); 8export const updateMiniLocations = (view, locations, range = null) => { 9 view.dispatch({ effects: setMiniLocations.of({ locations, range }) }); 10}; 11export const highlightMiniLocations = (view, atTime, haps) => { 12 view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) }); 13}; 14 15const miniLocations = StateField.define({ 16 create() { 17 return Decoration.none; 18 }, 19 update(locations, tr) { 20 if (tr.docChanged) { 21 locations = locations.map(tr.changes); 22 } 23 24 for (let e of tr.effects) { 25 if (e.is(setMiniLocations)) { 26 //block-based eval case 27 if (e.value.range) { 28 const stateMiniLocations = getMiniLocationsFromDecorations(locations); 29 30 const normalized = e.value.locations 31 .filter(([from]) => from < tr.newDoc.length) 32 .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]); 33 34 const newIds = new Set(normalized.map((r) => r.join(':'))); 35 36 const marks = normalized.map((range) => { 37 const id = range.join(':'); 38 return Decoration.mark({ 39 id, 40 // this green is only to verify that the decoration moves when the document is edited 41 // it will be removed later, so the mark is not visible by default 42 attributes: { style: `background-color: #00CA2880` }, 43 }).range(...range); // -> Decoration 44 }); 45 46 const previousMarks = stateMiniLocations 47 .filter(({ id }) => !newIds.has(id)) 48 .map(({ from, to, id }) => 49 Decoration.mark({ 50 id, 51 attributes: { style: `background-color: #00CA2880` }, 52 }).range(from, to), 53 ); 54 55 locations = Decoration.set(previousMarks.concat(marks), true); // -> DecorationSet === RangeSet<Decoration> 56 } else { 57 // this is called on eval, with the mini locations obtained from the transpiler 58 // codemirror will automatically remap the marks when the document is edited 59 // create a mark for each mini location, adding the range to the spec to find it later 60 const marks = e.value.locations 61 .filter(([from]) => from < tr.newDoc.length) 62 .map(([from, to]) => [from, Math.min(to, tr.newDoc.length)]) 63 .map( 64 (range) => 65 Decoration.mark({ 66 id: range.join(':'), 67 // this green is only to verify that the decoration moves when the document is edited 68 // it will be removed later, so the mark is not visible by default 69 attributes: { style: `background-color: #00CA2880` }, 70 }).range(...range), // -> Decoration 71 ); 72 locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration> 73 } 74 } 75 } 76 77 return locations; 78 }, 79}); 80 81const visibleMiniLocations = StateField.define({ 82 create() { 83 return { atTime: 0, haps: new Map() }; 84 }, 85 update(visible, tr) { 86 for (let e of tr.effects) { 87 if (e.is(showMiniLocations)) { 88 // this is called every frame to show the locations that are currently active 89 // we can NOT create new marks because the context.locations haven't changed since eval time 90 // this is why we need to find a way to update the existing decorations, showing the ones that have an active range 91 const haps = new Map(); 92 for (let hap of e.value.haps) { 93 if (!hap.context?.locations || !hap.whole) { 94 continue; 95 } 96 for (let { start, end } of hap.context.locations) { 97 let id = `${start}:${end}`; 98 if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) { 99 haps.set(id, hap); 100 } 101 } 102 } 103 visible = { atTime: e.value.atTime, haps }; 104 } 105 } 106 107 return visible; 108 }, 109}); 110 111const displayMiniLocationsState = StateField.define({ 112 create() { 113 return true; // default to showing miniLocations 114 }, 115 update(display, tr) { 116 for (let e of tr.effects) { 117 if (e.is(displayMiniLocations)) { 118 display = e.value; 119 } 120 } 121 return display; 122 }, 123});
// Derive the set of decorations from the miniLocations and visibleLocations
126const miniLocationHighlights = EditorView.decorations.compute( 127 [miniLocations, visibleMiniLocations, displayMiniLocationsState], 128 (state) => { 129 // Check if miniLocations display is disabled 130 const shouldDisplay = state.field(displayMiniLocationsState); 131 if (!shouldDisplay) { 132 return Decoration.none; // Return empty decorations if display is disabled 133 } 134 135 const iterator = state.field(miniLocations).iter(); 136 const { haps } = state.field(visibleMiniLocations); 137 const builder = new RangeSetBuilder(); 138 139 while (iterator.value) { 140 const { 141 from, 142 to, 143 value: { 144 spec: { id }, 145 }, 146 } = iterator; 147 148 if (haps.has(id)) { 149 const hap = haps.get(id); 150 const color = hap.value?.color ?? 'var(--foreground)'; 151 const style = hap.value?.markcss || `outline: solid 2px ${color}`; 152 // Get explicit channels for color values 153 /* 154 const swatch = document.createElement('div'); 155 swatch.style.color = color; 156 document.body.appendChild(swatch); 157 let channels = getComputedStyle(swatch) 158 .color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/) 159 .slice(1) 160 .map((c) => parseFloat(c || 1)); 161 document.body.removeChild(swatch);
// Get percentage of event const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration; channels[3] *= percent;
178 iterator.next(); 179 } 180 181 return builder.finish(); 182 }, 183); 184 185const getMiniLocationsFromDecorations = (decorations) => { 186 const iterator = decorations.iter(); 187 const miniLocationsArray = []; 188 while (iterator.value) { 189 const { 190 from, 191 to, 192 value: { 193 spec: { id }, 194 }, 195 } = iterator; 196 miniLocationsArray.push({ 197 from, 198 to, 199 id, 200 }); 201 iterator.next(); 202 } 203 return miniLocationsArray; 204}; 205 206export const getMiniLocations = (state) => { 207 const decorations = state.field(miniLocations); 208 return getMiniLocationsFromDecorations(decorations); 209}; 210 211export const getActiveMiniLocations = (state) => { 212 const miniLocations = getMiniLocations(state); 213 const { haps } = state.field(visibleMiniLocations); 214 215 const activeMiniLocations = miniLocations.filter((location) => haps.has(location.id)); 216 return activeMiniLocations; 217};
jevstrudel: the same outlined locations, to tint their lines and show on the minimap (lineHighlights.mjs), each in its hap's colour.
221const miniLocationLines = lineHighlights.compute([miniLocations, visibleMiniLocations, displayMiniLocationsState], (state) => { 222 if (!state.field(displayMiniLocationsState)) return []; 223 const { haps } = state.field(visibleMiniLocations); 224 const ranges = []; 225 for (const { from, to, id } of getMiniLocationsFromDecorations(state.field(miniLocations))) { 226 const hap = haps.get(id); 227 if (!hap) continue; 228 const color = typeof hap.value?.color === 'string' ? hap.value.color : null; 229 ranges.push({ from, to, color, at: Number(hap.whole.begin) }); 230 } 231 return ranges; 232});
234export const highlightExtension = [ 235 miniLocations, 236 visibleMiniLocations, 237 displayMiniLocationsState, 238 miniLocationHighlights, 239 miniLocationLines, 240 lineHighlightExtension, 241]; 242 243export const isPatternHighlightingEnabled = (on, config) => { 244 // NOTE: 245 // Modified this function to always return the highlightExtension, and instead just toggle whether or not the miniLocations are displayed. 246 // This is because block based evaluation only updates regions of miniLocations, and those updates need to be kept track of constantly. 247 // The setTimeout was also removed because it conflicted with the range-specific updates required by 248 // block based evaluation. 249 // Not sure if this is the best approach, but for block based eval I can't think of a better way to do it. 250 251 if (config) { 252 // Toggle the display state for miniLocations 253 config.editor.dispatch({ effects: displayMiniLocations.of(on) }); 254 } 255 256 return Prec.highest(highlightExtension); 257};