Telescope Manager

Telescope is a highly extensible fuzzy finder over a set of various sources, such as commands, notebooks and tags. The Telescope Manager holds those sources and lets your plugin register its own. Available as env.telescope on the Environment passed to activate(env).

Open it with core:toggle-telescope, or jump straight to one source with core:show-telescope and its scopedSourceId argument — see Opening your source directly.

Built-in sources

Each source has an id, and most also have an alias — type it followed by a Space in the Telescope input to scope the search to that source. The space is what commits the prefix, so b on its own still searches everything while b switches to Notebooks. The id works as a prefix too, so books does the same thing.

SourceidDefault aliasDescription
Commandscommands>Search and run commands
NotebooksbooksbSearch notebooks
TagstagstSearch tags
IndexindexSearch sources

The Index source is special: it lists the other available sources so the user can pick one, and it is reached through getIndexSource() rather than being part of the registered set.

Users can override an alias or turn a source off entirely from config.json, and your source gets the same treatment for free:

{
  "telescope": {
    "sources": {
      "my-plugin-source": { "alias": "m", "disabled": false }
    }
  }
}

Creating a source

A source is a class extending TelescopeSource, which Inkdrop provides at runtime through the inkdrop module — so import it from there, and keep inkdrop out of your bundle. You implement two methods: getItems() returns what to show for the current query, and apply() runs when the user picks one of your items.

import { TelescopeSource } from 'inkdrop'
import type {
  TelescopeContext,
  TelescopeResult,
  TelescopeSourceItem
} from '@inkdropapp/types'

import { getEnv } from './env'

export class MyPluginSource extends TelescopeSource {
  id = 'my-plugin-source'
  name = 'My Plugin'
  description = 'Search my plugin'
  defaultAlias = 'm'

  getItems(context: TelescopeContext): TelescopeResult {
    return {
      options: [
        {
          id: 'say-hello',
          type: 'Action',
          label: 'Say hello',
          detail: context.query,
          source: this.id,
          actions: [{ type: 'run', label: 'Run' }]
        }
      ]
    }
  }

  apply(item: TelescopeSourceItem, action: string): boolean {
    if (action === 'run') {
      getEnv().notifications.addInfo(`Hello from ${item.label}!`)
    }
    return true
  }
}

Register it in activate() and remove it in deactivate():

import type { Environment, IInkdropPlugin } from '@inkdropapp/types'

import { setEnv } from './env'
import { MyPluginSource } from './my-source'

class MyPlugin implements IInkdropPlugin {
  source = new MyPluginSource()

  activate(env: Environment) {
    setEnv(env)
    env.telescope.registerSource(this.source)
  }

  deactivate(env: Environment) {
    env.telescope.unregisterSource(this.source.id)
    setEnv(undefined)
  }
}

export default new MyPlugin()

Opening your source directly

Registering a source is enough for the user to reach it by typing its alias, but a source usually wants its own keystroke too. Add a command that dispatches core:show-telescope scoped to your id — inside activate(), alongside the registerSource() call above:

this.disposable = env.commands.add(document.body, {
  'my-plugin:show': () => {
    env.commands.dispatch(document.body, 'core:show-telescope', {
      scopedSourceId: this.source.id,
      initialSelectedItemId: 'say-hello',
      cancelBehavior: 'close'
    })
  }
})

initialSelectedItemId preselects one of your items — the id you gave it in getItems() — which is how a source can open with the row matching the current context already highlighted. cancelBehavior: 'close' makes Esc on an empty query dismiss Telescope outright, and hides the back button, instead of unscoping back to all sources first — the right choice when your command is the only reason Telescope is open. Leave it out (or pass 'backToIndex') to keep the default step-back.

Dispose it in deactivate() next to unregisterSource(), then bind it from your plugin's keymap.

Example plugins

