Skip to content

Instantly share code, notes, and snippets.

@savetheclocktower
Last active July 15, 2023 03:02
Show Gist options
  • Save savetheclocktower/be378d52fd9c6c09fd42af3bfb01b83e to your computer and use it in GitHub Desktop.
Save savetheclocktower/be378d52fd9c6c09fd42af3bfb01b83e to your computer and use it in GitHub Desktop.

symbols-view refactor

The goal is to refactor this package to be completely implementation-agnostic, much like autocomplete-plus. Other packages would then register as providers.

For instance, the current built-in ctags approach would be moved to, say, symbol-provider-ctags and would remain as a built-in package. The ctags approach notoriously doesn't work at all on untitled files, and is possibly stale when the buffer is modified; thus we could write a tree-sitter provider (as symbol-provider-tree-sitter) that doesn't suffer from these drawbacks. The tree-sitter provider would have drawbacks of its own, but each provider would be able to nominate itself as the best candidate for a given situation, and user preferences could help break ties as well.

And, of course, any LSP package could register itself as a provider of symbols.

Abstract behaviors we’re supporting; items in bold are already implemented in symbols-view, and should remain a part of any refactor:

  • project-wide symbol search (though the current implementation requires a tags file that the user has to generate themselves).
  • file-wide symbol search
  • ability to jump to definition (tags file required)
    • when there’s one result, jumps straight to it
    • otherwise acts like project search with the name of the current token already filled in
    • (this is a project-wide thing; we may want to also have something like “jump to where this is defined in this editor”)
  • ability to return to the place you were before jumping

Core package

Main module

{
  activate () {}
  deactivate () {}

  consumeSymbolProvider(...providers) {
    // Adds each provider to the `ProviderBroker` instance
  }

  toggleFileView() {
    // Loads `FileView`, populates with results
  }
  toggleProjectView() {
    // Loads `ProjectView`, populates with results
  }
  goToDeclaration() {
    // Loads `GoToView`, either goes straight to a definition or populates the
    // view with candidates
  }
  returnFromDeclaration() {
    // Remembers the last place you called `go-to-declaration` from and returns
    // you there
  }
}

Abstract Symbols view

class SymbolsView {
  static highlightMatches() {}
  constructor() {}
  destroy() {

  }

  // ...
  cancel() {}
}

File view

class FileView extends SymbolsView {
  constructor() {}
  destroy() {}
  // ...
  toggle() {
    // If already open, closes. Otherwise loads the `AtomSelectList` instance
    // and calls `populate`.
  }
  async populate() {
    // Call `getSymbols`; populate the results in the list view.
  }
  async getSymbols() {
    // Pick providers.
    // Call `provider#getSymbols` on each provider and aggregate results.
    // Ignore any providers that don't return results promptly.
  }
}

Project view

class ProjectView extends SymbolsView {
  constructor() {}
  destroy() {}
  // ...
  toggle() {
    // If already open, closes. Otherwise loads the `AtomSelectList` instance
    // and calls `populate`.
  }
  async populate() {
    // Call `getSymbols`; populate the results in the list view.
  }
  async getSymbols() {
    // Pick providers.
    // Call `provider#getSymbols` on each provider and aggregate results.
    // Ignore any providers that don't return results promptly.
  }
}

Broker

// Keeps track of which things have registered as providers.
class ProviderBroker {
  constructor() {}
  destroy() {}
  add(...providers) {
    // Adds the provider. Possibly should return a `Disposable` (or array of
    // `Disposable`s) that would call `remove` for the corresponding provider.
  }
  remove(provider) {}
  select(meta) {
    // Given some metadata, asks each active provider to rate their ability to
    // provide symbols.
    //
    // Returns an array of providers. The array can contain any number of
    // providers, including zero. All providers returned have indicated that
    // they are able to provide symbols for a given task.
  }
}

Provider

Main

{
  activate () {},
  deactivate () {},

  provideSymbols() {
    // Called with no arguments.
    //
    // No business logic should go in here. If a package wants to provide
    // symbols only under certain circumstances, it should decide those
    // circumstances on demand, rather than return this provider only
    // conditionally.
    //
    // A provider author may argue that they should be allowed to inspect the
    // environment before deciding what (or if) to return — but anything they'd
    // inspect is something that can change mid-session. Too complicated. All
    // provider decisions can get decided at runtime.
    //
    // So, for instance, if a certain provider only works with PHP files, it
    // should return its instance here no matter what, and that instance can
    // respond to `canProvideSymbols` with `false` if the given editor isn't
    // using a PHP grammar. It shouldn't try to get clever and bail out
    // entirely if, say, the project doesn't have any PHP files on load —
    // because, of course, it _could_ add a PHP file at any point, and we're
    // not going to revisit the decision later.
    //
    // We should probably allow a package to return an _array_ of providers as
    // an alternative to returning a single provider.
    return new SomeProvider();
  }
}

Provider instance

// All `getX` methods are optionally async. They all accept a `signal` argument
// — an `AbortSignal` generated from an `AbortController` — and if they _do_ go
// async, they should react if we abort the request, at the very least by
// returning `null` instead of trying to return results.
//
// I figure the `getX` methods should also be using a keyword args pattern
// because a lot of them will overlap a bit (e.g., all will take `signal`, but
// not all will take `range`) and I don't want to make people remember (or have
// to remember myself) the order they come in.

