DEV Community

Cover image for Code Telescope: bringing Telescope's power to VS Code
Guilherme Costa
Guilherme Costa

Posted on

Code Telescope: bringing Telescope's power to VS Code

If you're a Neovim user, you've probably heard of (or use) Telescope.nvim

One of the most powerful tools in the Neovim ecosystem for fuzzy navigationβ€”files, text, symbols, git commits, branches, and more, all in a single interface.

What if you could have that same experience in VS Code?

That's the motivation behind Code Telescopeβ€”an extension that brings Telescope's philosophy to Visual Studio Code.


🎯 The Problem It Solves

Let's be honest: VS Code already has several ways to search for things:

  • Ctrl+P for files
  • Ctrl+Shift+F for text
  • Ctrl+Shift+O for symbols
  • Ctrl+Shift+G for git

But each of these is a different interface, with different behaviors, and different keyboard shortcuts. There's no unified "hub" for searching.

Code Telescope solves this by providing a single interface that abstracts all these functionalitiesβ€”and moreβ€”into a consistent, extensible, and powerful fuzzy finder.

πŸ—οΈ Architecture: Extensibility via Decorators

The extension is built on three architectural pillars:

  1. Annotation-based adapters β€” Each finder is registered via decorators
  2. Backend/UI separation β€” Extension host (backend) separate from webview (UI)
  3. Type-safe communication β€” Shared interfaces between layers
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Extension Host (Backend)                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                        β”‚
β”‚  β”‚ Finder Providersβ”‚                                        β”‚
β”‚  β”‚ @FuzzyFinder()  β”‚                                        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                        β”‚
β”‚           β”‚                                                 β”‚
β”‚           β”‚                                                 β”‚
β”‚           β”‚                                                 β”‚
β”‚    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                                        β”‚
β”‚    β”‚ Presentation  β”‚                                        β”‚
β”‚    β”‚     Layer     β”‚                                        β”‚
β”‚    β”‚  - Registry   β”‚                                        β”‚
β”‚    β”‚  - Handlers   β”‚                                        β”‚
β”‚    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚ Message Protocol (type-safe)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”           Webview (UI)                 β”‚
β”‚    β”‚   Webview     β”‚                                        β”‚
β”‚    β”‚  Controller   β”‚                                        β”‚
β”‚    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                        β”‚
β”‚           β”‚                                                 β”‚
β”‚           ┼────────────────┐                                β”‚
β”‚           β”‚                β”‚                                β”‚
β”‚     β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚     β”‚   Data    β”‚  β”‚    Preview     β”‚                       β”‚
β”‚     β”‚  Adapters β”‚  β”‚   Renderers    β”‚                       β”‚
β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ @PreviewRender β”‚                       β”‚
β”‚                    β”‚      ()        β”‚                       β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚                             β”‚                               β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
β”‚                    β”‚    Keyboard     β”‚                      β”‚
β”‚                    β”‚    Handlers     β”‚                      β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

πŸ”Œ Finders (Backend)

Finders are data providers registered via @FuzzyFinderAdapter:

@FuzzyFinderAdapter({
  fuzzy: "workspace.files",
})
export class WorkspaceFileProvider implements IFuzzyFinderProvider {
  async querySelectableOptions(): Promise<QueryResult> {
    // Return list of files
  }

  async onSelect(identifier: string): Promise<SelectResult> {
    // Action on selection (open file, navigate, etc.)
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”„ Data Adapters (UI)

On the UI side, data adapters transform backend data into displayable options:

export class FileDataAdapter implements IFuzzyFinderDataAdapter {
  parseOptions(data: any): FileOption[] {
    // Convert backend data to UI options
  }

  getDisplayText(option: FileOption): string {
    // Define how the option appears in the list
  }

  filterOption(option: FileOption, query: string): boolean {
    // Custom filtering logic
  }
}
Enter fullscreen mode Exit fullscreen mode

Data adapters are the bridge between raw backend responses and the interactive list shown to users.

🎨 Preview Renderers (UI)

Preview renderers are also on the UI side and transform raw data into visual representations, registered via @PreviewRendererAdapter:

@PreviewRendererAdapter({
  adapter: "preview.codeHighlighted",
})
export class CodeHighlightedPreviewRenderer implements IPreviewRendererAdapter {
  async render(
    previewElement: HTMLElement,
    data: PreviewData,
    theme: string
  ): Promise<void> {
    // Render syntax-highlighted code in the preview pane
  }
}
Enter fullscreen mode Exit fullscreen mode

This keeps the backend cleanβ€”it only provides dataβ€”while the UI is responsible for how that data looks.
Code Telescope supports any VS Code theme out of the box. The preview uses Shiki as the syntax highlighter, optimized for minimal bundle size by loading only the necessary grammars on-demand. This means syntax highlighting that perfectly matches your current themeβ€”whether you're using Dracula, One Dark Pro, or any custom themeβ€”without bloating the extension.

πŸ“¦ What's Built-In

The extension ships with a complete suite of finders:

  • Workspace files β€” Fuzzy matching across all files
  • Text search β€” Integrated ripgrep
  • Symbols β€” Functions, classes, variables
  • Git branches β€” Quick branch switching
  • Keybindings β€” Browse VS Code shortcuts
  • Recent files β€” Recently opened files
  • Color schemes β€” Theme switching
  • Diagnostics β€” Errors, warnings, hints
  • Tasks β€” Workspace tasks
  • Call hierarchy β€” Function call relationships

And the best part: all with inline preview and keyboard-only navigation.

🎯 Harpoon: Bookmarking Style

Inspired by ThePrimeagen's Harpoon, I also included a bookmarking system:

  • Mark files with Ctrl+Alt+M
  • Navigate with Ctrl+1 through Ctrl+9
  • View all marks with Ctrl+Alt+H
  • Persists per workspace

This allows instant navigation to your most important filesβ€”no searching required.

πŸ”§ Extensibility: Creating Your Own Finders

One of the coolest parts: you can create custom finders without touching the extension code.

Just create a .cjs file in .vscode/code-telescope/:

module.exports = {
  fuzzyAdapterType: "custom.example",

  backend: {
    async querySelectableOptions() {
      return { items: ["Item 1", "Item 2"] };
    },

    async onSelect(item) {
      return { data: item, action: "showMessage" };
    }
  },

  ui: {
    dataAdapter: {
      parseOptions(data) {
        return data.items.map((item, i) => ({ id: i, text: item }));
      },
      getDisplayText(option) {
        return option.text;
      },
      filterOption(option, query) {
        return option.text.toLowerCase().includes(query.toLowerCase());
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

The system does the restβ€”automatic wiring via the type system. You can check an example here

πŸ€” Why This Architecture?

I chose this design for several reasons:

  1. Decoupling β€” Finders don't know about UI, UI doesn't know about implementation details
  2. Testability β€” Each component can be tested independently
  3. Type safety β€” Compile-time guarantees prevent integration bugs
  4. Extensibility β€” New finders without touching existing code

The decorator-based approach means the registry is built at compile-time, and the message protocol ensures the backend and UI stay in sync.

πŸ’‘ Inspiration

Obviously, Telescope.nvim by tjdevries (TJ DeVries) was the main inspirationβ€”the fuzzy finder that revolutionized Neovim navigation.

And Harpoon by ThePrimeagenβ€”the bookmarking system that changed how we think about file navigation.

What makes Telescope special isn't just the functionalityβ€”it's the extensibility and the unified experience. Code Telescope tries to capture that same essence.


If you use VS Code and miss Telescope's power, check it out on the VS Code Marketplace or Open VSX.
Youtube video explaining (It is in portuguese BR, but you can enable automatic translation)

Check code on Github

Feedback and contributions are welcome!

Top comments (0)