Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -460,21 +460,12 @@ class EnrichedMarkdownTextInputView(
fun toggleInlineStyle(styleType: StyleType) {
val handler = formatter.handlers[styleType] ?: return
val mergingConfig = handler.mergingConfig

val selStart = selectionStart
val selEnd = selectionEnd

// Check blocking rules: if any blocking style is active, refuse to toggle on.
if (mergingConfig.blockingStyles.isNotEmpty()) {
val isCurrentlyActive = formattingStore.isStyleActive(styleType, selStart)
if (!isCurrentlyActive) {
for (blocker in mergingConfig.blockingStyles) {
if (formattingStore.isStyleActive(blocker, selStart)) {
return
}
}
}
}
if (formattingStore.isToggleBlocked(styleType, selStart, mergingConfig.blockingStyles)) return

val result = formattingStore.toggleStyle(styleType, selStart, selEnd, mergingConfig.conflictingStyles)

if (selStart == selEnd) {
if (pendingStyleRemovals.contains(styleType)) {
Expand All @@ -483,23 +474,13 @@ class EnrichedMarkdownTextInputView(
} else if (pendingStyles.contains(styleType)) {
pendingStyles.remove(styleType)
pendingStyleRemovals.add(styleType)
} else if (formattingStore.isStyleActive(styleType, selStart)) {
} else if (result == FormattingStore.ToggleResult.WAS_ACTIVE) {
pendingStyleRemovals.add(styleType)
} else {
pendingStyles.add(styleType)
}
eventEmitter.emitState()
} else {
val isActive = formattingStore.isStyleActive(styleType, selStart)
if (isActive) {
formattingStore.removeType(styleType, selStart, selEnd)
} else {
// Remove conflicting styles from the range before applying.
for (conflict in mergingConfig.conflictingStyles) {
formattingStore.removeType(conflict, selStart, selEnd)
}
formattingStore.addRange(FormattingRange(styleType, selStart, selEnd))
}
applyFormattingAndEmit()
}
}
Expand Down Expand Up @@ -1066,15 +1047,7 @@ class EnrichedMarkdownTextInputView(
) {
if (start >= end) return
val handler = formatter.handlers[styleType] ?: return
val isActive = formattingStore.isStyleActive(styleType, start)
if (isActive) {
formattingStore.removeType(styleType, start, end)
} else {
for (conflict in handler.mergingConfig.conflictingStyles) {
formattingStore.removeType(conflict, start, end)
}
formattingStore.addRange(FormattingRange(styleType, start, end))
}
formattingStore.toggleStyle(styleType, start, end, handler.mergingConfig.conflictingStyles)
applyFormattingAndEmit()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,55 @@ class FormattingStore {
return if (link != null && start > link.start && start < link.end) Pair(link.end, link.end) else null
}

enum class ToggleResult {
/** The style was active and has been removed (or is active at the caret). */
WAS_ACTIVE,

/** The style was inactive and has been added (or is inactive at the caret). */
WAS_INACTIVE,
}

/**
* Toggles [type] on the range `[start, end)`. For a selection (`start < end`),
* removes the style when active at `start`, otherwise strips [conflictingStyles]
* and adds it. For a caret (`start == end`), reports the current state without
* mutating — the caller manages pending-style bookkeeping.
*/
fun toggleStyle(
type: StyleType,
start: Int,
end: Int,
conflictingStyles: Set<StyleType>,
): ToggleResult {
val isActive = isStyleActive(type, start)
if (start < end) {
if (isActive) {
removeType(type, start, end)
} else {
for (conflict in conflictingStyles) {
removeType(conflict, start, end)
}
addRange(FormattingRange(type, start, end))
}
}
return if (isActive) ToggleResult.WAS_ACTIVE else ToggleResult.WAS_INACTIVE
}

/**
* Returns true when toggling [type] ON at [position] should be refused
* because one of the [blockingStyles] is currently active there.
* Toggling OFF (the style is already active) is never blocked.
*/
fun isToggleBlocked(
type: StyleType,
position: Int,
blockingStyles: Set<StyleType>,
): Boolean {
if (blockingStyles.isEmpty()) return false
if (isStyleActive(type, position)) return false
return blockingStyles.any { isStyleActive(it, position) }
}

fun addRange(newRange: FormattingRange) {
var mergedStart = newRange.start
var mergedEnd = newRange.end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ NS_ASSUME_NONNULL_BEGIN
/// the whole link, a caret inside a link moves to its end (unchanged when no adjustment is needed).
- (NSRange)selectionAdjustedForAtomicLinks:(NSRange)selection;

/// YES when the entire `range` is covered by adjacent/overlapping ranges of `type`.
- (BOOL)isStyleFullyActive:(ENRMInputStyleType)type inRange:(NSRange)range;

/// Toggles `type` on `range`. For a selection (length > 0), removes the style
/// when fully active, otherwise strips `conflictingStyles` and adds it.
/// For a caret (length == 0), reports the current state without mutating.
/// Returns YES when the style was active (removed / pending removal).
- (BOOL)toggleStyle:(ENRMInputStyleType)type
inRange:(NSRange)range
conflictingStyles:(NSSet<NSNumber *> *)conflictingStyles;

/// YES when toggling `type` ON at `position` should be refused because one of
/// the `blockingStyles` is active there. Toggling OFF is never blocked.
- (BOOL)isToggleBlocked:(ENRMInputStyleType)type
atPosition:(NSUInteger)position
blockingStyles:(NSSet<NSNumber *> *)blockingStyles;

- (void)addRange:(ENRMFormattingRange *)range;
- (void)removeType:(ENRMInputStyleType)type inRange:(NSRange)range;
- (void)removeRange:(ENRMFormattingRange *)range;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,59 @@ - (BOOL)isStyleAdjacentBefore:(ENRMInputStyleType)type position:(NSUInteger)posi
containingPosition:position - 1] != nil;
}

- (BOOL)isStyleFullyActive:(ENRMInputStyleType)type inRange:(NSRange)range
{
NSUInteger pos = range.location;
NSUInteger end = NSMaxRange(range);
while (pos < end) {
ENRMFormattingRange *match = [self rangeOfType:type containingPosition:pos];
if (match == nil) {
return NO;
}
pos = NSMaxRange(match.range);
}
return YES;
}

- (BOOL)toggleStyle:(ENRMInputStyleType)type
inRange:(NSRange)range
conflictingStyles:(NSSet<NSNumber *> *)conflictingStyles
{
BOOL wasActive;
if (range.length > 0) {
wasActive = [self isStyleFullyActive:type inRange:range];
if (wasActive) {
[self removeType:type inRange:range];
} else {
for (NSNumber *conflict in conflictingStyles) {
[self removeType:(ENRMInputStyleType)conflict.integerValue inRange:range];
}
[self addRange:[ENRMFormattingRange rangeWithType:type range:range]];
}
} else {
wasActive = [self isStyleActive:type atPosition:range.location];
}
return wasActive;
}

- (BOOL)isToggleBlocked:(ENRMInputStyleType)type
atPosition:(NSUInteger)position
blockingStyles:(NSSet<NSNumber *> *)blockingStyles
{
if (blockingStyles.count == 0) {
return NO;
}
if ([self isStyleActive:type atPosition:position]) {
return NO;
}
for (NSNumber *blocker in blockingStyles) {
if ([self isStyleActive:(ENRMInputStyleType)blocker.integerValue atPosition:position]) {
return YES;
}
}
return NO;
}

- (void)addRange:(ENRMFormattingRange *)newRange
{
NSMutableIndexSet *mergeIndexes = [NSMutableIndexSet indexSet];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,53 +872,27 @@ - (void)toggleInlineStyle:(ENRMInputStyleType)styleType
ENRMStyleMergingConfig *mergingConfig = handler.mergingConfig;

NSRange selection = _textView.selectedRange;
NSUInteger cursor = selection.location;
NSNumber *key = @(styleType);

// Check blocking rules: if any blocking style is active, refuse to toggle on.
if (mergingConfig.blockingStyles.count > 0) {
BOOL isCurrentlyActive = [_formattingStore isStyleActive:styleType atPosition:cursor];
if (!isCurrentlyActive) {
for (NSNumber *blockerNum in mergingConfig.blockingStyles) {
if ([_formattingStore isStyleActive:(ENRMInputStyleType)blockerNum.integerValue atPosition:cursor]) {
return;
}
}
}
if ([_formattingStore isToggleBlocked:styleType
atPosition:selection.location
blockingStyles:mergingConfig.blockingStyles]) {
return;
}

BOOL wasActive = [_formattingStore toggleStyle:styleType
inRange:selection
conflictingStyles:mergingConfig.conflictingStyles];

if (selection.length > 0) {
BOOL fullyStyled = YES;
NSUInteger pos = selection.location;
NSUInteger selEnd = NSMaxRange(selection);
while (pos < selEnd) {
ENRMFormattingRange *match = [_formattingStore rangeOfType:styleType containingPosition:pos];
if (match == nil) {
fullyStyled = NO;
break;
}
pos = NSMaxRange(match.range);
}
if (fullyStyled) {
[_formattingStore removeType:styleType inRange:selection];
} else {
// Remove conflicting styles from the range before applying.
for (NSNumber *conflictNum in mergingConfig.conflictingStyles) {
[_formattingStore removeType:(ENRMInputStyleType)conflictNum.integerValue inRange:selection];
}
ENRMFormattingRange *newRange = [ENRMFormattingRange rangeWithType:styleType range:selection];
[_formattingStore addRange:newRange];
}
[_pendingStyles removeObject:key];
[_pendingStyleRemovals removeObject:key];
} else {
BOOL isInsideRange = [_formattingStore isStyleActive:styleType atPosition:cursor];

if ([_pendingStyleRemovals containsObject:key]) {
[_pendingStyleRemovals removeObject:key];
} else if ([_pendingStyles containsObject:key]) {
[_pendingStyles removeObject:key];
} else if (isInsideRange) {
} else if (wasActive) {
[_pendingStyleRemovals addObject:key];
} else {
[_pendingStyles addObject:key];
Expand Down
Loading