Two small plugins that do little besides add a source, worth reading end to end:

  • telescope-toc — a table of contents for the note you are editing. Reads the editor's syntax tree in getItems(), shows heading depth and line number through prefix and detail, holds document order with boost, and gives its source a keystroke that opens Telescope with the section under the cursor already selected.
  • telescope-themes — switches between the installed themes. Draws a colour swatch per row from the item's icon, marks the active theme with an accessoryView, and applies the pick from apply().

Properties

  • Name
    availableSources
    Type
    Map<string, TelescopeSource>
    Description

    Every registered source, keyed by id. Note that this is the raw registry — unlike getAvailableSources(), it is not filtered by whether each source is enabled or currently available.


registerSource(source)

Register a source

Adds a source to the registry. Registering a source whose id is already taken replaces it, while keeping its original position in the source order.

Parameters

  • Name
    source
    Type
    TelescopeSource
    Required
    Description

    The source to register.

Example

env.telescope.registerSource(new MyPluginSource())

unregisterSource(sourceId)

Unregister a source

Removes a source from the registry by its id.

Parameters

  • Name
    sourceId
    Type
    string
    Required
    Description

    The id of the source to remove.

Example

env.telescope.unregisterSource('my-plugin-source')

getSource(sourceId)

Get a source

Looks up a registered source by its id, regardless of whether it is enabled or available.

Parameters

  • Name
    sourceId
    Type
    string
    Required
    Description

    The id of the source to get.

Returns

The matching TelescopeSource, or undefined if nothing is registered under that id.

Example

const source = env.telescope.getSource('books')

getAllSources()

Get all sources

Returns every registered source that is enabled, in registration order. Sources the user has disabled in config.json are left out.

Returns

An Array of TelescopeSource.

Example

const ids = env.telescope.getAllSources().map(source => source.id)

getAvailableSources()

Get available sources

Same as getAllSources(), narrowed further to the sources whose isAvailable() returns true for the current app state — this is the set Telescope actually offers the user right now.

Returns

An Array of TelescopeSource.

Example

const sources = env.telescope.getAvailableSources()

getActiveSourceForPrefix(prefix)

Get the source for a prefix

Finds the available source matching a typed prefix — the prefix matches either a source's id or its alias.

Parameters

  • Name
    prefix
    Type
    string
    Required
    Description

    The prefix to match, e.g. "b" or "books".

Returns

The matching TelescopeSource, or null if no available source matches.

Example

const source = env.telescope.getActiveSourceForPrefix('b')
// -> the Notebooks source

getIndexSource()

Get the index source

Returns the built-in Index source, which lists the available sources themselves so the user can pick one to scope the search to. It is held separately from the registry, so it never appears in getAllSources().

Returns

A TelescopeSource.

Example

const index = env.telescope.getIndexSource()

getSourceItems(context)

Get items from every source

Calls getItems() on every source from getAllSources() in parallel and collects the results.

This is a convenience for querying every source at once; it is not what the Telescope UI runs. Typing drives each source separately from getAvailableSources() — or the index source alone while the query is empty — so a source can resolve and render as soon as it is ready.

Parameters

  • Name
    context
    Type
    TelescopeContext
    Required
    Description

    The context to pass to each source. See TelescopeContext.

Returns

A Promise resolving to an Array of TelescopeResult — one per source, in the same order — or null if any source threw.

Example

const results = await env.telescope.getSourceItems(context)

TelescopeSource

The base class for a source. Import it from the inkdrop module and extend it, implementing at least id, name, description, getItems() and apply().

import { TelescopeSource } from 'inkdrop'

Source properties

  • Name
    id
    Type
    string
    Required
    Description

    A unique identifier for the source, e.g. "books". It doubles as a prefix the user can type, and as the key under telescope.sources in config.json.

  • Name
    name
    Type
    string
    Required
    Description

    A human-readable name for the source, e.g. "Notebooks". Nothing in the current UI renders it — description is what the index source shows — so treat it as a label for tooling and future surfaces.

  • Name
    description
    Type
    string
    Required
    Description

    What the source searches, e.g. "Search notebooks". It is shown as the source's label in the index source.

  • Name
    defaultAlias
    Type
    string
    Description

    A short prefix that scopes the search to this source, e.g. "b". The user can override it with telescope.sources.<id>.alias in config.json.