{
  // A function that is called when the provider is disposed, as may happen if
  // the project window is closed or reloaded.
  destroy() {},

  // The name of the package.
  packageName: String,

  // A humanized name for this provider to be shown in the UI when appropriate.
  name: String,

  // Whether the provider is exclusive. Defaults to `false`.
  //
  // A provider is “exclusive” if it aims to be the primary provider of symbols
  // — in other words, if it expects to provide the names of functions, class
  // names, and other important identifiers.
  //
  // The purpose of `isExclusive` is to prevent situations where multiple
  // providers will be competing to provide the exact same information. For
  // instance, there may be certain scenarios where symbol information can be
  // provided by `ctags`, a Tree-sitter query, _and_ a language server, and
  // it's wasteful to ask all three of them to do the task and aggregate their
  // results (which will largely be identical).
  //
  // The `symbols-view` package can aggregate the results of multiple
  // providers, but will include a maximum of _one_ provider that marks itself
  // as exclusive.
  //
  // On the other hand, certain kinds of providers will provide “unusual” kinds
  // of symbols, and are not necessarily competing with one another. For
  // instance, a provider that adds bookmarked rows to the symbols list would
  // not be an exclusive provider.
  //
  // If you're unsure if your provider is exclusive, set this value to `false`.
  //
  isExclusive: Boolean,

  // An optional method. If it exists, the main package will register a
  // callback so that it can clear the cache of this provider's symbols.
  //
  // The main package will automatically clear its cache for these reasons:
  //
  // * when the main package's config changes (entire cache);
  // * when any provider is activated or deactivated (single provider's cache);
  // * when the buffer is modified in any of several ways, including grammar
  //   change, save, or buffer change (entire cache).
  //
  // If your provider may have its cache invalidated for reasons not in this
  // list, you should implement `onShouldClearCache` and invoke any callback
  // that registers for it. The `EventEmitter` pattern found throughout Pulsar
  // is probably how you want to pull this off.
  onShouldClearCache() {},

  canProvideSymbols(meta) {
    // Using the metadata, returns a score representing its confidence at being
    // able to suggest symbols. `meta` will, as much as possible, contain the
    // same bundle of keyword arguments that the appropriate `getX` method will
    // take in a moment.
    //
    // This method is synchronous.
    //
    // Example: “This is an untitled document, and my provider reads from disk,
    // so I'll return `0` because I'd be of no help.”
    //
    // Or: “This document exists on disk, but has been modified, so I'll return
    // a score of `0.9` because my results probably will be less accurate than
    // another provider's.”
    //
    // Or: “This is an untitled document, and I'm useful in that situation… but
    // the user wants _project-wide_ search, and I'm not able to do that. I'll
    // return `0`.”
    //
    // Or: “I'm an LSP provider and I know what my capabilities and limitations
    // are, so I'll return `1` if I know the relevant LSP supports this and `0`
    // otherwise.”
    //
    // Can also return a boolean; `true` will be converted to `1` and `false`
    // will be converted to `0`.
    //
    // Scores come into play when more than one provider is able to do the
    // task, but _only_ for providers that have marked themselves as exclusive.
    // It's a bit adversarial, but otherwise I don't know how to negotiate and
    // break ties. User configuration could come into play here; not sure how
    // it'd work in practice, but maybe the user can specify a list of
    // comma-separated provider names. Providers that are present in that list
    // would win out over everything not present in that list, and earlier
    // entries in the list would win out over later entries.
    //
    // For non-exclusive providers, scores don't come into play, so providers
    // can simply return a boolean. `symbols-view` will include any
    // non-exclusive provider that returns truthy and exclude any that returns
    // falsy.
    //
    // If the score-based approach doesn't seem workable, we can just have this
    // method return a boolean.
  },

  getSymbols({ editor, type, signal }) {
    // Called in all scenarios where the consumer wants a list of symbols. The
    // provider can inspect the metadata and respond in various ways:
    //
    // * If `type` is `file`, the provider can return all symbols in the file.
    // * If `type` is `project`, the provider can return all symbols in the
    //   project.
    // * If `type` is `project-find`, the provider can return all symbols in
    //   the project that match the word under the cursor — leaving the provider
    //   free to determine what constitutes a “word.”
    //   
    // For certain providers, like LSPs, each different `type` might map to a
    // different LSP action. For others, like `ctags`, each `type` would
    // largely result in the same tag search.
    //
    // Other keyword args that might be present:
    //
    // * `editor`: The current `TextEditor`.
    // * `signal`: An instance of `AbortSignal` that represents whether the
    //   user has cancelled the task.
    // * `word`: The word under the cursor, if it's relevant. Provided when the
    //   user invokes “Go to Definition.” A provider is free to ignore this
    //   argument and use the `editor` argument if it thinks it might have a
    //   different definition of “word” than the editor itself.
    //   
    // Other information will be available from the `atom` global and would be
    // redundant to provide as a keyword argument. For instance, the project
    // path(s) will always be available under `atom.project.getPaths()`.
    //   
    // This method can go async. If it does, it should react to the `signal`
    // it's been given, which is an instance of `AbortSignal`. If the user
    // cancels the initiating task, the provider should stop its work and
    // return `null`.
    //
    // Otherwise, the method should return (or resolve with) a list of symbols,
    // each of which is an object with the following signature:
    //
    // * `position`: An instance of `Point` describing the symbol's location.
    //   The `column` value of the point will be ignored. Required.
    // * `name`: The name of the symbol. This value will be shown in the UI and
    //   will be filtered against if the user types in the text box. Required.
    //
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment