mirror of
https://github.com/kunalja/VariantEditor.git
synced 2026-07-22 13:00:32 +00:00
only show the variant thats selected
This commit is contained in:
parent
70804de7e4
commit
a20d005609
3 changed files with 224 additions and 133 deletions
295
src/main.ts
295
src/main.ts
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
30
styles.css
30
styles.css
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue