Skip to content

Commit ce9ad67

Browse files
rkpatel33claude
andcommitted
feat: smart trailing space in context-aware capitalization
- Add trailing space after sentence-ending punctuation (. ! ?) - Add trailing space in fallback mode (terminal apps) assuming consecutive sentences - Track context_readable flag to detect when accessibility APIs fail - Clean up unused get_capitalization_hint function - Fix test expectation for mixed_content test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 1f0db37 commit ce9ad67

File tree

1 file changed

+70
-54
lines changed

1 file changed

+70
-54
lines changed

src-tauri/src/context.rs

Lines changed: 70 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
//!
33
//! This module reads the character before the cursor in the currently focused
44
//! text field to determine appropriate capitalization for inserted text.
5+
//!
6+
//! Features:
7+
//! - Capitalizes after sentence-ending punctuation (. ! ?)
8+
//! - Lowercases after continuation punctuation (, ; : -)
9+
//! - Adds trailing space after sentence-ending punctuation in output
10+
//! - When context cannot be read (terminal apps), assumes consecutive sentences
511
612
use log::{debug, info};
713

@@ -201,45 +207,53 @@ fn find_relevant_punctuation(text: &str) -> Option<char> {
201207
None
202208
}
203209

204-
/// Result of context analysis including both capitalization and spacing.
210+
/// Result of context analysis.
205211
#[derive(Debug, Clone)]
206-
pub struct ContextResult {
212+
struct ContextResult {
207213
/// Whether to capitalize, lowercase, or leave unchanged
208-
pub hint: CapitalizationHint,
209-
/// Whether to prepend a space before the text
210-
pub needs_leading_space: bool,
214+
hint: CapitalizationHint,
215+
/// Whether context was successfully read (false = fallback mode)
216+
context_readable: bool,
211217
}
212218

213-
/// Analyze the context and return capitalization hint plus spacing info.
219+
/// Analyze the context and return capitalization hint plus context readability.
214220
///
215221
/// This function:
216222
/// 1. Reads text before the cursor
217223
/// 2. Scans back through whitespace (up to MAX_WHITESPACE_LOOKBACK chars)
218224
/// 3. Determines capitalization based on the punctuation found
219-
/// 4. Determines if a leading space is needed
225+
/// 4. Reports whether context was successfully read
220226
///
221227
/// ## Scenarios handled:
222-
/// - `"Hello."` → Capitalize, add leading space
223-
/// - `"Hello. "` → Capitalize, no leading space needed
224-
/// - `"Hey,"` → Lowercase, add leading space
225-
/// - `"Hey, "` → Lowercase, no leading space needed
226-
/// - `"Hello"` → Unknown (mid-word), no change
227-
/// - Empty/beginning → Capitalize (start of text)
228+
/// - `"Hello."` → Capitalize (context readable)
229+
/// - `"Hello. "` → Capitalize (context readable)
230+
/// - `"Hey,"` → Lowercase (context readable)
231+
/// - `"Hey, "` → Lowercase (context readable)
232+
/// - `"Hello"` → Unknown/mid-word (context readable)
233+
/// - Empty/beginning → Capitalize (context readable)
234+
/// - Terminal/API failure → Capitalize (context NOT readable - fallback mode)
228235
fn analyze_context() -> ContextResult {
229236
let text = match get_text_before_cursor() {
230237
Some(t) => t,
231238
None => {
232-
// Empty field, can't read, or at beginning - capitalize by default
233-
debug!("No text before cursor, defaulting to Capitalize");
239+
// Can't read context (unsupported app like terminal, or API failure)
240+
// In fallback mode, assume consecutive sentences
241+
debug!("No text before cursor (fallback mode), defaulting to Capitalize");
234242
return ContextResult {
235243
hint: CapitalizationHint::Capitalize,
236-
needs_leading_space: false,
244+
context_readable: false,
237245
};
238246
}
239247
};
240248

241-
// Check if there's already a space at the end
242-
let has_trailing_space = text.ends_with(' ') || text.ends_with('\t');
249+
// If we got an empty string, treat as start of text (but context IS readable)
250+
if text.is_empty() {
251+
debug!("Empty text field, Capitalize (context readable)");
252+
return ContextResult {
253+
hint: CapitalizationHint::Capitalize,
254+
context_readable: true,
255+
};
256+
}
243257

244258
// Find the relevant punctuation by looking back through whitespace
245259
let relevant_char = find_relevant_punctuation(&text);
@@ -250,57 +264,41 @@ fn analyze_context() -> ContextResult {
250264
debug!("No relevant punctuation found, defaulting to Capitalize");
251265
ContextResult {
252266
hint: CapitalizationHint::Capitalize,
253-
needs_leading_space: false,
267+
context_readable: true,
254268
}
255269
}
256270
Some(c) if CAPITALIZE_AFTER.contains(&c) => {
257-
debug!(
258-
"Found sentence-ending '{}', Capitalize, needs_space={}",
259-
c, !has_trailing_space
260-
);
271+
debug!("Found sentence-ending '{}', Capitalize", c);
261272
ContextResult {
262273
hint: CapitalizationHint::Capitalize,
263-
needs_leading_space: !has_trailing_space,
274+
context_readable: true,
264275
}
265276
}
266277
Some(c) if c == '\n' || c == '\r' => {
267-
// Newline - capitalize, no space needed (newline acts as separator)
268-
debug!("Found newline, Capitalize, no space needed");
278+
debug!("Found newline, Capitalize");
269279
ContextResult {
270280
hint: CapitalizationHint::Capitalize,
271-
needs_leading_space: false,
281+
context_readable: true,
272282
}
273283
}
274284
Some(c) if LOWERCASE_AFTER.contains(&c) => {
275-
debug!(
276-
"Found continuation '{}', Lowercase, needs_space={}",
277-
c, !has_trailing_space
278-
);
285+
debug!("Found continuation '{}', Lowercase", c);
279286
ContextResult {
280287
hint: CapitalizationHint::Lowercase,
281-
needs_leading_space: !has_trailing_space,
288+
context_readable: true,
282289
}
283290
}
284291
Some(c) => {
285292
// Some other character (letter, number, etc.)
286-
// Don't change capitalization, but might need space
287-
debug!(
288-
"Found other char '{}', Unknown, needs_space={}",
289-
c, !has_trailing_space
290-
);
293+
debug!("Found other char '{}', Unknown", c);
291294
ContextResult {
292295
hint: CapitalizationHint::Unknown,
293-
needs_leading_space: !has_trailing_space,
296+
context_readable: true,
294297
}
295298
}
296299
}
297300
}
298301

