Merge pull request 'Feature: Synonyms in autocomplete and reference' (#1535) from glossing/strudel:glossing/synonyms-in-autocomplete into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1535
This commit is contained in:
froos 2025-09-14 00:49:16 +02:00
commit f0839e02eb
6 changed files with 92 additions and 26 deletions

View file

@ -54,11 +54,12 @@ const buildExamples = (examples) =>
` `
: ''; : '';
export const Autocomplete = ({ doc, label }) => export const Autocomplete = (doc) =>
h` h`
<div class="autocomplete-info-container"> <div class="autocomplete-info-container">
<div class="autocomplete-info-tooltip"> <div class="autocomplete-info-tooltip">
<h3 class="autocomplete-info-function-name">${label || getDocLabel(doc)}</h3> <h3 class="autocomplete-info-function-name">${getDocLabel(doc)}</h3>
${doc.synonyms_text ? `<div class="autocomplete-info-function-synonyms">Synonyms: ${doc.synonyms_text}</div>` : ''}
${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''} ${doc.description ? `<div class="autocomplete-info-function-description">${doc.description}</div>` : ''}
${buildParamsList(doc.params)} ${buildParamsList(doc.params)}
${buildExamples(doc.examples)} ${buildExamples(doc.examples)}
@ -74,15 +75,43 @@ const isValidDoc = (doc) => {
const hasExcludedTags = (doc) => const hasExcludedTags = (doc) =>
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
const jsdocCompletions = jsdoc.docs export const getSynonymDoc = (doc, synonym) => {
.filter((doc) => isValidDoc(doc) && !hasExcludedTags(doc)) const synonyms = doc.synonyms || [];
// https://codemirror.net/docs/ref/#autocomplete.Completion const docLabel = getDocLabel(doc);
.map((doc) => ({ // Swap `doc.name` in for `s` in the list of synonyms
label: getDocLabel(doc), const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label. return {
info: () => Autocomplete({ doc }), ...doc,
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type name: synonym,
})); longname: synonym,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
};
};
const jsdocCompletions = (() => {
const seen = new Set(); // avoid repetition
const completions = [];
for (const doc of jsdoc.docs) {
if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
const docLabel = getDocLabel(doc);
// Remove duplicates
const synonyms = doc.synonyms || [];
let labels = [docLabel, ...synonyms];
for (const label of labels) {
// https://codemirror.net/docs/ref/#autocomplete.Completion
if (label && !seen.has(label)) {
seen.add(label);
completions.push({
label,
info: () => Autocomplete(getSynonymDoc(doc, label)),
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
});
}
}
}
return completions;
})();
export const strudelAutocomplete = (context) => { export const strudelAutocomplete = (context) => {
const word = context.matchBefore(/\w*/); const word = context.matchBefore(/\w*/);

View file

@ -1,6 +1,6 @@
import { hoverTooltip } from '@codemirror/view'; import { hoverTooltip } from '@codemirror/view';
import jsdoc from '../../doc.json'; import jsdoc from '../../doc.json';
import { Autocomplete } from './autocomplete.mjs'; import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
const getDocLabel = (doc) => doc.name || doc.longname; const getDocLabel = (doc) => doc.name || doc.longname;
@ -52,10 +52,11 @@ export const strudelTooltip = hoverTooltip(
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0]; let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
if (!entry) { if (!entry) {
// Try for synonyms // Try for synonyms
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0]; const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
if (!entry) { if (!doc) {
return null; return null;
} }
entry = getSynonymDoc(doc, word);
} }
return { return {
@ -66,7 +67,7 @@ export const strudelTooltip = hoverTooltip(
create(view) { create(view) {
let dom = document.createElement('div'); let dom = document.createElement('div');
dom.className = 'strudel-tooltip'; dom.className = 'strudel-tooltip';
const ac = Autocomplete({ doc: entry, label: word }); const ac = Autocomplete(entry);
dom.appendChild(ac); dom.appendChild(ac);
return { dom }; return { dom };
}, },

View file

@ -91,6 +91,7 @@ export const { s, sound } = registerControl(['s', 'n', 'gain'], 'sound');
* Define a custom webaudio node to use as a sound source. * Define a custom webaudio node to use as a sound source.
* *
* @name source * @name source
* @synonyms src
* @param {function} getSource * @param {function} getSource
* @synonyms src * @synonyms src
* *
@ -536,6 +537,7 @@ export const { tremolophase } = registerControl('tremolophase', 'tremphase');
* shape of amplitude modulation * shape of amplitude modulation
* *
* @name tremoloshape * @name tremoloshape
* @synonyms tremshape
* @param {number | Pattern} shape tri | square | sine | saw | ramp * @param {number | Pattern} shape tri | square | sine | saw | ramp
* @example * @example
* note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth") * note("{f g c d}%16").tremsync(4).tremoloshape("<sine tri square>").s("sawtooth")
@ -551,11 +553,13 @@ export const { tremoloshape } = registerControl('tremoloshape', 'tremshape');
* note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>") * note("{f g g c d a a#}%16".sub(17)).s("supersaw").lpenv(8).lpf(150).lpq(.8).ftype('ladder').drive("<.5 4>")
* *
*/ */
export const { drive } = registerControl('drive');
/** /**
* modulate the amplitude of an orbit to create a "sidechain" like effect * modulate the amplitude of an orbit to create a "sidechain" like effect
* *
* @name duckorbit * @name duckorbit
* @synonyms duck
* @param {number | Pattern} orbit target orbit * @param {number | Pattern} orbit target orbit
* @example * @example
* $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2) * $: n(run(16)).scale("c:minor:pentatonic").s("sawtooth").delay(.7).orbit(2)
@ -580,6 +584,7 @@ export const { duckdepth } = registerControl('duckdepth');
* the attack time of the duck effect * the attack time of the duck effect
* *
* @name duckattack * @name duckattack
* @synonyms duckatt
* @param {number | Pattern} time * @param {number | Pattern} time
* @example * @example
* stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1)) * stack( n(run(8)).scale("c:minor").s("sawtooth").delay(.7).orbit(2), s("bd:4!4").beat("0,4,8,11,14",16).duckorbit(2).duckattack("<0.2 0 0.4>").duckdepth(1))
@ -587,8 +592,6 @@ export const { duckdepth } = registerControl('duckdepth');
*/ */
export const { duckattack } = registerControl('duckattack', 'duckatt'); export const { duckattack } = registerControl('duckattack', 'duckatt');
export const { drive } = registerControl('drive');
/** /**
* Create byte beats with custom expressions * Create byte beats with custom expressions
* *
@ -711,7 +714,7 @@ export const { phasercenter, phc } = registerControl('phasercenter', 'phc');
* The amount the signal is affected by the phaser effect. Defaults to 0.75 * The amount the signal is affected by the phaser effect. Defaults to 0.75
* *
* @name phaserdepth * @name phaserdepth
* @synonyms phd * @synonyms phd, phasdp
* @param {number | Pattern} depth number between 0 and 1 * @param {number | Pattern} depth number between 0 and 1
* @example * @example
* n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5) * n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
@ -1206,6 +1209,7 @@ export const { dry } = registerControl('dry');
* Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope. * Used when using `begin`/`end` or `chop`/`striate` and friends, to change the fade out time of the 'grain' envelope.
* *
* @name fadeTime * @name fadeTime
* @synonyms fadeOutTime
* @param {number | Pattern} time between 0 and 1 * @param {number | Pattern} time between 0 and 1
* @example * @example
* s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc() * s("oh*4").end(.1).fadeTime("<0 .2 .4 .8>").osc()

View file

@ -1246,7 +1246,8 @@ export const silence = gap(1);
/* Like silence, but with a 'steps' (relative duration) of 0 */ /* Like silence, but with a 'steps' (relative duration) of 0 */
export const nothing = gap(0); export const nothing = gap(0);
/** A discrete value that repeats once per cycle. /**
* A discrete value that repeats once per cycle.
* *
* @returns {Pattern} * @returns {Pattern}
* @example * @example
@ -1299,7 +1300,8 @@ export function sequenceP(pats) {
return result; return result;
} }
/** The given items are played at the same time at the same length. /**
* The given items are played at the same time at the same length.
* *
* @return {Pattern} * @return {Pattern}
* @synonyms polyrhythm, pr * @synonyms polyrhythm, pr
@ -1382,11 +1384,11 @@ export function stackBy(by, ...pats) {
.setSteps(steps); .setSteps(steps);
} }
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle: /**
* * Concatenation: combines a list of patterns, switching between them successively, one per cycle.
* synonyms: `cat`
* *
* @return {Pattern} * @return {Pattern}
* @synonyms cat
* @example * @example
* slowcat("e5", "b4", ["d5", "c5"]) * slowcat("e5", "b4", ["d5", "c5"])
* *

View file

@ -112,6 +112,13 @@
margin: 0 0 8px 0; margin: 0 0 8px 0;
} }
.autocomplete-info-function-synonyms {
margin: 0 0 12px 0;
color: var(--foreground);
line-height: 1.5;
opacity: 0.8;
}
.autocomplete-info-function-description { .autocomplete-info-function-description {
margin: 0 0 12px 0; margin: 0 0 12px 0;
color: var(--foreground); color: var(--foreground);

View file

@ -2,9 +2,32 @@ import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json'; import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox'; import { Textbox } from '../textbox/Textbox';
const availableFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description) const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
const availableFunctions = (() => {
const seen = new Set(); // avoid repetition
const functions = [];
for (const doc of jsdocJson.docs) {
if (!isValid(doc)) continue;
functions.push(doc);
const synonyms = doc.synonyms || [];
for (const s of synonyms) {
if (!s || seen.has(s)) continue;
seen.add(s);
// Swap `doc.name` in for `s` in the list of synonyms
const synonymsWithDoc = [doc.name, ...synonyms].filter((x) => x && x !== s);
functions.push({
...doc,
name: s, // update names for the synonym
longname: s,
synonyms: synonymsWithDoc,
synonyms_text: synonymsWithDoc.join(', '),
});
}
}
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
})();
const getInnerText = (html) => { const getInnerText = (html) => {
var div = document.createElement('div'); var div = document.createElement('div');