CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-jupyterlab--codemirror

CodeMirror 6 editor provider for JupyterLab with comprehensive language support, themes, extensions, and collaborative editing capabilities

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

search-replace.mddocs/

Search and Replace

Advanced search provider with regex support, match highlighting, and comprehensive find/replace functionality for editors.

Capabilities

EditorSearchProvider

Abstract base class for implementing search functionality in editors.

/**
 * Search provider for editors
 * Abstract base class providing search and replace functionality
 */
abstract class EditorSearchProvider<T extends CodeEditor.IModel = CodeEditor.IModel> 
  implements IBaseSearchProvider {
  
  constructor();
  
  /**
   * Current match index (null if no matches)
   */
  readonly currentMatchIndex: number | null;
  
  /**
   * Whether the search is currently active
   */
  readonly isActive: boolean;
  
  /**
   * Whether the search provider is disposed
   */
  readonly isDisposed: boolean;
  
  /**
   * Number of matches found
   */
  readonly matchesCount: number;
  
  /**
   * Signal emitted when search state changes
   */
  readonly stateChanged: ISignal<IBaseSearchProvider, void>;
  
  // Abstract properties (must be implemented by subclasses)
  protected abstract readonly editor: CodeEditor.IEditor | null;
  protected abstract readonly model: T;
  
  // Lifecycle
  dispose(): void;
  
  // Search state management
  setIsActive(active: boolean): Promise<void>;
  setSearchSelection(selection: CodeEditor.IRange | null): Promise<void>;
  setProtectSelection(protect: boolean): void;
  
  // Query management
  startQuery(query: RegExp | null, filters?: IFilters): Promise<void>;
  endQuery(): Promise<void>;
  
  // Match highlighting and navigation
  clearHighlight(): Promise<void>;
  highlightNext(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
  highlightPrevious(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
  getCurrentMatch(): ISearchMatch | undefined;
  
  // Text replacement
  replaceCurrentMatch(newText: string, loop?: boolean, options?: IReplaceOptions): Promise<boolean>;
  replaceAllMatches(newText: string, options?: IReplaceOptions): Promise<boolean>;
}

Usage Examples:

import { EditorSearchProvider } from "@jupyterlab/codemirror";
import { CodeEditor } from "@jupyterlab/codeeditor";

// Implement search provider for specific editor
class MyEditorSearchProvider extends EditorSearchProvider<CodeEditor.IModel> {
  constructor(private _editor: CodeEditor.IEditor, private _model: CodeEditor.IModel) {
    super();
  }
  
  protected get editor(): CodeEditor.IEditor | null {
    return this._editor;
  }
  
  protected get model(): CodeEditor.IModel {
    return this._model;
  }
}

// Use search provider
const searchProvider = new MyEditorSearchProvider(editor, model);

// Start search query
await searchProvider.startQuery(/function\s+\w+/g);

// Navigate through matches
const nextMatch = await searchProvider.highlightNext();
const prevMatch = await searchProvider.highlightPrevious();

// Replace matches
await searchProvider.replaceCurrentMatch('const functionName =');
await searchProvider.replaceAllMatches('const replacement =');

// Listen for state changes
searchProvider.stateChanged.connect(() => {
  console.log(`Found ${searchProvider.matchesCount} matches`);
  console.log(`Current match: ${searchProvider.currentMatchIndex}`);
});

CodeMirrorSearchHighlighter

Helper class for highlighting search matches in CodeMirror editors.

/**
 * Helper class to highlight texts in a CodeMirror editor
 * Manages match decorations and navigation
 */
class CodeMirrorSearchHighlighter {
  constructor(editor: CodeMirrorEditor | null);
  
  /**
   * Current index of the selected match (null if no selection)
   */
  readonly currentIndex: number | null;
  
  /**
   * List of search matches
   */
  matches: ISearchMatch[];
  
  /**
   * Whether cursor/selection should not be modified
   */
  protectSelection: boolean;
  
  // Highlighting management
  clearHighlight(): void;
  endQuery(): Promise<void>;
  
  // Match navigation
  highlightNext(options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
  highlightPrevious(options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
  
  // Editor management
  setEditor(editor: CodeMirrorEditor): void;
}

Usage Examples:

import { CodeMirrorSearchHighlighter, CodeMirrorEditor } from "@jupyterlab/codemirror";

// Create highlighter for editor
const highlighter = new CodeMirrorSearchHighlighter(editor as CodeMirrorEditor);

// Set matches to highlight
highlighter.matches = [
  { text: 'function', start: { line: 0, column: 0 }, end: { line: 0, column: 8 } },
  { text: 'function', start: { line: 5, column: 4 }, end: { line: 5, column: 12 } },
  { text: 'function', start: { line: 10, column: 0 }, end: { line: 10, column: 8 } }
];

// Navigate through matches
await highlighter.highlightNext(); // Highlights first match
await highlighter.highlightNext(); // Highlights second match
await highlighter.highlightPrevious(); // Back to first match

// Protect selection from modification
highlighter.protectSelection = true;

// Clear all highlights
highlighter.clearHighlight();

Search Options and Configuration

Types and interfaces for configuring search behavior.

/**
 * Search start anchor defines from which position search should be executed
 */
type SearchStartAnchor = 'auto' | 'selection' | 'selection-start' | 'previous-match' | 'start';

/**
 * Options for highlighting matches
 */
interface IHighlightMatchOptions {
  /**
   * Whether the highlighted match should be scrolled into view (default: true)
   */
  scroll?: boolean;
  
  /**
   * Whether the user cursor should be moved to select the match (default: true)
   */
  select?: boolean;
}

/**
 * Options for highlighting adjacent matches
 */
interface IHighlightAdjacentMatchOptions extends IHighlightMatchOptions {
  /**
   * What should be used as anchor when searching for adjacent match (default: 'auto')
   */
  from?: SearchStartAnchor;
}

/**
 * Options for replace operations
 */
interface IReplaceOptions {
  /**
   * Whether to preserve case when replacing
   */
  preserveCase?: boolean;
  
  /**
   * Whether to use regex replacement patterns
   */
  useRegex?: boolean;
}

/**
 * Search filter options
 */
interface IFilters {
  /**
   * Case sensitive search
   */
  caseSensitive?: boolean;
  
  /**
   * Whole word matching
   */
  wholeWord?: boolean;
  
  /**
   * Use regular expressions
   */
  regex?: boolean;
}

Advanced Search Features

Complex search scenarios and advanced usage patterns.

// Case-sensitive regex search with word boundaries
const searchQuery = /\bclass\s+\w+\b/g;
const filters: IFilters = {
  caseSensitive: true,
  regex: true,
  wholeWord: false
};

await searchProvider.startQuery(searchQuery, filters);

// Search within selection
const selection = {
  start: { line: 10, column: 0 },
  end: { line: 20, column: 0 }
};
await searchProvider.setSearchSelection(selection);

// Navigate with custom options
const nextMatch = await searchProvider.highlightNext(true, {
  from: 'selection-start',
  scroll: true,
  select: false  // Don't modify cursor position
});

// Replace with regex capture groups
await searchProvider.replaceCurrentMatch('interface $1', false, {
  useRegex: true,
  preserveCase: false
});

// Batch replace all matches
const replaceCount = await searchProvider.replaceAllMatches('interface $1', {
  useRegex: true
});
console.log(`Replaced ${replaceCount} matches`);

Custom Search Implementation

Implementing a custom search provider with additional features.

class AdvancedSearchProvider extends EditorSearchProvider {
  private _searchHistory: string[] = [];
  private _replaceHistory: string[] = [];
  
  constructor(
    private _editor: CodeMirrorEditor,
    private _model: CodeEditor.IModel
  ) {
    super();
  }
  
  protected get editor(): CodeMirrorEditor {
    return this._editor;
  }
  
  protected get model(): CodeEditor.IModel {
    return this._model;
  }
  
  // Enhanced search with history
  async startQueryWithHistory(query: RegExp | null, filters?: IFilters): Promise<void> {
    if (query) {
      const queryString = query.source;
      this._searchHistory.unshift(queryString);
      this._searchHistory = this._searchHistory.slice(0, 10); // Keep last 10
    }
    
    return this.startQuery(query, filters);
  }
  
  // Enhanced replace with history and validation
  async replaceCurrentMatchWithValidation(
    newText: string, 
    validator?: (text: string) => boolean
  ): Promise<boolean> {
    const currentMatch = this.getCurrentMatch();
    if (!currentMatch) return false;
    
    // Validate replacement text
    if (validator && !validator(newText)) {
      throw new Error('Invalid replacement text');
    }
    
    // Add to history
    this._replaceHistory.unshift(newText);
    this._replaceHistory = this._replaceHistory.slice(0, 10);
    
    return this.replaceCurrentMatch(newText);
  }
  
  // Search history accessors
  getSearchHistory(): string[] {
    return [...this._searchHistory];
  }
  
  getReplaceHistory(): string[] {
    return [...this._replaceHistory];
  }
  
  // Search with preview
  async previewReplace(newText: string): Promise<Array<{ match: ISearchMatch; preview: string }>> {
    const matches = this.getAllMatches();
    return matches.map(match => ({
      match,
      preview: this.generatePreview(match, newText)
    }));
  }
  
  private generatePreview(match: ISearchMatch, replacement: string): string {
    // Generate preview text showing before/after replacement
    const beforeText = match.text;
    const afterText = replacement;
    return `${beforeText} → ${afterText}`;
  }
  
  private getAllMatches(): ISearchMatch[] {
    // Implementation to get all current matches
    return [];
  }
}

Search UI Integration

Integrating search functionality with user interface components.

class SearchWidget {
  private searchProvider: EditorSearchProvider;
  private searchInput: HTMLInputElement;
  private replaceInput: HTMLInputElement;
  private matchCounter: HTMLElement;
  
  constructor(searchProvider: EditorSearchProvider) {
    this.searchProvider = searchProvider;
    this.createUI();
    this.connectSignals();
  }
  
  private createUI(): void {
    // Create search input
    this.searchInput = document.createElement('input');
    this.searchInput.type = 'text';
    this.searchInput.placeholder = 'Search...';
    
    // Create replace input
    this.replaceInput = document.createElement('input');
    this.replaceInput.type = 'text';
    this.replaceInput.placeholder = 'Replace...';
    
    // Create match counter
    this.matchCounter = document.createElement('span');
    this.matchCounter.className = 'search-match-counter';
    
    // Add event listeners
    this.searchInput.addEventListener('input', () => this.onSearchInput());
    this.searchInput.addEventListener('keydown', (e) => this.onSearchKeydown(e));
  }
  
  private connectSignals(): void {
    this.searchProvider.stateChanged.connect(() => {
      this.updateMatchCounter();
    });
  }
  
  private async onSearchInput(): Promise<void> {
    const query = this.searchInput.value;
    if (query) {
      const regex = new RegExp(query, 'gi');
      await this.searchProvider.startQuery(regex);
    } else {
      await this.searchProvider.endQuery();
    }
  }
  
  private async onSearchKeydown(event: KeyboardEvent): Promise<void> {
    if (event.key === 'Enter') {
      if (event.shiftKey) {
        await this.searchProvider.highlightPrevious();
      } else {
        await this.searchProvider.highlightNext();
      }
    } else if (event.key === 'Escape') {
      await this.searchProvider.endQuery();
    }
  }
  
  private updateMatchCounter(): void {
    const current = this.searchProvider.currentMatchIndex;
    const total = this.searchProvider.matchesCount;
    
    if (total === 0) {
      this.matchCounter.textContent = 'No matches';
    } else {
      this.matchCounter.textContent = `${current + 1} of ${total}`;
    }
  }
  
  async replaceNext(): Promise<void> {
    const replaceText = this.replaceInput.value;
    await this.searchProvider.replaceCurrentMatch(replaceText, true);
  }
  
  async replaceAll(): Promise<void> {
    const replaceText = this.replaceInput.value;
    const count = await this.searchProvider.replaceAllMatches(replaceText);
    console.log(`Replaced ${count} matches`);
  }
}

Types

interface ISearchMatch {
  text: string;
  start: CodeEditor.IPosition;
  end: CodeEditor.IPosition;
}

interface IBaseSearchProvider extends IDisposable {
  readonly currentMatchIndex: number | null;
  readonly isActive: boolean;
  readonly matchesCount: number;
  readonly stateChanged: ISignal<IBaseSearchProvider, void>;
  
  clearHighlight(): Promise<void>;
  setIsActive(active: boolean): Promise<void>;
  startQuery(query: RegExp | null, filters?: IFilters): Promise<void>;
  endQuery(): Promise<void>;
  highlightNext(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
  highlightPrevious(loop?: boolean, options?: IHighlightAdjacentMatchOptions): Promise<ISearchMatch | undefined>;
  replaceCurrentMatch(newText: string, loop?: boolean, options?: IReplaceOptions): Promise<boolean>;
  replaceAllMatches(newText: string, options?: IReplaceOptions): Promise<boolean>;
}

type SearchStartAnchor = 'auto' | 'selection' | 'selection-start' | 'previous-match' | 'start';

interface IHighlightMatchOptions {
  scroll?: boolean;
  select?: boolean;
}

interface IHighlightAdjacentMatchOptions extends IHighlightMatchOptions {
  from?: SearchStartAnchor;
}

interface IReplaceOptions {
  preserveCase?: boolean;
  useRegex?: boolean;
}

interface IFilters {
  caseSensitive?: boolean;
  wholeWord?: boolean;
  regex?: boolean;
}

docs

editor-commands.md

editor-core.md

editor-factory.md

extension-system.md

index.md

language-support.md

mime-type-service.md

search-replace.md

special-extensions.md

theme-system.md

tile.json