299-
/// Legacy function for getting just the hint (used by tests).
300-
pub fn get_capitalization_hint() -> CapitalizationHint {
301-
analyze_context().hint
302-
}
303-
304302
/// Apply capitalization hint to the given text.
305303
///
306304
/// - If hint is `Capitalize`, ensures first alphabetic char is uppercase
@@ -345,31 +343,42 @@ pub fn apply_capitalization(text: &str, hint: CapitalizationHint) -> String {
345343
/// Convenience function that reads context and applies capitalization in one step.
346344
///
347345
/// This is the main entry point for context-aware capitalization.
348-
/// On non-macOS platforms or if context cannot be read, returns text unchanged.
346+
/// On non-macOS platforms or if context cannot be read, uses fallback behavior.
349347
///
350348
/// This function:
351349
/// 1. Analyzes the text before the cursor
352350
/// 2. Determines appropriate capitalization (capitalize/lowercase/unchanged)
353-
/// 3. Adds a leading space if needed (no space after punctuation)
351+
/// 3. Adds a trailing space based on smart logic:
352+
/// - If output ends with sentence-ending punctuation (. ! ?), add space
353+
/// - If context was NOT readable (terminal apps, etc.), add space (assume consecutive sentences)
354354
///
355355
/// ## Examples:
356-
/// - After `"Hello."` with input `"world"` → `" World"` (space + capitalize)
357-
/// - After `"Hello. "` with input `"world"` → `"World"` (capitalize, space exists)
358-
/// - After `"Hey,"` with input `"What"` → `" what"` (space + lowercase)
359-
/// - After `"Hey, "` with input `"What"` → `"what"` (lowercase, space exists)
356+
/// - After `"Hello."` with input `"world"` → `"World"` (capitalize, context readable)
357+
/// - After `"Hey,"` with input `"What"` → `"what"` (lowercase, context readable)
358+
/// - Input `"Hello world."` → `"Hello world. "` (trailing space for punctuation)
359+
/// - In terminal with input `"hello"` → `"Hello "` (trailing space for fallback mode)
360360
pub fn apply_context_aware_capitalization(text: &str) -> String {
361361
let context = analyze_context();
362362
let capitalized = apply_capitalization(text, context.hint);
363363

364-
let result = if context.needs_leading_space {
365-
format!(" {}", capitalized)
364+
// Determine if we should add a trailing space:
365+
// 1. Always add space after sentence-ending punctuation
366+
// 2. Add space in fallback mode (context not readable) - assume consecutive sentences
367+
let ends_with_sentence_punctuation = capitalized.ends_with('.')
368+
|| capitalized.ends_with('!')
369+
|| capitalized.ends_with('?');
370+
371+
let should_add_trailing_space = ends_with_sentence_punctuation || !context.context_readable;
372+
373+
let result = if should_add_trailing_space {
374+
format!("{} ", capitalized)
366375
} else {
367376
capitalized
368377
};
369378

370379
info!(
371-
"Context-aware capitalization: hint={:?}, needs_space={}, input='{}', output='{}'",
372-
context.hint, context.needs_leading_space, text, result
380+
"Context-aware capitalization: hint={:?}, context_readable={}, input='{}', output='{}'",
381+
context.hint, context.context_readable, text, result
373382
);
374383
result
375384
}
@@ -485,9 +494,16 @@ mod tests {
485494
apply_capitalization("42 - hello", CapitalizationHint::Capitalize),
486495
"42 - Hello"
487496
);
497+
// Lowercase hint only affects the first alphabetic char
498+
// Here 'n' in 'note' is already lowercase, so no change
488499
assert_eq!(
489500
apply_capitalization("(note: Hello)", CapitalizationHint::Lowercase),
490-
"(note: hello)"
501+
"(note: Hello)"
502+
);
503+
// Test with first letter being uppercase
504+
assert_eq!(
505+
apply_capitalization("(Note: Hello)", CapitalizationHint::Lowercase),
506+
"(note: Hello)"
491507
);
492508
}
493509

0 commit comments

Comments
 (0)