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
{
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
}
}
class SymbolsView {
static highlightMatches() {}
constructor() {}
destroy() {
}
// ...
cancel() {}
}
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.
}
}
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.
}
}
// 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.
}
}
{
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();
}
}
// 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.
//
}
}