only show the variant thats selected

This commit is contained in:
kunalja 2025-06-07 19:08:26 -04:00
parent 70804de7e4
commit a20d005609
3 changed files with 224 additions and 133 deletions

View file

@ -6,30 +6,7 @@ import { TextInputModal } from './modals/TextInputModal';
import { ViewUpdate, PluginValue, EditorView, ViewPlugin, Decoration, DecorationSet, WidgetType } from '@codemirror/view';
import { RangeSetBuilder, StateField, Extension } from '@codemirror/state';
/**
* Widget that displays only the active variant text while preserving the original variant syntax in the document
*/
class ActiveVariantWidget extends WidgetType {
constructor(private activeVariantText: string, private fullVariantText: string) {
super();
}
toDOM() {
const span = document.createElement('span');
span.className = 'variant-indicator';
span.textContent = this.activeVariantText;
// Store the full variant text as a data attribute for debugging
span.setAttribute('data-full-variant', this.fullVariantText);
return span;
}
eq(other: ActiveVariantWidget): boolean {
return other.activeVariantText === this.activeVariantText &&
other.fullVariantText === this.fullVariantText;
}
}
// No longer need the ActiveVariantWidget class as we're using CSS-based approach
export default class VariantEditor extends Plugin {
// Track active line for dimming
@ -78,7 +55,8 @@ export default class VariantEditor extends Plugin {
}
/**
* Widget that displays only the active variant text while preserving the original variant syntax in the document
* Creates an extension that styles variant syntax to show only the active variant
* while keeping the original text editable
*/
private createVariantIndicatorExtension(): Extension {
return ViewPlugin.fromClass(
@ -98,32 +76,69 @@ export default class VariantEditor extends Plugin {
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
// Process all visible ranges
for (let {from, to} of view.visibleRanges) {
const text = view.state.doc.sliceString(from, to);
// Find all variant syntax matches using regex with capture groups for variants and index
const variantRegex = /\{\{([^{}]+?)\}\}\^(\d+)/g;
let match;
while ((match = variantRegex.exec(text)) !== null) {
const start = from + match.index;
const end = start + match[0].length;
const matchStart = from + match.index;
const matchEnd = matchStart + match[0].length;
const fullMatch = match[0];
const variantsText = match[1];
const activeIndex = parseInt(match[2], 10);
// Split the variants and get the active one
const variants = variantsText.split('|').map(v => v.trim());
const activeVariant = variants[activeIndex] || variants[0] || '';
// Replace the entire variant syntax with just the active variant
builder.add(
start,
end,
Decoration.replace({
widget: new ActiveVariantWidget(activeVariant, fullMatch)
})
);
// Find the position of the active variant
let pos = 0;
if (activeIndex === 0) {
pos += 2; // Skip past the opening {{
}
for (let i = 0; i < activeIndex; i++) {
pos = fullMatch.indexOf('|', pos) + 1;
}
const activeVariantStart = matchStart + pos;
const activeVariantEnd = activeVariantStart + variantsText.split('|')[activeIndex].length;
// Add three decorations for each variant:
// 1. Hide everything before the active variant
builder.add(
matchStart,
activeVariantStart,
Decoration.mark({
attributes: {
class: 'variant-syntax-start',
'data-full-variant': fullMatch,
'data-variant-index': activeIndex.toString()
}
})
);
// 2. Show the active variant
builder.add(
activeVariantStart,
activeVariantEnd,
Decoration.mark({
attributes: {
class: 'variant-active-option',
'data-full-variant': fullMatch,
'data-variant-index': activeIndex.toString()
}
})
);
// 3. Hide everything after the active variant
builder.add(
activeVariantEnd,
matchEnd,
Decoration.mark({
attributes: {
class: 'variant-syntax-end',
'data-full-variant': fullMatch,
'data-variant-index': activeIndex.toString()
}
})
);
}
}
@ -197,7 +212,8 @@ export default class VariantEditor extends Plugin {
const builder = new RangeSetBuilder<Decoration>();
const activeLine = pluginInstance.activeLine;
// Add decorations to all lines except the active one
// First pass: Add decorations to all lines except the active one
// This ensures line decorations are added in order
for (let i = 1; i <= view.state.doc.lines; i++) {
if (i !== activeLine) {
try {
@ -209,35 +225,43 @@ export default class VariantEditor extends Plugin {
} catch (e) {
console.error(`Error adding decoration to line ${i}:`, e);
}
} else if (pluginInstance.selectedText) {
// This is the active line, add highlight for the selected text
try {
const line = view.state.doc.line(i);
const lineText = line.text;
const selectedText = pluginInstance.selectedText;
// Find all occurrences of the selected text in this line
let searchIndex = 0;
let foundIndex;
while ((foundIndex = lineText.indexOf(selectedText, searchIndex)) !== -1) {
const start = line.from + foundIndex;
const end = start + selectedText.length;
// Add highlight decoration for the selected text
builder.add(
start,
end,
Decoration.mark({
attributes: { class: "fh-highlight" }
})
);
searchIndex = foundIndex + selectedText.length;
}
} catch (e) {
console.error(`Error adding highlight decoration:`, e);
}
}
// Second pass: Add highlight decorations for the active line if needed
if (pluginInstance.selectedText && activeLine <= view.state.doc.lines) {
try {
const line = view.state.doc.line(activeLine);
const lineText = line.text;
const selectedText = pluginInstance.selectedText;
// Collect all occurrences first and sort them
const highlights = [];
let searchIndex = 0;
let foundIndex;
while ((foundIndex = lineText.indexOf(selectedText, searchIndex)) !== -1) {
const start = line.from + foundIndex;
const end = start + selectedText.length;
highlights.push({ start, end });
searchIndex = foundIndex + selectedText.length;
}
// Sort highlights by start position
highlights.sort((a, b) => a.start - b.start);
// Add them in sorted order
for (const { start, end } of highlights) {
builder.add(
start,
end,
Decoration.mark({
attributes: { class: "fh-highlight" }
})
);
}
} catch (e) {
console.error(`Error adding highlight decoration:`, e);
}
}
@ -258,81 +282,108 @@ export default class VariantEditor extends Plugin {
private highlightSelection(): void {
try {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) {
return;
}
if (!view) return;
const editor = view.editor;
const selection = editor.listSelections()[0];
// Check if selection spans multiple lines
// Variants only work within single lines
if (selection.anchor.line !== selection.head.line) {
new Notice('Selection must be within a single line. Variants only work within single lines.');
new Notice('Selection must be within a single line');
return;
}
const selectedText = editor.getSelection();
const selectedWord = selectedText.trim();
if (!selectedWord) {
return;
}
// Get selection range for replacement
// Get the selected text and range
const from = {
line: selection.anchor.line,
line: Math.min(selection.anchor.line, selection.head.line),
ch: Math.min(selection.anchor.ch, selection.head.ch)
};
const to = {
line: selection.anchor.line,
line: Math.max(selection.anchor.line, selection.head.line),
ch: Math.max(selection.anchor.ch, selection.head.ch)
};
// Store the selected text for highlighting
this.selectedText = selectedWord;
const selectedText = editor.getRange(from, to);
if (!selectedText) return;
// Set active line for dimming
this.activeLine = from.line + 1;
this.selectedText = selectedText;
// Clear the selection to prevent the selection overlay from washing out our highlight
editor.setCursor(from);
// Setup for variant editing
let initialText = selectedText;
let initialActiveIndex = 0;
let isExistingVariant = false;
// Force editor refresh to apply decorations
this.app.workspace.updateOptions();
// Check if the selection is already a variant
const variantRegex = /^\{\{([^{}]+?)\}\}\^(\d+)$/;
const variantMatch = selectedText.match(variantRegex);
// Open the text input modal after highlighting with the original text
if (variantMatch) {
// Direct selection of a variant
initialText = variantMatch[1];
initialActiveIndex = parseInt(variantMatch[2], 10);
isExistingVariant = true;
} else {
// Check if we're clicking inside a variant
const line = editor.getLine(from.line);
if (line) {
const fullLineRegex = /\{\{([^{}]+?)\}\}\^(\d+)/g;
let match;
while ((match = fullLineRegex.exec(line)) !== null) {
const matchStart = match.index;
const matchEnd = matchStart + match[0].length;
if (from.ch >= matchStart && to.ch <= matchEnd) {
// Selection is inside a variant
initialText = match[1];
initialActiveIndex = parseInt(match[2], 10);
isExistingVariant = true;
// Expand selection to cover the entire variant
from.ch = matchStart;
to.ch = matchEnd;
editor.setSelection(from, to);
break;
}
}
}
}
if (isExistingVariant) {
new Notice('Editing existing variant');
}
// Open the variant editor modal
new TextInputModal(
this.app,
selectedWord,
(variantText: string, activeIndex?: number, commitVariant?: boolean) => {
if (variantText && variantText.trim()) {
if (commitVariant) {
// If committing, just replace with the selected variant text directly
this.app,
initialText,
(variantText, activeIndex, commitVariant) => {
if (commitVariant) {
// Replace the variant with just the active variant text
if (variantText) {
editor.replaceRange(variantText, from, to);
new Notice(`Committed variant: "${variantText}"`);
} else {
// Create the variant syntax: {{original | variant1 | variant2}}^n
const variants = variantText.split('|').map(v => v.trim()).filter(v => v);
// Ensure we have at least one variant (the original text)
if (variants.length > 0) {
// Use the provided activeIndex or default to 0
const activeIdx = typeof activeIndex === 'number' ? activeIndex : 0;
const variantSyntax = `{{${variants.join(' | ')}}}^${activeIdx}`;
// Replace the selected text with the variant syntax
editor.replaceRange(variantSyntax, from, to);
new Notice(`Created variant with ${variants.length} options (${variants[activeIdx]} active)`);
}
}
} else {
// Create or update the variant syntax
const variants = variantText.split('|').filter(v => v);
// Clear highlighting after creating variant
this.clearHighlight();
if (variants.length > 0) {
const activeIdx = typeof activeIndex === 'number' ? activeIndex : 0;
const variantSyntax = `{{${variants.join('|')}}^${activeIdx}`;
editor.replaceRange(variantSyntax, from, to);
const action = isExistingVariant ? 'Updated' : 'Created';
new Notice(`${action} variant with ${variants.length} options (${variants[activeIdx]} active)`);
}
}
this.clearHighlight();
},
// Pass the cursor position to position the modal relative to the highlighted text
from
from,
initialActiveIndex
).open();
} catch (e) {
@ -342,16 +393,14 @@ export default class VariantEditor extends Plugin {
private clearHighlight(): void {
try {
// Reset active line, previous cursor line, and selected text
// Reset state
this.activeLine = null;
this.previousCursorLine = null;
this.selectedText = null;
// Use the hack to force a complete re-render
// Force editor refresh
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
hackToRerender(view);
}
if (view) hackToRerender(view);
} catch (e) {
console.error('Error in clearHighlight:', e);
}

View file

@ -16,12 +16,22 @@ export class TextInputModal extends Modal {
app: App,
originalText: string,
onSubmit: (result: string, activeIndex?: number, commitVariant?: boolean) => void,
cursorPosition: EditorPosition | null = null
cursorPosition: EditorPosition | null = null,
initialActiveIndex: number = 0
) {
super(app);
this.originalText = originalText;
this.onSubmit = onSubmit;
this.variants = [originalText];
// If the originalText contains pipe characters, it might be a variant list
if (originalText.includes('|')) {
this.variants = originalText.split('|').filter(v => v);
this.activeVariantIndex = initialActiveIndex;
} else {
this.variants = [originalText];
this.activeVariantIndex = 0;
}
this.cursorPosition = cursorPosition;
}
@ -44,11 +54,13 @@ export class TextInputModal extends Modal {
}, 0);
}
flexContainer.createEl('h2', {text: 'Create Variants'});
// Change title based on whether we're editing an existing variant or creating a new one
const isEditing = this.variants.length > 1;
flexContainer.createEl('h2', {text: isEditing ? 'Edit Variants' : 'Create Variants'});
// Show the original text
// Show the original text or selected variant
flexContainer.createEl('div', {
text: `Original text: "${this.originalText}"`,
text: isEditing ? `Selected variant: "${this.variants[this.activeVariantIndex]}"` : `Original text: "${this.originalText}"`,
cls: 'variant-editor-original-text'
});
@ -98,7 +110,7 @@ export class TextInputModal extends Modal {
.onClick(() => {
// Filter out empty variants and track their original indices
const nonEmptyVariantsWithIndices = this.variants
.map((v, i) => ({ text: v.trim(), originalIndex: i }))
.map((v, i) => ({ text: v, originalIndex: i }))
.filter(v => v.text.length > 0);
// Find the active variant after filtering
@ -112,14 +124,14 @@ export class TextInputModal extends Modal {
})
.buttonEl.addClass('variant-editor-button', 'variant-editor-commit-button');
// Create variants button
// Create/Update variants button
new ButtonComponent(buttonsContainer)
.setButtonText('Create Variants')
.setButtonText(isEditing ? 'Update Variants' : 'Create Variants')
.setCta()
.onClick(() => {
// Filter out empty variants and track their original indices
const nonEmptyVariantsWithIndices = this.variants
.map((v, i) => ({ text: v.trim(), originalIndex: i }))
.map((v, i) => ({ text: v, originalIndex: i }))
.filter(v => v.text.length > 0);
if (nonEmptyVariantsWithIndices.length >= 2) {
@ -252,7 +264,7 @@ export class TextInputModal extends Modal {
// Update the variant when the input changes
variantInput.addEventListener('input', (e) => {
this.variants[index] = (e.target as HTMLInputElement).value;
this.variants[index] = (e.target as HTMLInputElement).value.trim();
});
// Don't allow deleting the original variant

View file

@ -49,6 +49,36 @@
border-bottom: 1px dashed #79b8ff;
border-radius: 2px;
position: relative;
}
/* Hide variant syntax parts */
.variant-syntax-start,
.variant-syntax-end,
.variant-inactive-option {
display: inline;
color: transparent;
font-size: 0;
position: relative;
pointer-events: auto;
cursor: text;
}
/* Show a small indicator for hidden parts when hovered */
.variant-syntax-start:hover,
.variant-syntax-end:hover,
.variant-inactive-option:hover {
background-color: rgba(121, 184, 255, 0.3);
}
/* Active variant option */
.variant-active-option {
display: inline;
background-color: rgba(121, 184, 255, 0.2);
border-bottom: 1px dashed #79b8ff;
border-radius: 2px;
position: relative;
pointer-events: auto;
cursor: text;
z-index: 5;
}