[symbol-provider-tree-sitter] Add specs for context, tag, icon

This commit is contained in:
Andrew Dupont 2024-01-14 13:38:33 -08:00
parent 58d9a0393e
commit 3565ea822e
4 changed files with 262 additions and 42 deletions

View File

@ -3,46 +3,54 @@
(atx_h1_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "· "))
(#set! symbol.prepend "· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h2_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "·· "))
(#set! symbol.prepend "·· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h3_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "··· "))
(#set! symbol.prepend "··· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h4_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "···· "))
(#set! symbol.prepend "···· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h5_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "····· "))
(#set! symbol.prepend "····· ")
(#set! symbol.icon "chevron-right"))
((atx_heading
(atx_h6_marker)
(heading_content) @name) @definition.heading
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "······ "))
(#set! symbol.prepend "······ ")
(#set! symbol.icon "chevron-right"))
((setext_heading
(heading_content) @name) @definition.heading
(setext_h1_underline)
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "· "))
(#set! symbol.prepend "· ")
(#set! symbol.icon "chevron-right"))
((setext_heading
(heading_content) @name) @definition.heading
(setext_h2_underline)
(#set! symbol.strip "(^\\s*|\\s*$)")
(#set! symbol.prepend "·· "))
(#set! symbol.prepend "·· ")
(#set! symbol.icon "chevron-right"))

View File

@ -107,7 +107,7 @@ This allows us to incorporate any transformations that were applied to the other
##### Adding the `context` field
The `context` field of a symbol is a short piece of text meant to give context. For instance, a symbol that represents a class method could have a `context` field that contains the name of the owning class. The `context` field is not filtered on.
The `context` field of a symbol is a short piece of text meant to give context. For instance, a symbol that represents a class method could have a `context` field that contains the name of the class it belongs to. The `context` field is not filtered on.
###### symbol.contextNode
@ -135,7 +135,11 @@ The point of `context` is to provide information to help you tell symbols apart,
##### Adding a tag
The `tag` field is a string (ideally a short string) that indicates a symbols kind or type. A `tag` for a class methods symbol might say `method`, whereas the symbol for the class itself might have a `tag` of `class`. These tags will be indicated in the UI with a badge or an icon.
The `tag` field is a string that indicates a symbols kind or type. It should be a single word wherever possible. A `tag` for a class methods symbol would typically be `method`, whereas the symbol for the class itself would typically have a `tag` of `class`. These tags will be indicated in the UI with a badge, an icon, or both.
If youre not sure what to call something, consult [this list from the Language Server Protocol spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind). But some symbols may not fit any of those, so ultimately its up to the author. (For example, headings in Markdown files are assigned a kind of `heading`.)
For consistency, tags should be all lowercase. The interface will apply its own casing effect through CSS (`text-transform: capitalize` by default, but customizable in UI themes).
The preferred method of adding a tag is to leverage the `@definition.` captures that are typically present in a tags file. For instance, in this excerpt from the JavaScript grammars `tags.scm` file…
@ -154,18 +158,7 @@ The preferred method of adding a tag is to leverage the `@definition.` captures
In cases where this is impractical, you can provide the tag explicitly with a predicate.
###### symbol.icon
```scm
(class_body (method_definition
name: (property_identifier) @name
(#set! symbol.icon "package")
))
```
The icon to be shown alongside the symbol in a list. Will only be shown if the user has enabled the “Show Icons in Symbols View” option in the `symbols-view` settings. You can see the full list of available icons by invoking the **Styleguide: Show** command and browsing the “Icons” section. The value can include the preceding `icon-` or can omit it; e.g., `icon-package` and `package` are both valid values.
If this value is omitted, this provider will still attempt to match certain common tag values to icons. If `tag` is not present on the symbol, or is an uncommon value, there will be a blank space instead of an icon.
Nearly all the tags on [the aforementioned list](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind) will also apply an appropriate `icon` to their symbol when assigned. If you choose a tag name not on that list, or want to override the default, you can use the `symbol.icon` predicate described below.
###### symbol.tag
@ -181,3 +174,16 @@ The `symbol.tag` predicate will set the value of a symbols `tag` property to
The `tag` property is used to supply a word that represents the symbol in some way. For conventional symbols, this will often be something like `class` or `function`.
This provider will attempt to match certain common tag values to icons. This can be overridden by specifying an explicit `symbol.icon` value.
###### symbol.icon
```scm
(class_body (method_definition
name: (property_identifier) @name
(#set! symbol.icon "package")
))
```
The icon to be shown alongside the symbol in a list. Will only be shown if the user has enabled the “Show Icons in Symbols View” option in the `symbols-view` settings. You can see the full list of available icons by invoking the **Styleguide: Show** command and browsing the “Icons” section. The value can include the preceding `icon-` or can omit it; e.g., `icon-package` and `package` are both valid values.
If this value is omitted, this provider will still attempt to match certain common tag values to icons. If `tag` is not present on the symbol, or is an uncommon value, there will be a blank space instead of an icon.

View File

@ -27,28 +27,66 @@ const PatternCache = {
},
};
// Assign a default icon type for each tag — or what LSP calls “kind.” This
// list is copied directly from the LSP spec's exhaustive list of potential
// symbol kinds:
//
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind
function iconForTag(tag) {
switch (tag) {
case 'function':
return 'icon-gear';
case 'method':
return 'icon-gear';
case 'namespace':
return 'icon-tag';
case 'variable':
return 'icon-code';
case 'class':
return 'icon-package';
case 'constant':
return 'icon-primitive-square';
case 'property':
return 'icon-primitive-dot';
case 'interface':
return 'icon-key';
case 'constructor':
return 'icon-tools';
case 'file':
return 'icon-file';
case 'module':
return 'icon-database';
case 'namespace':
return 'icon-tag';
case 'package':
return 'icon-package';
case 'class':
return 'icon-puzzle';
case 'method':
return 'icon-gear';
case 'property':
return 'icon-primitive-dot';
case 'field':
return 'icon-primitive-dot';
case 'constructor':
return 'icon-tools';
case 'enum':
return 'icon-list-unordered';
case 'interface':
return 'icon-key';
case 'function':
return 'icon-gear';
case 'variable':
return 'icon-code';
case 'constant':
return 'icon-primitive-square';
case 'string':
return 'icon-quote';
case 'number':
return 'icon-plus';
case 'boolean':
return 'icon-question';
case 'array':
return 'icon-list-ordered';
case 'object':
return 'icon-file-code';
case 'key':
return 'icon-key';
case 'null':
return null;
case 'enum-member':
return 'icon-primitive-dot';
case 'struct':
return 'icon-book';
case 'event':
return 'icon-calendar';
case 'operator':
return 'icon-plus';
case 'type-parameter':
return null;
default:
return null;
}
@ -66,9 +104,10 @@ class Container {
this.capture = capture;
this.node = capture.node;
this.organizer = organizer;
this.props = capture.setProperties || {};
this.tag = capture.name.substring(capture.name.indexOf('.') + 1);
this.icon = iconForTag(this.tag);
this.icon = this.resolveIcon();
this.position = capture.node.range.start;
}
@ -108,6 +147,13 @@ class Container {
);
}
resolveIcon() {
let icon = this.props['symbol.icon'] ?? iconForTag(this.tag);
if (icon && !icon.startsWith('icon-'))
icon = `icon-${icon}`;
return icon;
}
toSymbol() {
if (!this.nameCapture) return null;
let nameSymbol = this.nameCapture.toSymbol();
@ -115,7 +161,7 @@ class Container {
name: nameSymbol.name,
shortName: nameSymbol.shortName,
tag: nameSymbol.tag ?? this.tag,
icon: nameSymbol.icon ?? iconForTag(nameSymbol.tag) ?? iconForTag(this.tag),
icon: nameSymbol.icon ?? this.icon,
position: this.position
};

View File

@ -248,6 +248,166 @@ describe('TreeSitterProvider', () => {
grammar = editor.getGrammar();
});
describe('symbol.context', () => {
beforeEach(async () => {
await grammar.setQueryForTest('tagsQuery', scm`
(
(variable_declaration
(variable_declarator
name: (identifier) @name
value: [(arrow_function) (function)]))
(#set! symbol.context "something")
)
`);
});
it('assigns a `context` property on each symbol', async () => {
let symbols = await getSymbols(editor, 'file');
expect(symbols[0].context).toBe('something');
expect(symbols[0].position.row).toEqual(0);
expect(symbols[1].context).toBe('something');
expect(symbols[1].position.row).toEqual(1);
});
});
describe('symbol.contextNode', () => {
beforeEach(async () => {
await grammar.setQueryForTest('tagsQuery', scm`
(
(property_identifier) @name
(#eq? @name "push")
(#set! symbol.contextNode "parent.firstNamedChild")
)
`);
});
it('assigns a `context` property on each symbol containing the text of the referenced node', async () => {
let symbols = await getSymbols(editor, 'file');
expect(symbols[0].name).toBe('push');
expect(symbols[0].context).toBe('left');
expect(symbols[0].position.row).toEqual(6);
expect(symbols[1].name).toBe('push');
expect(symbols[1].context).toBe('right');
expect(symbols[1].position.row).toEqual(6);
});
});
describe('symbol.icon', () => {
it('defines an `icon` property on each symbol', async () => {
await grammar.setQueryForTest('tagsQuery', scm`
(
(variable_declaration
(variable_declarator
name: (identifier) @name
value: [(arrow_function) (function)]))
(#set! symbol.icon "book")
)
`);
let symbols = await getSymbols(editor, 'file');
console.log('symbols:', symbols);
expect(symbols[0].icon).toBe('icon-book');
expect(symbols[0].position.row).toEqual(0);
expect(symbols[1].icon).toBe('icon-book');
expect(symbols[1].position.row).toEqual(1);
});
it('supersedes an `icon` property assigned by a tag', async () => {
await grammar.setQueryForTest('tagsQuery', scm`
(
(variable_declaration
(variable_declarator
name: (identifier) @name
value: [(arrow_function) (function)]))
(#set! symbol.tag "class")
(#set! symbol.icon "book")
)
`);
let symbols = await getSymbols(editor, 'file');
expect(symbols[0].icon).toBe('icon-book');
expect(symbols[0].position.row).toEqual(0);
expect(symbols[1].icon).toBe('icon-book');
expect(symbols[1].position.row).toEqual(1);
});
it('supersedes an `icon` property inferred by its container', async () => {
await grammar.setQueryForTest('tagsQuery', scm`
(
(variable_declaration
(variable_declarator
name: (identifier) @name
value: [(arrow_function) (function)]))
(#set! symbol.tag "class")
(#set! symbol.icon "book")
) @definition.namespace
`);
let symbols = await getSymbols(editor, 'file');
expect(symbols[0].icon).toBe('icon-book');
expect(symbols[0].position.row).toEqual(0);
expect(symbols[1].icon).toBe('icon-book');
expect(symbols[1].position.row).toEqual(1);
});
});
describe('symbol.tag', () => {
it('defines a `tag` property on each symbol', async () => {
await grammar.setQueryForTest('tagsQuery', scm`
(
(variable_declaration
(variable_declarator
name: (identifier) @name
value: [(arrow_function) (function)]))
(#set! symbol.tag "class")
)
`);
let symbols = await getSymbols(editor, 'file');
expect(symbols[0].tag).toBe('class');
expect(symbols[0].icon).toBe('icon-puzzle');
expect(symbols[0].position.row).toEqual(0);
expect(symbols[1].tag).toBe('class');
expect(symbols[1].icon).toBe('icon-puzzle');
expect(symbols[1].position.row).toEqual(1);
});
it('supersedes the `tag` property inferred by its container', async () => {
await grammar.setQueryForTest('tagsQuery', scm`
(
(variable_declaration
(variable_declarator
name: (identifier) @name
value: [(arrow_function) (function)]))
(#set! symbol.tag "class")
) @definition.namespace
`);
let symbols = await getSymbols(editor, 'file');
expect(symbols[0].tag).toBe('class');
expect(symbols[0].icon).toBe('icon-puzzle');
expect(symbols[0].position.row).toEqual(0);
expect(symbols[1].tag).toBe('class');
expect(symbols[1].icon).toBe('icon-puzzle');
expect(symbols[1].position.row).toEqual(1);
});
});
describe('symbol.strip', () => {
beforeEach(async () => {
await grammar.setQueryForTest('tagsQuery', scm`