getItems(context)

Get the items

Required. Produces the items to show for the current query. It may return synchronously or as a Promise, so it is fine to hit the database or the network here — but check context.aborted before doing expensive work, since the user may have typed on.

Parameters

  • Name
    context
    Type
    TelescopeContext
    Required
    Description

    The current query and state. See TelescopeContext.

Returns

A TelescopeResult, or a Promise of one.

Example

getItems(context) {
  return {
    options: [
      {
        id: 'say-hello',
        type: 'Action',
        label: 'Say hello',
        source: this.id
      }
    ]
  }
}

apply(item, action)

Apply an item

Required. Runs when the user picks one of your items. action is the type of the action they chose — with no modifier key held, that is the first action in the item's actions array.

Parameters

  • Name
    item
    Type
    TelescopeSourceItem
    Required
    Description

    The item the user selected. See TelescopeSourceItem.

  • Name
    action
    Type
    string
    Required
    Description

    The type of the chosen action.

Returns

A Boolean: true closes the Telescope bar, false leaves it open — useful when the item drills down into another view instead of finishing the interaction.

Example

apply(item, action) {
  if (action === 'run') {
    getEnv().notifications.addInfo(item.label)
  }
  return true
}

getIcon()

Get the icon

Optional. Returns the icon shown for the source itself, in the row the index source renders for it.

Return a React node — Inkdrop does not expose its own icon set to plugins, so ship the SVG with your plugin. The signature also permits a String, but the return value is rendered as a React child, so a string of markup comes out as escaped text.

Returns

A React node.

Example

const OutlineIcon = () => (
  <svg width="16" height="16" viewBox="0 0 16 16"
    fill="none" stroke="currentColor" strokeWidth="1.5">
    <line x1="5.5" y1="3" x2="14" y2="3" />
    <line x1="5.5" y1="8" x2="14" y2="8" />
    <line x1="5.5" y1="13" x2="14" y2="13" />
  </svg>
)

getIcon() {
  return <OutlineIcon />
}

getAlias()

Get the alias

Returns the prefix that scopes the search to this source, reading telescope.sources.<id>.alias from the Config and falling back to defaultAlias. You inherit this — override it only if the alias has to be computed.

Returns

A String, or undefined if the source has neither a configured alias nor a defaultAlias.


isEnabled()

Is the source enabled

Returns whether the user has kept this source on, reading telescope.sources.<id>.disabled from the Config. You inherit this, and rarely need to override it.

Returns

A Boolean. Defaults to true.


isAvailable()

Is the source available

Returns whether the source makes sense in the current app state. Override it when your source depends on something that isn't always there — the built-in Notebooks source, for instance, hides itself while the sidebar is closed or distraction-free mode is on.

Returns

A Boolean. Defaults to true.

Example

isAvailable() {
  const { mainLayout } = getEnv().store.getState()
  return mainLayout.sidebarVisible
}

Types

TelescopeContext

The query and state handed to getItems().

  • Name
    query
    Type
    string
    Description

    The text the user has typed, with any source prefix already stripped.

  • Name
    activeElement
    Type
    HTMLElement
    Description

    The element that was focused when Telescope opened, falling back to document.body.

  • Name
    workspaceId
    Type
    string | null
    Description

    The workspace the search is scoped to, or null when it isn't scoped.

  • Name
    aborted
    Type
    boolean
    Description

    Whether the query has been superseded. Check this in an async getItems() to bail out of work whose result would be thrown away.

  • Name
    addEventListener(type, listener)
    Type
    function
    Description

    Registers an abort handler. type is always "abort".


TelescopeResult

