From 3565ea822e6968a31610fd21e922cc7834e0a662 Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Sun, 14 Jan 2024 13:38:33 -0800 Subject: [PATCH] [symbol-provider-tree-sitter] Add specs for `context`, `tag`, `icon` --- .../tree-sitter/tree-sitter-markdown/tags.scm | 24 ++- .../symbol-provider-tree-sitter/README.md | 34 ++-- .../lib/capture-organizer.js | 86 +++++++--- .../spec/symbol-provider-tree-sitter-spec.js | 160 ++++++++++++++++++ 4 files changed, 262 insertions(+), 42 deletions(-) diff --git a/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/tags.scm b/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/tags.scm index de77c5828..b77668759 100644 --- a/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/tags.scm +++ b/packages/language-gfm/grammars/tree-sitter/tree-sitter-markdown/tags.scm @@ -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")) diff --git a/packages/symbol-provider-tree-sitter/README.md b/packages/symbol-provider-tree-sitter/README.md index 9ef72458b..e5a5dc204 100644 --- a/packages/symbol-provider-tree-sitter/README.md +++ b/packages/symbol-provider-tree-sitter/README.md @@ -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 symbol’s kind or type. A `tag` for a class method’s 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 symbol’s kind or type. It should be a single word wherever possible. A `tag` for a class method’s 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 you’re 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 it’s 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 grammar’s `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 symbol’s `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. diff --git a/packages/symbol-provider-tree-sitter/lib/capture-organizer.js b/packages/symbol-provider-tree-sitter/lib/capture-organizer.js index 9086bb149..64e3f38e2 100644 --- a/packages/symbol-provider-tree-sitter/lib/capture-organizer.js +++ b/packages/symbol-provider-tree-sitter/lib/capture-organizer.js @@ -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 }; diff --git a/packages/symbol-provider-tree-sitter/spec/symbol-provider-tree-sitter-spec.js b/packages/symbol-provider-tree-sitter/spec/symbol-provider-tree-sitter-spec.js index acc0228f7..ad8b7fdf3 100644 --- a/packages/symbol-provider-tree-sitter/spec/symbol-provider-tree-sitter-spec.js +++ b/packages/symbol-provider-tree-sitter/spec/symbol-provider-tree-sitter-spec.js @@ -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`