What getItems() returns.

  • Name
    options
    Type
    Array<TelescopeSourceItem>
    Required
    Description

    The items to display. See TelescopeSourceItem.

  • Name
    filter
    Type
    boolean
    Description

    Whether Telescope should fuzzy-filter and score your items against the query. Set it to false when you have already filtered them yourself — they are then all shown, in the order given, above the filtered ones. Defaults to true.

  • Name
    getMatch
    Type
    (item, matched?) => readonly number[]
    Description

    Computes which ranges of an item's displayLabel to highlight. Return a flat Array of numbers where each adjacent pair is a start and end offset. Only needed for items that set displayLabel.


TelescopeSourceItem

A single row in the Telescope list.

  • Name
    id
    Type
    string
    Required
    Description

    A unique identifier for the item. Telescope merges every source's items into one list and resolves both the selection and the picked item by id alone, so it has to be unique across all sources, not just yours — namespace it, the way the built-in Commands source emits command:core-open-note.

  • Name
    type
    Type
    string
    Required
    Description

    A short kind label shown on the right side of the row, e.g. "Notebook".

  • Name
    label
    Type
    string
    Required
    Description

    The text shown for the item, and what the query is matched against.

  • Name
    source
    Type
    string
    Required
    Description

    The id of the source that produced the item — set it to this.id.

  • Name
    labelClass
    Type
    string
    Description

    An extra CSS class for the label element.

  • Name
    displayLabel
    Type
    string
    Description

    Renders instead of label without affecting matching. Pair it with the result's getMatch to keep characters highlighted.

  • Name
    prefix
    Type
    string
    Description

    A short piece of information shown before the label, in a dimmer style.

  • Name
    detail
    Type
    string
    Description

    A short piece of information shown after the label, in a dimmer style.

  • Name
    boost
    Type
    number
    Description

    Nudges the item's rank against equally-matching items. A number from -99 to 99; positive moves it up, negative moves it down. It is added to the fuzzy-match score, so it is ignored entirely when the result sets filter: false.

    Deriving it from the item's position is a handy way to keep your own ordering as a tiebreak while still letting the query filter — a table of contents can pass boost: items.length - index so headings stay in document order.

  • Name
    icon
    Type
    string | () => React.ReactNode
    Description

    An icon for the left side of the row. A function returning a React node is the usual choice; a String has to be raw SVG markup starting with <svg, which is injected as HTML. Any other String renders nothing.

  • Name
    accessoryView
    Type
    string | () => React.ReactNode
    Description

    A custom view for the right side of the row, after type. A function returning a React node, or a String rendered as plain text.

  • Name
    actions
    Type
    Array<TelescopeAction>
    Description

    What the user can do with the item. See TelescopeAction. An item with no actions cannot be picked at all — Enter does nothing and apply() is never called — so give every selectable item at least one.


TelescopeAction

One of the things a user can do with an item. Telescope picks the first action whose modifierKeys are all held; if none matches, it falls back to actions[0]. So the first action in the array is the default — the one that runs on a bare Enter — whether or not it declares modifierKeys. Put the plain one first. Whichever is chosen, its type is passed to apply().

  • Name
    type
    Type
    string
    Required
    Description

    The identifier passed to apply() as its action argument, e.g. "open".

  • Name
    label
    Type
    string
    Required
    Description

    The text shown for the action in the footer hint at the bottom of Telescope, e.g. "Open Notebook", next to the keystroke that runs it.

  • Name
    icon
    Type
    string
    Description

    Accepted but not yet rendered anywhere.

  • Name
    modifierKeys
    Type
    Array<TelescopeModifierKey>
    Description

    The modifiers that select this action. All of them must be held for it to match. Leave it off the default action.

TelescopeModifierKey

An enum of "alt", "shift", "ctrl", "cmd", and "cmd-or-ctrl" — the last resolving to Cmd on macOS and Ctrl elsewhere.

import { TelescopeModifierKey } from '@inkdropapp/types'

actions: [
  { type: 'open', label: 'Open Notebook' },
  {
    type: 'open-workspace',
    label: 'Open as Workspace',
    modifierKeys: [TelescopeModifierKey.CmdOrCtrl]
  }
]
Can you help us improve the docs? 🙏

The source of these docs is here on GitHub. If you see a way these docs can be improved, please fork us!