From 5b03c4d00879de8dc06ebcbc6a8c2fe49ec712d6 Mon Sep 17 00:00:00 2001
From: nick <49458459+ndonfris@users.noreply.github.com>
Date: Fri, 7 Jun 2024 05:45:22 -0500
Subject: [PATCH] fix: fixed hover doc support & improved test coverage (#33)
* chore: added significant coverage to test-data/node-types.test.ts & src/utils/node-types.ts
* chore: testing functions in tree-sitter.ts
* chore: began writing signature.test.fish
* chore: 'signature -> alias, functions' in test-data/signature.test.ts
* Refactor: Update signatureHelpProvider in config.ts and import statements in server.ts
Why:
- The signatureHelpProvider object in config.ts has been refactored to include a new property 'workDoneProgress' and updated 'triggerCharacters'.
- Additional imports related to signature handling have been added in server.ts for better functionality.
Changes:
- Updated signatureHelpProvider in config.ts to include 'workDoneProgress' property and update 'triggerCharacters'.
- Added import statements for signature-related functions in server.ts to improve signature handling.
* docs: updates to ./docs/CONTRIBUTING.md
* feat: fixed test-data/formatting.test.ts
* docs: fixed emoji's in Header
* added to ./test-data/node-types.test.ts
* feat: added src/document-highlight.ts
* feat: added mini `fish-lsp.executeLine` command with `onExecuteCommandHandler`
- on line ~/.config/fish/config/fish (line: 596)
```fish
echo yes
```
`:CocCommand fish-lsp.executeLine $HOME/.config/fish/config.fish 595`
---
Output's in popup window:
```console
Executed: echo yes
Output: yes
```
* fix: line correction for client `onCommandExecution(connection, line-1)` in src/server.ts
* feat: parser language tests
* feat: (with logging) fix status/$status + multiline \ + function hover --flags
* docs: added contributing screenshots
* docs: README.md features
* fix: removed `{'paths': { '@utils/*': ['./src/utils/*']}}' bad resolution from tsconfig
---
README.md | 4 +-
docs/CONTRIBUTING.md | 197 +++++++--
package.json | 12 +-
src/config.ts | 11 +-
src/document-highlight.ts | 45 ++
src/executeHandler.ts | 19 +
src/hover.ts | 28 +-
src/server.ts | 144 +++++--
src/signature.ts | 142 +++++-
src/utils/completion/pager.ts | 12 +-
src/utils/completion/startup-cache.ts | 31 +-
src/utils/node-types.ts | 64 +++
src/utils/snippets.ts | 2 +-
src/utils/translation.ts | 23 +-
src/utils/tree-sitter.ts | 13 +-
test-data/fish_files/umask.fish | 2 +-
test-data/formatting.test.ts | 1 -
test-data/inline-parser.test.ts | 458 ++++++++++++++++++++
test-data/node-types.test.ts | 600 ++++++++++++++++++++++++--
test-data/parser.test.ts | 118 ++++-
test-data/semantic-tokens.test.ts | 27 ++
test-data/signature.test.ts | 224 ++++++++++
test-data/tree-sitter.test.ts | 356 +++++++++++++++
tsconfig.json | 3 +-
24 files changed, 2365 insertions(+), 171 deletions(-)
create mode 100644 src/document-highlight.ts
create mode 100644 src/executeHandler.ts
create mode 100644 test-data/inline-parser.test.ts
create mode 100644 test-data/semantic-tokens.test.ts
create mode 100644 test-data/signature.test.ts
create mode 100644 test-data/tree-sitter.test.ts
diff --git a/README.md b/README.md
index ac4638eb..c6563918 100644
--- a/README.md
+++ b/README.md
@@ -40,14 +40,14 @@ Introducing the [fish-lsp](https://fish-lsp.dev), a [Language Server Protocol (L
| --- | --- | --- |
| __Completion__ | Provides completions for commands, variables, and functions | ✅ |
| __Hover__ | Shows documentation for commands, variables, and functions. Has special handlers for `--flag`, `commands`, `functions`, `variables` | ✅ |
-| __Signature Help__ | Shows the signature of a command or function | ✖ |
+| __Signature Help__ | Shows the signature of a command or function | ✅ |
| __Goto Definition__ | Jumps to the definition of a command, variable, or function | ✅ |
| __Find References__ | Shows all references to a command, variable, or function | ✅ |
| __Rename__ | Rename within _matching_ __global__ && __local__ scope | ✅ |
| __Document Symbols__ | Shows all commands, variables, and functions in a document | ✅ |
| __Workspace Symbols__ | Shows all commands, variables, and functions in a workspace | ✅ |
| __Document Formatting__ | Formats a document, _full_ & _selection_ | ✅ |
-| __Document Highlight__ / __Semantic Token__ | Highlights all references to a command, variable, or function. | ✖ |
+| __Document Highlight__ / __Semantic Token__ | Highlights all references to a command, variable, or function. | ✅ |
| __Command Execution__ | Executes a server command from the client | ✖ |
| __Code Action__ | Shows all available code actions | ✖ |
| __Code Lens__ | Shows all available code lenses | ✖ |
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index d11ede0a..0a64ad73 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -1,57 +1,121 @@
-# Contributing
+
+# Contributing :busts_in_silhouette:
The [fish-lsp](https://fish-lsp.dev) aims to create an experience that aligns with the [fish](https://fishshell.com) language goals.
In a quick overview, the project hopes to create a development environment that is as
_friendly_ as possible.
-_Thanks for the interest in contributing to the project_ 🙏
-
+_Thanks for the interest in contributing to the project_ :pray:
---
-There are many ways to contribute to the project:
+__There are many ways to contribute to the project:__
- Submit bugs, and help work on fixes
-- __Refactor__ out unnecessary source code.
+- __Refactor__ out unnecessary source code
- Implement features, outlined in the [roadmap](./ROADMAP.md).
- Implement new client configurations, outlined in this [repo](https://github.com/ndonfris/fish-lsp-language-clients/blob/master/)
- Add [tests](https://github.com/ndonfris/fish-lsp/blob/master/test-data) to verify expected behavior.
-- Update documentation, across any of the project's repositories
+- Update __documentation__, across any of the project's repositories
-## Getting started
+## Getting started :rocket:
1. Begin by forking the project, then [build your local fork](../README.md#installation) :card_file_box:.
2. Once you have installed the local fork of the project (_i.e.,_ you have a successfully
compiled `fish-lsp` executable, and have a working [client configuration](https://github.com/ndonfris/fish-lsp-language-clients)),
-you can then begin testing locally :memo:.
+you can then begin [testing locally](#helpful-workflows) :memo:.
3. Upon completing a change, submit a [PR](https://github.com/ndonfris/fish-lsp/pulls) :tada:.
-## Places to start
+## Places to Start :checkered_flag:
-- [ROADMAP](./ROADMAP.md) - _future ideas to support_
+- [Roadmap](./ROADMAP.md) - _future ideas to support_
- [Issues and discussions](https://github.com/ndonfris/fish-lsp/discussions) - _get ideas from others_
- [Sources](https://github.com/ndonfris/fish-lsp/wiki/sources) - _helpful insight about potential features you want to adapt_
-## Workflows
+> [!NOTE]
+> Browsing both [wiki/sources#vscode-extensions-examples](https://github.com/ndonfris/fish-lsp/wiki/sources#vscode-extension-examples) and [ROADMAP](./ROADMAP.md) are the easiest method for
+> understanding how to create future [fish-lsp feature's](https://github.com/ndonfris/fish-lsp#features)
+
+## Helpful Workflows :hourglass:
+
+### Test Driven Development Workflow :hatching_chick:
+
+Since __stdin/stdout__ are reserved for the protocol to communicate, a generally
+successful method to achieve quick results, is through [TDD (Test Driven
+Development)](https://en.wikipedia.org/wiki/Test-driven_development). Many tree-sitter helper functions ([tree-sitter.ts](../src/utils/tree-sitter.ts), and [node-types.ts](../src/utils/node-types.ts)) have already been written, to
+aid in providing useful functionality for generic support of any possible
+combination need for future types.
-### TDD Workflow
+Having said that, if you a need for a new definition in [tree-sitter.ts](../src/utils/tree-sitter.ts) or [node-types.ts](../src/utils/node-types.ts) comes up,
+adding it to the proper file is fine (`tree-sitter.ts` genrally deals with movement or interacting with
+a `SyntaxNode[] | Tree`, where as `node-types.ts` generally deals with filter
+functions that can determine what __type__ of `SyntaxNode` is passed into it).
+The only requirement is that you will for new additions to these files, is that
+you include proper tests in their corresponding [test-data/{node-types,tree-sitter}.test.ts](https://github.com/ndonfris/fish-lsp/blob/master/test-data/))
-Since stdin/stdout are reserved for the protocol to communicate, a generally
-successful method to achieve quick results, is through TDD (Test Driven
-Development). Many tree-sitter helper functions have already been written, to
-aid in providing useful functionality further down the release cycle.
+
+ Sceenshot
-### Integration Testing Workflow
+![](https://github.com/ndonfris/fish-lsp/blob/d797189991cb55259d28aa43ff15b547fb454835/unit-testing.png?raw=true)
-Test directly in the client of your choosing. This is more difficult to setup,
+
+
+### Integration Testing Workflow :exploding_head:
+
+Test directly in the [client](https://github.com/ndonfris/fish-lsp-language-clients) of your choosing. _This is a more difficult to setup_,
but could be helpful if you are testing specific behaviors like the interacting
-with fish-lsp's environment variables, configuration options, handler testing or
+with [fish-lsp's environment variables](https://github.com/ndonfris/fish-lsp/?tab=readme-ov-file#environment-variables), [configuration options](https://github.com/ndonfris/fish-lsp/?tab=readme-ov-file#command-flags), handler testing or
other more specific tasks.
+
+ Screenshot
+
+![](https://github.com/ndonfris/fish-lsp/blob/d797189991cb55259d28aa43ff15b547fb454835/integration-testing.png?raw=true)
+
+
+
+### How to Build using these Workflows :building_construction:
+
+1. __Pull up__ some Documentaion :microscope:
+ - [lsif](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#languageFeatures) - The official Language Server Protocol specification
+ - [wiki/sources](https://github.com/ndonfris/fish-lsp/wiki/sources) - Sources that are similar to this project
+ - [roadmap](./ROADMAP.md) - Ideas/Documentation for future plans
+
+1. __Create__ a `file` in the [test-data/](https://github.com/ndonfris/fish-lsp/tree/master/test-data) directory :construction_worker:
+ - __START WITH VERY BASIC EXAMPLES!!!__ [Pure functions](https://en.wikipedia.org/wiki/Pure_function) are your friend
+ - Checkout [./test-data/helpers.ts](../test-data/helpers.ts), `setLogger()` which is provided for `logging` tests
+ - Test your `FILE.test.ts` with command: `yarn test FILE --watchAll`
+ - Feel free to overwrite _any existing test-file_ that is not in the
+ [package.json](../package.json) key `test-hook`
+ - Use `import { initalizeParser } from '../src/parser` for building
+ `SyntaxNode[]` [composite object](https://en.wikipedia.org/wiki/Composite_pattern) arrays (aka [trees](https://en.wikipedia.org/wiki/Tree_traversal)).
+
+1. __Iteratively__ continue improving your feature :infinity:
+ - Once you have a feature's hard coded input & outputs working as
+ expected, you can begin trying to impalement it as an actual `server.handler`
+ - You can try adding logging to your feature's specific `handlerParams`, to get an exact
+ example of it's shape. (_This is the premise outlined via:_ [integration testing workflow](#integration-testing-workflow-explodinghead))
+
+ > ```fish
+ > # display the logs
+ > tail -f $(fish-lsp info --logs-file)
+ > ```
+
+ - Alternatively, you can mock the data-type from the `vscode-languageserver`
+ or refer to the [same documentation on lsif](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#definitionParams)
+
+1. __Add__ your feature to a [server.ts](../src/server.ts) `handler` :handshake:
+ - Document your handler, if necessary.
+ - Feel free to submit your [server handler](../src/server.ts) in __seperate__ working release stages,
+ instead of trying to build entire feature's independently. (_i.e._, if your
+ `CodeAction's` only support a singular `CodeActionType`)
+ - Submit your [PR](https://github.com/ndonfris/fish-lsp/pulls) :champagne:
+
+
-## Helpful Topics and Concepts
+## Helpful Topics and Concepts :books:
[Currying](https://en.wikipedia.org/wiki/Currying) is a useful design pattern, that makes iterating through the
-[Abstract Syntax Trees (ASTs)](https://en.wikipedia.org/wiki/Abstract_syntax_tree) significantly less error prone. Earlier language server
-protocol versions required the Nodes in our tree items, to be stored as a flat list.
-Due to this reason, there has been the need for a significant rewrite of previously
-working features (diagnostics, etc...). __This would be a great place to start__, as many of
-[server](../src/server.ts) providers are implemented using range based location
-calculation's to abide to their prior protocol use.
+[Abstract Syntax Trees (ASTs)](https://en.wikipedia.org/wiki/Abstract_syntax_tree) significantly less error prone.
+
+> [!NOTE]
+> While it is still not entirely perfect, errors that appear to be caused by inconsistencies
+> in our [node-types.ts](../src/utils/node-types.ts) [functors](https://en.wikipedia.org/wiki/Functor_(functional_programming)) are more likely to be
+> caused by the earlier language server protocol versions requirement for our Nodes in our tree items, to be stored as a flat list.
+>
+> Due to this reason, the project has undergone a significant rewrite of previously
+> working features (diagnostics, etc...). __Working on reintroducing the disabled features would be a great place to start__,
+> as many of [server](../src/server.ts) providers were implemented using range based location
+> calculation's to abide to their prior protocol use.
+>
+> Relevant examples for each of the feature's mentioned above are included @ [wiki/sources](https://github.com/ndonfris/fish-lsp/wiki/sources#vscode-extension-examples)
[Child process](https://nodejs.org/api/child_process.html) execution via sub-shells. Sub-shell environment's
are extensively relied on throughout the code base.
@@ -96,19 +167,62 @@ are extensively relied on throughout the code base.
Prefetching relevant information and [caching](https://en.wikipedia.org/wiki/Cache_(computing)) it for global use.
-## Important Tooling Provided
+## Important Tooling Provided :toolbox:
+
+- [tree-sitter](https://www.google.com/search?client=firefox-b-1-d&q=web-tree-sitter) - used for data structures/algorithms, prevalent to the shell language.
+ - `yarn sh:build-wasm ` - should handle building the wasm file
+ - `@esdmr/tree-sitter-fish@3.5.1` - handles installing the actual tree-sitter-fish.wasm package
+ - `web-tree-sitter` - is the API for `SyntaxNode[]`, `Parser`, `Range`, etc...
-- [tree-sitter](https://www.google.com/search?client=firefox-b-1-d&q=web-tree-sitter) - used for data structures/algorithms, prevelant to the shell language.
- [eslint](https://eslint.org/) - used for linting and formatting
+ - `yarn lint` - lint and fix the current project (`husky pre-push` hook)
+ - `yarn lint:verbose` - lint, and display output
+
- [knip](https://github.com/webpro/knip) - used for tree-shaking and checking unused dependcies
+ - `yarn refactacor` - package.json script to run knip
+ - You can _refactor_ major sections of __unused code__ of out the project easily
+ with this command
+
- [commander.js](https://github.com/tj/commander.js) - used for [src/cli.ts](../src/cli.ts) and other tooling to start the server
+ - Handles parsing the [./bin/fish-lsp](../bin/fish-lsp) `stdin`, in a structured manor
+
+- [zod](https://github.com/colinhacks/zod) - parses the `env` into a [typesafe object](https://github.com/ndonfris/fish-lsp/blob/a41b2654cc7607993b3fd80c8560e2fdcfeca6d2/src/config.ts#L86C54-L86C55)
+ - handles _parsing_ the `fish_lsp*` variables in our __node__ `process.env` object
+ - _builds_ the result object in the global variable `config`
+
+- [vscode-languageserver](https://github.com/Microsoft/vscode-languageserver-node) - the _SPEC_ for defining our _LSP_.
+ - `Objects` & `Interfaces` specific to `fish-lsp` typically __extend__ this base
+ specification
+ - `Type Definitions` useful for handler's are defined throughout this
+ package
+
+- [husky](https://typicode.github.io/husky/) - the [git-hooks](https://github.com/ndonfris/fish-lsp/blob/a41b2654cc7607993b3fd80c8560e2fdcfeca6d2/package.json#L42) for interacting with project's source code
+ - lints the project `on-push`
+ - removes dependencies before commit `pre-commit`
+ - initializes yarn `post-merge`
+
+- [jest](https://jestjs.io/docs/configuration) - testing the project
+ - relevant locations: [test-data/*.test.ts](https://github.com/ndonfris/fish-lsp/blob/master/test-data), [jest.config.js](https://github.com/ndonfris/fish-lsp/blob/master/jest.config.js) && [tsconfig.test.json](https://github.com/ndonfris/fish-lsp/blob/master/tsconfig.test.js)
+ - `yarn test-hook` is a future supported feature, for specifying individual test files to opt into
+ successful reporting for master branch (_via_ `git action`).
+ - `yarn test someFile.test.ts --watchAll` is the designated method for watching a test's changes
-Becoming familiar with using the tree-sitter code, is significantly easier while
-using the previously mentioned __TDD__. Although, another helpful method is
-avaliable for any neovim devlepers via the `:InspectEdit` command. This will
-allow you to visualize the AST that tree-sitter parsed from fish input.
+### Other Noteworthy Tooling :hammer_and_wrench:
-## Adding support to new Langauge Clients/Editors
+Becoming familiar with using the `src/utils/{tree-sitter,node-types}.ts` code, is significantly easier while
+using the previously mentioned [TDD Workflow](#test-driven-development-workflow-hatchingchick).
+
+Using an equivalent tree-sitter visualization command to neovim's command, `:InspectEdit` is also
+highly recommended. If you are unsure what this command does, it essentially allows you to visualize
+the AST that tree-sitter parsed from fish input. Using this while writing test
+files, significantly improves the overall testing experience.
+
+Also don't forget to make use of the [fish-lsp --flags](https://github.com/ndonfris/fish-lsp/blob/master/README.md#command-flags)! There is plenty of use cases the binary supports,
+like:
`fish-lsp env --create`, `fish-lsp complete`,
+`fish-lsp info --logs-file`,
`fish-lsp info --time`, `fish-lsp url
+--sources`, `fish-lsp logger --show`, ___+ more...___
+
+## Adding New Language Clients :chart_with_upwards_trend:
Generally, all that is required is using the `fish-lsp start` command, and
specifying fish for attaching the server to a filetype. Any other fluff in this
@@ -116,15 +230,18 @@ settings, as seen in the [JSON](../README.md#client-usage) example, is only for
Adding new client configurations, to the [fish-lsp-client's](https://github.com/ndonfris/fish-lsp-language-clients/) repo, is greatly appreciated!
-## Add helpful scripts
+## Contributing Github Actions :recycle:
-[Show & tell](https://github.com/ndonfris/fish-lsp/discussions) is a helpful place to document your useful configurations for working on the fish-lsp.
+If you're trying to add a new github action to the project, please take a close
+look at the [scripts/*](https://github.com/ndonfris/fish-lsp/tree/master/scripts) directory, along with [package.json](https://github.com/ndonfris/fish-lsp/blob/master/package.json).
-Displaying demos, features and other cool discoveries is also welcomed.
+A github __action__ that uses that compiles the project, requires `fish` to be
+installed and setup, before `yarn` in the __action__.
-
+[Show & tell](https://github.com/ndonfris/fish-lsp/discussions) is a helpful place to document your useful configurations for working on the fish-lsp.
+
+Displaying demos, features and other cool discoveries are also welcome :)
diff --git a/package.json b/package.json
index 99de8241..a0ba2ea3 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"clean:all": "yarn exec rimraf out *.tsbuildinfo node_modules tree-sitter-fish.wasm",
"fresh": "yarn run clean:all && yarn install && yarn run setup",
"pretest-hook": "yarn clean:all && yarn install --ignore-scripts && yarn run sh:setup",
- "test-hook": "yarn jest document.test.ts node-types.test.ts fish-syntax-node.test.ts exec.test.ts logger.test.ts parser.test.ts completion-startup-config.test.ts document.test.ts formatting.test.ts",
+ "test-hook": "yarn jest document.test.ts tree-sitter.test.ts node-types.test.ts fish-syntax-node.test.ts exec.test.ts logger.test.ts parser.test.ts snippets.test.ts completion-startup-config.test.ts formatting.test.ts",
"test": "jest",
"compile": "yarn tsc -b",
"watch": "yarn tsc -b -w",
@@ -46,6 +46,14 @@
"post-merge": "yarn"
}
},
+ "contributes": {
+ "commands": [
+ {
+ "command": "fish-lsp.executeLine",
+ "title": "execute the line under the cursor"
+ }
+ ]
+ },
"dependencies": {
"@esdmr/tree-sitter-fish": "^3.5.1",
"colors": "^1.4.0",
@@ -91,4 +99,4 @@
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2"
}
-}
+}
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index de3c8f45..d5e07aa7 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -255,7 +255,7 @@ export function adjustInitializeResultCapabilitiesFromConfig(configHandlers: z.i
resolveProvider: true,
} : undefined,
executeCommandProvider: configHandlers.executeCommand ? {
- commands: ['APPLY_REFACTORING', 'SELECT_REFACTORING', 'APPLY_WORKSPACE_EDIT', 'RENAME', 'onHover', 'rename'],
+ commands: ['APPLY_REFACTORING', 'SELECT_REFACTORING', 'APPLY_WORKSPACE_EDIT', 'RENAME', 'onHover', 'rename', 'fish-lsp.executeLine'],
workDoneProgress: true,
} : undefined,
documentSymbolProvider: {
@@ -264,13 +264,10 @@ export function adjustInitializeResultCapabilitiesFromConfig(configHandlers: z.i
workspaceSymbolProvider: {
resolveProvider: true,
},
- documentHighlightProvider: false,
+ documentHighlightProvider: configHandlers.highlight,
inlayHintProvider: false, /*configHandlers.inlayHint,*/
- signatureHelpProvider: configHandlers.signature ? {
- retriggerCharacters: ['.'],
- triggerCharacters: ['.', ' '],
- } : undefined,
+ signatureHelpProvider: configHandlers.signature ? {workDoneProgress: false, triggerCharacters: ['.']} : undefined,
},
};
-}
+}
\ No newline at end of file
diff --git a/src/document-highlight.ts b/src/document-highlight.ts
new file mode 100644
index 00000000..97ea0b28
--- /dev/null
+++ b/src/document-highlight.ts
@@ -0,0 +1,45 @@
+
+import { toSymbolKind } from './utils/translation';
+import { equalRanges, getNodeAtPosition, getRange } from './utils/tree-sitter';
+import { DocumentHighlight, DocumentHighlightKind } from 'vscode-languageserver';
+import Parser, { SyntaxNode, Tree } from 'web-tree-sitter';
+
+/**
+ * TODO:
+ * ADD DocumentHighlightKind.Read | DocumentHighlightKind.Write support
+ */
+export function getDocumentHighlights(tree: Tree, node: SyntaxNode): DocumentHighlight[] {
+ const highlights: DocumentHighlight[] = [];
+
+ const nodeSymbolKind = toSymbolKind(node)
+
+
+ function visitNode(currentNode: SyntaxNode) {
+ if (!currentNode) return;
+
+ const currSymbolKind = toSymbolKind(currentNode)
+ const equalKinds = (currSymbolKind === nodeSymbolKind || currentNode.type === node.type)
+ if (equalKinds && currentNode.text === node.text) {
+ highlights.push({
+ range: {
+ start: {
+ line: currentNode.startPosition.row,
+ character: currentNode.startPosition.column,
+ },
+ end: {
+ line: currentNode.endPosition.row,
+ character: currentNode.endPosition.column,
+ },
+ },
+ // kind: DocumentHighlightKind.Text,
+ kind: equalRanges(getRange(currentNode), getRange(node))
+ ? DocumentHighlightKind.Read
+ : DocumentHighlightKind.Text
+ });
+ }
+ currentNode.children.forEach(child => visitNode(child));
+ }
+
+ visitNode(tree.rootNode);
+ return highlights;
+}
diff --git a/src/executeHandler.ts b/src/executeHandler.ts
new file mode 100644
index 00000000..1adf477b
--- /dev/null
+++ b/src/executeHandler.ts
@@ -0,0 +1,19 @@
+import { TextDocumentPositionParams, Connection } from 'vscode-languageserver';
+import {exec} from 'child_process'
+
+ export function execRequest(connection: Connection, line: string) {
+ // Here you would execute the current line in the parent shell environment
+ // For example, you could use Node.js's child_process to execute the command
+ exec(line, (error: any, stdout: any, stderr: any) => {
+ if (error) {
+ connection.window.showErrorMessage(`Error: ${error.message}`);
+ return;
+ }
+ if (stderr) {
+ connection.window.showErrorMessage(`Stderr: ${stderr}`);
+ return;
+ }
+ connection.window.showInformationMessage(`Executed: ${line}\nOutput: ${stdout}`);
+ });
+}
+
diff --git a/src/hover.ts b/src/hover.ts
index 73d9ec40..115bb680 100644
--- a/src/hover.ts
+++ b/src/hover.ts
@@ -6,8 +6,10 @@ import { LspDocument } from './document';
import { documentationHoverProvider, enrichCommandWithFlags } from './documentation';
import { DocumentationCache } from './utils/documentationCache';
import { execCommandDocs, execCompletions, execSubCommandCompletions } from './utils/exec';
-import { isCommand } from './utils/node-types';
+import { isCommand, isFunctionDefinition, isOption } from './utils/node-types';
import { findFirstParent } from './utils/tree-sitter';
+import { symbolKindToString, symbolKindsFromNode, toSymbolKind } from './utils/translation';
+import { Logger } from './logger';
export async function handleHover(
analyzer: Analyzer,
@@ -15,8 +17,9 @@ export async function handleHover(
position: LSP.Position,
current: Parser.SyntaxNode,
cache: DocumentationCache,
+ logger?: Logger,
): Promise {
- if (current.text.startsWith('-')) {
+ if (isOption(current)) {
return await getHoverForFlag(current);
}
const local = analyzer.getDefinition(document, position);
@@ -29,10 +32,16 @@ export async function handleHover(
range: local.selectionRange,
};
}
+ let { kindType, kindString } = symbolKindsFromNode(current)
+ const symbolType = ['function', 'class', 'variable'].includes(kindString) ? kindType : undefined
+ logger?.log({'./src/hover.ts:37': kindType})
+
if (cache.find(current.text) !== undefined) {
- await cache.resolve(current.text);
- const item = cache.getItem(current.text);
- if (item?.docs) {
+ await cache.resolve(current.text, document.uri, symbolType);
+ const item = !!symbolType ? cache.find(current.text, symbolType) : cache.getItem(current.text);
+ logger?.logAsJson('call: [./src/hover.ts:42]')
+
+ if (item && item?.docs) {
return {
contents: {
kind: MarkupKind.Markdown,
@@ -42,11 +51,14 @@ export async function handleHover(
}
}
const commandString = await collectCommandString(current);
- return await documentationHoverProvider(commandString);
+
+ const result = await documentationHoverProvider(commandString);
+ logger?.log({commandString, result})
+ return result
}
export async function getHoverForFlag(current: Parser.SyntaxNode): Promise {
- const commandNode = findFirstParent(current, n => isCommand(n));
+ const commandNode = findFirstParent(current, n => isCommand(n) || isFunctionDefinition(n));
if (!commandNode) {
return null;
}
@@ -127,4 +139,4 @@ export async function collectCommandString(current: Parser.SyntaxNode): Promise<
return commandText;
}
return commandNodeText || '';
-}
+}
\ No newline at end of file
diff --git a/src/server.ts b/src/server.ts
index 8290daa5..c5d11619 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -2,13 +2,14 @@ import Parser, { SyntaxNode } from 'web-tree-sitter';
import { initializeParser } from './parser';
import { Analyzer } from './analyze';
//import { generateCompletionList, } from "./completion";
-import { InitializeParams, TextDocumentSyncKind, CompletionParams, Connection, CompletionList, CompletionItem, MarkupContent, DocumentSymbolParams, DefinitionParams, Location, ReferenceParams, DocumentSymbol, DidOpenTextDocumentParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidSaveTextDocumentParams, InitializeResult, HoverParams, Hover, RenameParams, TextDocumentPositionParams, TextDocumentIdentifier, WorkspaceEdit, TextEdit, DocumentFormattingParams, CodeActionParams, CodeAction, DocumentRangeFormattingParams, FoldingRangeParams, FoldingRange, InlayHintParams, MarkupKind, WorkspaceSymbolParams, WorkspaceSymbol, SymbolKind, CompletionTriggerKind, SignatureHelpParams, SignatureHelp, MessageType, NotificationType } from 'vscode-languageserver';
+import { InitializeParams, TextDocumentSyncKind, CompletionParams, Connection, CompletionList, CompletionItem, MarkupContent, DocumentSymbolParams, DefinitionParams, Location, ReferenceParams, DocumentSymbol, DidOpenTextDocumentParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidSaveTextDocumentParams, InitializeResult, HoverParams, Hover, RenameParams, TextDocumentPositionParams, TextDocumentIdentifier, WorkspaceEdit, TextEdit, DocumentFormattingParams, CodeActionParams, CodeAction, DocumentRangeFormattingParams, FoldingRangeParams, FoldingRange, InlayHintParams, MarkupKind, WorkspaceSymbolParams, WorkspaceSymbol, SymbolKind, CompletionTriggerKind, SignatureHelpParams, SignatureHelp, MessageType, NotificationType, DocumentHighlight, DocumentHighlightParams, ExecuteCommandParams } from 'vscode-languageserver';
+import {execRequest} from './executeHandler'
import * as LSP from 'vscode-languageserver';
import { LspDocument, LspDocuments } from './document';
import { formatDocumentContent } from './formatting';
import { Logger, ServerLogsPath } from './logger';
-import { uriToPath } from './utils/translation';
-import { findFirstParent, getChildNodes, getRange } from './utils/tree-sitter';
+import { pathToUri, symbolKindToString, symbolKindsFromNode, toSymbolKind, uriToPath } from './utils/translation';
+import { findFirstParent, getChildNodes, getNodeAtPosition, getRange } from './utils/tree-sitter';
import { handleHover } from './hover';
import { /*getDiagnostics*/ } from './diagnostics/validate';
import { CodeActionKind } from './code-action';
@@ -30,9 +31,13 @@ import { getDocumentationResolver } from './utils/completion/documentation';
import { FishCompletionList } from './utils/completion/list';
import { config } from './cli';
import { PrebuiltDocumentationMap, getPrebuiltDocUrl, getPrebuiltDocUrlByName } from './utils/snippets';
-import { isCommand, isVariableDefinition, isVariableDefinitionCommand } from './utils/node-types';
+import { findParentCommand, isCommand, isCommandName, isVariableDefinition, isVariableDefinitionCommand } from './utils/node-types';
import { adjustInitializeResultCapabilitiesFromConfig, configHandlers } from './config';
import { enrichToMarkdown } from './documentation';
+import { getAliasedCompletionItemSignature, lineSignatureBuilder } from './signature';
+import { CompletionItemMap } from './utils/completion/startup-cache';
+import { getDocumentHighlights } from './document-highlight';
+import { pathToFileURL } from 'url';
// @TODO
export type SupportedFeatures = {
@@ -46,11 +51,13 @@ export default class FishServer {
): Promise {
const documents = new LspDocuments();
const logger = new Logger(config.fish_lsp_logfile || ServerLogsPath, true, connection.console);
+ const completionsMap = await CompletionItemMap.initialize()
+
return await Promise.all([
initializeParser(),
initializeDocumentationCache(),
initializeDefaultFishWorkspaces(),
- initializeCompletionPager(logger),
+ initializeCompletionPager(logger, completionsMap),
]).then(([parser, cache, workspaces, completions]) => {
const analyzer = new Analyzer(parser, workspaces);
return new FishServer(
@@ -59,6 +66,7 @@ export default class FishServer {
analyzer,
documents,
completions,
+ completionsMap,
cache,
logger,
);
@@ -75,6 +83,7 @@ export default class FishServer {
private analyzer: Analyzer,
private docs: LspDocuments,
private completion: CompletionPager,
+ private completionMap: CompletionItemMap,
private documentationCache: DocumentationCache,
protected logger: Logger,
) {
@@ -111,8 +120,10 @@ export default class FishServer {
connection.onCodeAction(this.onCodeAction.bind(this));
connection.onFoldingRanges(this.onFoldingRanges.bind(this));
//this.connection.workspace.applyEdit()
+ connection.onDocumentHighlight(this.onDocumentHighlight.bind(this));
connection.languages.inlayHint.on(this.onInlayHints.bind(this));
connection.onSignatureHelp(this.onShowSignatureHelp.bind(this));
+ connection.onExecuteCommand(this.onExecuteCommand.bind(this));
connection.console.log('FINISHED FishLsp.register()');
}
@@ -271,6 +282,48 @@ export default class FishServer {
);
}
+ public onExecuteCommand(params: ExecuteCommandParams) {
+ this.logParams('onExecuteCommand', params);
+
+ const file = params.arguments![0] as string || ''
+ const line = params.arguments![1] as string || ''
+ // console.log({'last accessed: ': this.docs.files})
+
+ if (!file || !line) {
+ this.logger.log({gotNull: 'gotNull', file, line})
+ return null
+ }
+
+ const doc = this.docs.get(file)
+
+ if (!doc) {
+ this.logger.log({title: 'docs was null', doc})
+ return []
+ }
+ const text = doc.getLine(Number.parseInt(line)-1)
+ this.logParams('onExecuteCommand', text)
+ execRequest(this.connection, text)
+ return null
+ }
+
+ /**
+ * highlight provider
+ */
+ onDocumentHighlight(params: DocumentHighlightParams): DocumentHighlight[] {
+ this.logParams('onDocumentHighlight', params)
+ const { doc } = this.getDefaults(params)
+ if (!doc) return []
+
+ const text = doc.getText();
+ const tree = this.parser.parse(text);
+ const node = getNodeAtPosition(tree, params.position);
+ if (!node) return []
+
+ const highlights = getDocumentHighlights(tree, node);
+
+ return highlights
+ }
+
async onWorkspaceSymbol(params: WorkspaceSymbolParams): Promise {
this.logParams('onWorkspaceSymbol', params.query);
@@ -296,9 +349,7 @@ export default class FishServer {
return getRefrenceLocations(this.analyzer, doc, params.position);
}
- // opens package.json on hover of document symbol!
- //
- // NEED TO REMOVE documentationCache. It works but is too expensive memory wise.
+ // Probably should move away from `documentationCache`. It works but is too expensive memory wise.
// REFACTOR into a procedure that conditionally determines output type needed.
// Also plan to get rid of any other cache's, so that the garbage collector can do its job.
async onHover(params: HoverParams): Promise {
@@ -307,7 +358,10 @@ export default class FishServer {
if (!doc || !uri || !root || !current) {
return null;
}
- // this.logger.log({ current: current.text });
+
+
+ let { kindType, kindString } = symbolKindsFromNode(current)
+ this.logger.log({ currentText: current.text, currentType: current.type, symbolKind: kindString });
const prebuiltSkipType = [
...PrebuiltDocumentationMap.getByType('pipe'),
@@ -328,12 +382,21 @@ export default class FishServer {
].join('\n')),
};
}
+ const symbolType = [
+ 'function',
+ 'class',
+ 'variable',
+ ].includes(kindString) ? kindType : undefined
+
const globalItem = await this.documentationCache.resolve(
current.text.trim(),
uri,
+ symbolType
);
- this.logger.logAsJson('docCache found ' + globalItem?.resolved.toString() || `docCache not found ${current.text}`);
+
+ this.logger.log({'./src/server.ts:395': `this.documentationCache.resolve() found ${!!globalItem}` , docs: globalItem.docs});
if (globalItem && globalItem.docs) {
+ this.logger.log(globalItem.docs)
return {
contents: {
kind: MarkupKind.Markdown,
@@ -341,13 +404,16 @@ export default class FishServer {
},
};
}
- return await handleHover(
+ const fallbackHover = await handleHover(
this.analyzer,
doc,
params.position,
current,
this.documentationCache,
+ // this.logger,
);
+ this.logger.log(fallbackHover?.contents);
+ return fallbackHover
}
// workspace.fileOperations.didRename
@@ -546,29 +612,41 @@ export default class FishServer {
const { doc, uri } = this.getDefaults(params);
if (!doc || !uri) return null;
+
const { line, lineRootNode, lineLastNode } = this.analyzer.parseCurrentLine(doc, params.position);
- const varNode = getChildNodes(lineRootNode).find(c => isVariableDefinition(c));
- const lastCmd = getChildNodes(lineRootNode).filter(c => isCommand(c)).pop();
- this.logger.log({ line, lastCmds: lastCmd?.text });
- if (varNode && (line.startsWith('set') || line.startsWith('read')) && lastCmd?.text === lineRootNode.text.trim()) {
- const varName = varNode.text;
- const varDocs = PrebuiltDocumentationMap.getByName(varNode.text);
- if (!varDocs.length) return null;
- return {
- signatures: [
- {
- label: varName,
- documentation: {
- kind: 'markdown',
- value: varDocs.map(d => d.description).join('\n'),
- },
- },
- ],
- activeSignature: 0,
- activeParameter: 0,
- };
- }
- return null;
+ if (line.trim() === '') return null
+ const currentCmd = findParentCommand(lineLastNode)!
+ // const commands = getChildNodes(lineRootNode).filter(isCommand)
+ const aliasSignature = this.completionMap.allOfKinds('alias').find(a => a.label === currentCmd.text)
+ if (aliasSignature) return getAliasedCompletionItemSignature(aliasSignature)
+ return lineSignatureBuilder(currentCmd, lineLastNode, this.completionMap)
+ // if (currentCmd.text.startsWith('string') || commands.length > 1) {
+ // }
+ //
+ // return lineSignatureBuilder(lineRootNode, lineLastNode)
+ //
+ // const varNode = getChildNodes(lineRootNode).find(c => isVariableDefinition(c));
+ // const lastCmd = getChildNodes(lineRootNode).filter(c => isCommand(c)).pop();
+ // this.logger.log({ line, lastCmds: lastCmd?.text });
+ // if (varNode && (line.startsWith('set') || line.startsWith('read')) && lastCmd?.text === lineRootNode.text.trim()) {
+ // const varName = varNode.text;
+ // const varDocs = PrebuiltDocumentationMap.getByName(varNode.text);
+ // if (!varDocs.length) return null;
+ // return {
+ // signatures: [
+ // {
+ // label: varName,
+ // documentation: {
+ // kind: 'markdown',
+ // value: varDocs.map(d => d.description).join('\n'),
+ // },
+ // },
+ // ],
+ // activeSignature: 0,
+ // activeParameter: 0,
+ // };
+ // }
+ // return null;
}
/////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/signature.ts b/src/signature.ts
index 7fc10f6e..1ebc03f5 100644
--- a/src/signature.ts
+++ b/src/signature.ts
@@ -5,11 +5,143 @@ import {
SignatureHelpParams,
Command,
} from 'vscode-languageserver';
-//import { FishCompletionItemKind } from './utils/completion-strategy';
-//import { stringRegexExpressions} from './utils/completion-types';
-import { isBuiltin } from './utils/builtins';
+import { SyntaxNode } from 'web-tree-sitter';
+import { ExtendedBaseJson, PrebuiltDocumentationMap } from './utils/snippets';
+import { FishAliasCompletionItem } from './utils/completion/types';
+import * as NodeTypes from './utils/node-types'
+import * as TreeSitter from './utils/tree-sitter'
+import { CompletionItemMap } from './utils/completion/startup-cache';
+
+export function buildSignature(label: string, value: string) : SignatureInformation {
+ return {
+ label: label,
+ documentation: {
+ kind: 'markdown',
+ value: value,
+ },
+ }
+}
+
+export function getCurrentNodeType(input: string) {
+ const prebuiltTypes = PrebuiltDocumentationMap.getByName(input)
+ if (!prebuiltTypes || prebuiltTypes.length === 0) {
+ return null
+ }
+ let longestDocs = prebuiltTypes[0]!
+ for (const prebuilt of prebuiltTypes) {
+ if (prebuilt.description.length > longestDocs.description.length) {
+ longestDocs = prebuilt
+ }
+ }
+ return longestDocs
+}
+
+export function lineSignatureBuilder(lineRootNode: SyntaxNode, lineCurrentNode: SyntaxNode, completeMmap: CompletionItemMap): SignatureHelp | null {
+ const currentCmd = NodeTypes.findParentCommand(lineCurrentNode) || lineRootNode;
+ const pipes = getPipes(lineRootNode);
+ const varNode = getVariableNode(lineRootNode);
+ const allCmds = getAllCommands(lineRootNode);
+ const regexOption = getRegexOption(lineRootNode);
+
+ if (pipes.length === 1) return getPipesSignature(pipes);
-function regexStringSignature() : SignatureInformation {
+ switch (true) {
+ case isStringWithRegex(currentCmd.text, regexOption):
+ return getDefaultSignatures();
+
+ case varNode && isSetOrReadWithVarNode(currentCmd?.text || lineRootNode.text, varNode, lineRootNode, allCmds):
+ return getSignatureForVariable(varNode);
+
+ case currentCmd?.text.startsWith('return') || lineRootNode.text.startsWith('return'):
+ return getReturnStatusSignature();
+
+ case allCmds.length === 1:
+ return getCommandSignature(currentCmd);
+
+ default:
+ return null;
+ }
+}
+
+export function getPipes(rootNode: SyntaxNode): ExtendedBaseJson[] {
+ const pipeNames = PrebuiltDocumentationMap.getByType('pipe');
+ return TreeSitter.getChildNodes(rootNode).reduce((acc: ExtendedBaseJson[], node) => {
+ const pipe = pipeNames.find(p => p.name === node.text);
+ if (pipe) acc.push(pipe);
+ return acc;
+ }, []);
+}
+
+function getVariableNode(rootNode: SyntaxNode): SyntaxNode | undefined {
+ return TreeSitter.getChildNodes(rootNode).find(c => NodeTypes.isVariableDefinition(c));
+}
+
+function getAllCommands(rootNode: SyntaxNode): SyntaxNode[] {
+ return TreeSitter.getChildNodes(rootNode).filter(c => NodeTypes.isCommand(c));
+}
+
+function getRegexOption(rootNode: SyntaxNode): SyntaxNode | undefined {
+ return TreeSitter.getChildNodes(rootNode).find(n => NodeTypes.isMatchingOption(n, { shortOption: '-r', longOption: '--regex' }));
+}
+
+function isStringWithRegex(line: string, regexOption: SyntaxNode | undefined): boolean {
+ return line.startsWith('string') && !!regexOption;
+}
+
+function isSetOrReadWithVarNode(line: string, varNode: SyntaxNode | undefined, rootNode: SyntaxNode, allCmds: SyntaxNode[]): boolean {
+ return !!varNode && (line.startsWith('set') || line.startsWith('read')) && allCmds.pop()?.text === rootNode.text.trim();
+}
+
+function getSignatureForVariable(varNode: SyntaxNode): SignatureHelp | null {
+ const output = getCurrentNodeType(varNode.text);
+ if (!output) return null;
+ return {
+ signatures: [buildSignature(output.name, output.description)],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+function getReturnStatusSignature(): SignatureHelp {
+ const output = PrebuiltDocumentationMap.getByType('status').map((o: ExtendedBaseJson) => `___${o.name}___ - _${o.description}_`).join('\n')
+ return {
+ signatures: [buildSignature('$status', output)],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+function getPipesSignature(pipes: ExtendedBaseJson[]): SignatureHelp {
+ return {
+ signatures: pipes.map((o: ExtendedBaseJson) => buildSignature(o.name, `${o.name} - _${o.description}_`)),
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+function getCommandSignature(firstCmd: SyntaxNode): SignatureHelp {
+ const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text);
+ return {
+ signatures: [buildSignature(firstCmd.text, output.map((o: ExtendedBaseJson) => `${o.name} - _${o.description}_`).join('\n'))],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+export function getAliasedCompletionItemSignature(item: FishAliasCompletionItem): SignatureHelp {
+ // const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text);
+ return {
+ signatures: [buildSignature(item.label, [
+ '```fish',
+ `${item.fishKind} ${item.label} ${item.detail}`,
+ '```'
+ ].join('\n'))],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+export function regexStringSignature() : SignatureInformation {
//const regexItems = stringRegexExpressions;
//let signatureDoc = ["__String Regex Patterns__", "---"];
//for (const item of regexItems) {
@@ -108,4 +240,4 @@ const markdownStringGroups = [
'- __(?:...)__ is a non-capturing group',
'- __\\n__ is a backreference (where n is the number of the group, starting with 1)',
'- __$n__ is a reference from the replacement expression to a group in the match expression.',
-].join('\n');
+].join('\n');
\ No newline at end of file
diff --git a/src/utils/completion/pager.ts b/src/utils/completion/pager.ts
index 88370b54..edf171c4 100644
--- a/src/utils/completion/pager.ts
+++ b/src/utils/completion/pager.ts
@@ -140,13 +140,9 @@ export class CompletionPager {
}
}
-export async function initializeCompletionPager(logger: Logger) {
- return await Promise.all([
- InlineParser.create(),
- CompletionItemMap.initialize(),
- ]).then(([inline, items]) => {
- return new CompletionPager(inline, items, logger);
- });
+export async function initializeCompletionPager(logger: Logger, items: CompletionItemMap) {
+ const inline = await InlineParser.create()
+ return new CompletionPager(inline, items, logger);
}
function addFirstIndexedItems(command: string, items: CompletionItemMap) {
@@ -375,4 +371,4 @@ function addItemTypes(line: string, parser: InlineParser): FishCompletionItemKin
break;
}
return [];
-}
+}
\ No newline at end of file
diff --git a/src/utils/completion/startup-cache.ts b/src/utils/completion/startup-cache.ts
index c93c284a..1abcc59f 100644
--- a/src/utils/completion/startup-cache.ts
+++ b/src/utils/completion/startup-cache.ts
@@ -7,9 +7,8 @@ import { SetupItemsFromCommandConfig } from './startup-config';
export type ItemMapRecord = Record;
export class CompletionItemMap {
- constructor(
- private _items: ItemMapRecord = {} as ItemMapRecord,
- ) {}
+
+ constructor(private _items: ItemMapRecord = {} as ItemMapRecord) {}
static async initialize(): Promise {
const result: ItemMapRecord = {} as ItemMapRecord;
@@ -19,6 +18,7 @@ export class CompletionItemMap {
const stdout = await execCmd(item.command);
cmdOutputs.set(item.fishKind, stdout);
}));
+
SetupItemsFromCommandConfig.forEach((item) => {
const items: FishCompletionItem[] = [];
const stdout = cmdOutputs.get(item.fishKind)!;
@@ -36,12 +36,13 @@ export class CompletionItemMap {
const detail = getCommandsDetail(value || item.detail);
items.push(FishCompletionItem.create(label, item.fishKind, detail, line));
});
- result[item.fishKind] = items;
+ result[ item.fishKind ] = items;
});
- Object.entries(StaticItems).forEach(([key, value]) => {
+
+ Object.entries(StaticItems).forEach(([ key, value ]) => {
const kind = key as FishCompletionItemKind;
- if (!result[kind]) {
- result[kind] = value.map((item) => FishCompletionItem.create(
+ if (!result[ kind ]) {
+ result[ kind ] = value.map((item) => FishCompletionItem.create(
item.label,
kind,
item.detail,
@@ -50,23 +51,30 @@ export class CompletionItemMap {
));
}
});
+
return new CompletionItemMap(result);
}
+
get(kind: FishCompletionItemKind): FishCompletionItem[] {
- return this._items[kind] || [];
+ return this._items[ kind ] || [];
}
+
get allKinds(): FishCompletionItemKind[] {
return Object.keys(this._items) as FishCompletionItemKind[];
}
+
allOfKinds(...kinds: FishCompletionItemKind[]): FishCompletionItem[] {
return kinds.reduce((acc, kind) => acc.concat(this.get(kind)), [] as FishCompletionItem[]);
}
- entries(): [FishCompletionItemKind, FishCompletionItem[]][] {
- return Object.entries(this._items) as [FishCompletionItemKind, FishCompletionItem[]][];
+
+ entries(): [ FishCompletionItemKind, FishCompletionItem[] ][] {
+ return Object.entries(this._items) as [ FishCompletionItemKind, FishCompletionItem[] ][];
}
+
forEach(callbackfn: (key: FishCompletionItemKind, value: FishCompletionItem[]) => void) {
- this.entries().forEach(([key, value]) => callbackfn(key, value));
+ this.entries().forEach(([ key, value ]) => callbackfn(key, value));
}
+
allCompletionsWithoutCommand() {
return this.allOfKinds(
FishCompletionItemKind.ABBR,
@@ -77,6 +85,7 @@ export class CompletionItemMap {
//FishCompletionItemKind.VARIABLE,
);
}
+
findLabel(label: string, ...searchKinds: FishCompletionItemKind[]): FishCompletionItem | undefined {
const kinds: FishCompletionItemKind[] = searchKinds?.length > 0 ? searchKinds : this.allKinds;
for (const kind of kinds) {
diff --git a/src/utils/node-types.ts b/src/utils/node-types.ts
index bad3b26e..0396b1de 100644
--- a/src/utils/node-types.ts
+++ b/src/utils/node-types.ts
@@ -221,10 +221,48 @@ export function isLongOption(node: SyntaxNode): boolean {
export function isShortOption(node: SyntaxNode): boolean {
return node.text.startsWith('-') && !isLongOption(node);
}
+
export function isOption(node: SyntaxNode): boolean {
return isShortOption(node) || isLongOption(node);
}
+/** careful not to call this on old unix style flags/options */
+export function isJoinedShortOption(node: SyntaxNode) {
+ if (isLongOption(node)) return false
+ return isShortOption(node) && node.text.slice(1).length > 1
+}
+
+/** careful not to call this on old unix style flags/options */
+export function hasShortOptionCharacter(node: SyntaxNode, findChar: string) {
+ if (isLongOption(node)) return false
+ return isShortOption(node) && node.text.slice(1).includes(findChar)
+}
+
+export type NodeOptionQueryText = {
+ shortOption?: `-${string}`
+ oldUnixOption?: `-${string}`,
+ longOption?: `--${string}`
+}
+
+/**
+ * @param node - the node to check
+ * @param optionQuery - object of node strings to match
+ * @returns boolean result corresponding to query
+ */
+export function isMatchingOption(node: SyntaxNode, optionQuery: NodeOptionQueryText) : boolean {
+ if (!isOption(node)) return false
+
+ const nodeText = node.text.includes('=') ? node.text.slice(0, node.text.indexOf('=')) : node.text
+
+ if (isLongOption(node) && optionQuery?.longOption === nodeText) return true
+
+ if (isShortOption(node) && optionQuery?.oldUnixOption === nodeText) return true
+
+ if (!optionQuery.shortOption) return false
+ return isShortOption(node) && hasShortOptionCharacter(node, optionQuery.shortOption.slice(1))
+
+}
+
export function isPipe(node: SyntaxNode): boolean {
return node.type === 'pipe';
}
@@ -687,3 +725,29 @@ export function isPartialForLoop(node: SyntaxNode) {
!errorNode.text.includes(' in ')
);
}
+
+export function isInlineComment(node: SyntaxNode) {
+ if (!isComment(node)) return false;
+ const previousSibling: SyntaxNode | undefined | null = node.previousNamedSibling
+ if (!previousSibling) return false;
+ return previousSibling?.startPosition.row === node.startPosition.row && previousSibling?.type !== 'comment'
+}
+
+// TODO: either move use or remove
+// /**
+// * checks for SyntaxNode.text === '-f1' | '--fields=1'
+// * but not SyntaxNode.text !== '-1' | '-m1f1' | '--fields-1'
+// */
+// export function isOptionWithValue(node: SyntaxNode) {
+// if (!isOption(node)) return false
+// // must be option
+//
+// if (isShortOption(node)) {
+// const lastChar = node.text.charAt(2) || ''
+// return Number.isInteger(Number.parseInt(lastChar));
+// } else if (isLongOption(node)) {
+// return node.text.includes('=')
+// }
+// return false
+// }
+//
diff --git a/src/utils/snippets.ts b/src/utils/snippets.ts
index 8e9438a5..62fb9287 100644
--- a/src/utils/snippets.ts
+++ b/src/utils/snippets.ts
@@ -20,7 +20,7 @@ interface BaseJson {
type JsonType = 'command' | 'pipe' | 'status' | 'variable';
type SpecialType = 'fishlsp' | 'env' | 'locale' | 'special' | 'theme';
-interface ExtendedBaseJson extends BaseJson {
+export interface ExtendedBaseJson extends BaseJson {
type: JsonType;
specialType: SpecialType | undefined;
// otherTypes: string[]; //TODO
diff --git a/src/utils/translation.ts b/src/utils/translation.ts
index 26bf35e3..e9c530e3 100644
--- a/src/utils/translation.ts
+++ b/src/utils/translation.ts
@@ -3,7 +3,7 @@ import * as LSP from 'vscode-languageserver';
import * as TreeSitter from 'web-tree-sitter';
import { SyntaxNode } from 'web-tree-sitter';
import { URI } from 'vscode-uri';
-import { findParentVariableDefintionKeyword, isCommand, isCommandName, isComment, isFunctionDefinition, isFunctionDefinitionName, isProgram, isScope, isStatement, isString, isVariableDefinition } from './node-types';
+import { findParentVariableDefintionKeyword, isCommand, isCommandName, isComment, isFunctionDefinition, isFunctionDefinitionName, isProgram, isScope, isStatement, isString, isVariable, isVariableDefinition } from './node-types';
import { LspDocument, LspDocuments } from '../document';
import { FishProtocol } from './fishProtocol';
import { getPrecedingComments, getRange, getRangeWithPrecedingComments } from './tree-sitter';
@@ -210,7 +210,7 @@ export function toLspDocument(filename: string, content: string): LspDocument {
}
export function toSymbolKind(node: SyntaxNode): SymbolKind {
- if (isVariableDefinition(node)) {
+ if (isVariable(node)) {
return SymbolKind.Variable;
} else if (isFunctionDefinitionName(node)) { // change from isFunctionDefinition(node)
return SymbolKind.Function;
@@ -245,3 +245,22 @@ export function symbolKindToString(kind: SymbolKind) {
return 'other';
}
}
+
+
+/**
+ * @param node - SyntaxNode toSymbolKind/symbolKindToString wrapper for both
+ * `string` and `number` type
+ * @returns {
+ * kindType: toSymbolKind(node) -> 13 | 12 | 15 | 3 | 5 | 21
+ * kindString: symbolKindToString(kindType) -> number
+ * }
+ */
+export function symbolKindsFromNode(node: SyntaxNode) {
+ const kindType = toSymbolKind(node);
+ const kindString = symbolKindToString(kindType)
+ return {
+ kindType,
+ kindString
+ }
+
+}
\ No newline at end of file
diff --git a/src/utils/tree-sitter.ts b/src/utils/tree-sitter.ts
index fbf28b19..e129bd12 100644
--- a/src/utils/tree-sitter.ts
+++ b/src/utils/tree-sitter.ts
@@ -3,9 +3,9 @@ import { extname, join } from 'path';
//import { pathToFileURL, URL } from 'url'
import { Position, Range, URI } from 'vscode-languageserver';
import { Point, SyntaxNode, Tree } from 'web-tree-sitter';
-import { pathToFileURL } from 'url'; // typescript-langauge-server -> https://github.com/typescript-language-server/typescript-language-server/blob/master/src/document.ts
-import vscodeUri from 'vscode-uri'; // typescript-langauge-server -> https://github.com/typescript-language-server/typescript-language-server/blob/master/src/document.ts
-import { existsSync } from 'fs-extra';
+// import { pathToFileURL } from 'url'; // typescript-langauge-server -> https://github.com/typescript-language-server/typescript-language-server/blob/master/src/document.ts
+// import vscodeUri from 'vscode-uri'; // typescript-langauge-server -> https://github.com/typescript-language-server/typescript-language-server/blob/master/src/document.ts
+// import { existsSync } from 'fs-extra';
import { findSetDefinedVariable, findParentCommand, isFunctionDefinition, isVariableDefinition, isFunctionDefinitionName, isVariable, isScope, isProgram, isCommandName, isForLoop, findForLoopVariable } from './node-types';
/**
@@ -106,7 +106,7 @@ export function findFirstParent(node: SyntaxNode, predicate: (node: SyntaxNode)
*/
export function getSiblingNodes(
node: SyntaxNode,
- predicate : (n: SyntaxNode) => true,
+ predicate : (n: SyntaxNode) => boolean,
direction: 'before' | 'after' = 'before',
): SyntaxNode[] {
const siblingFunc = (n: SyntaxNode) =>
@@ -151,7 +151,7 @@ export function findFirstSibling(
direction === 'before' ? n.previousSibling : n.nextSibling;
let current: SyntaxNode | null = node;
while (current) {
- console.log('curr: ', current.text);
+ // console.log('curr: ', current.text);
current = siblingFunc(current);
if (current && predicate(current)) {
return current;
@@ -606,3 +606,6 @@ export function getCommandArgumentValue(command: SyntaxNode, argName: string): S
//
// return result
//}
+export function getNodeAtPosition(tree: Tree, position: { line: number; character: number }): SyntaxNode | null {
+ return tree.rootNode.descendantForPosition({ row: position.line, column: position.character });
+}
diff --git a/test-data/fish_files/umask.fish b/test-data/fish_files/umask.fish
index 779dc436..69ca0592 100644
--- a/test-data/fish_files/umask.fish
+++ b/test-data/fish_files/umask.fish
@@ -182,4 +182,4 @@ function umask --description "Set default file permission mask"
printf (_ '%s: Too many arguments\n') umask >&2
return 1
end
-end
+end
\ No newline at end of file
diff --git a/test-data/formatting.test.ts b/test-data/formatting.test.ts
index f6d82436..9ab89637 100644
--- a/test-data/formatting.test.ts
+++ b/test-data/formatting.test.ts
@@ -132,4 +132,3 @@ describe('formatting tests', () => {
})
})
-
diff --git a/test-data/inline-parser.test.ts b/test-data/inline-parser.test.ts
new file mode 100644
index 00000000..9200cfaf
--- /dev/null
+++ b/test-data/inline-parser.test.ts
@@ -0,0 +1,458 @@
+
+import { setLogger } from './helpers';
+import { initializeParser } from '../src/parser';
+import Parser, { SyntaxNode, TreeCursor } from 'web-tree-sitter';
+import { FishSource, InlineParser } from '../src/utils/completion/inline-parser';
+import { getChildNodes, getLastLeaf, getLeafs } from '../src/utils/tree-sitter';
+import { ChildProcess, exec, spawn } from 'child_process';
+import { promisify } from 'util';
+import { Writable } from 'stream';
+
+import { CompletionItem, CompletionItemKind, CompletionParams } from 'vscode-languageserver';
+
+// jest.setTimeout(5000); // Increase the timeout if necessary
+// import { Writable } from 'stream';
+
+// import { promisify } from 'util';
+const execAsync = promisify(exec);
+
+function createJob(): ChildProcess {
+ const fishJob = spawn('fish', [ '-ic', 'while read val -P ""; complete -C "$val"; echo FISH_COMPLETION_END; end' ], {
+ stdio: [ 'pipe', 'pipe', 'ignore' ]
+ });
+
+ return fishJob;
+}
+
+function handleStream(
+ stream: NodeJS.ReadableStream | null,
+ callback: (data: string) => void,
+ endCallback: () => void
+): void {
+ if (stream) {
+ stream.on('data', (data) => {
+ const lines: string[] = data.toString().split('\n');
+ lines.forEach((line: string) => {
+ if (line !== '') {
+ callback(line);
+ }
+ });
+ });
+ stream.on('end', endCallback);
+ }
+}
+
+function handleStderr(fishJob: ChildProcess): void {
+ if (fishJob.stderr) {
+ fishJob.stderr.on('data', (data) => {
+ console.error(`stderr: ${data}`);
+ });
+ }
+}
+
+function handleClose(fishJob: ChildProcess): void {
+ // fishJob.on('close', (code) => {
+ // // console.log(`fish process exited with code ${code}`);
+ // });
+}
+
+function processOutput(outputBuffer: string[]): string[] {
+ return outputBuffer.map((item) => {
+ const index = item.indexOf('\t');
+ if (index !== -1) {
+ const label = item.substring(0, index);
+ const detail = item.substring(index + 1);
+ return `${label} (${detail})`;
+ }
+ return item;
+ });
+}
+
+
+export async function testCompletion(input: string): Promise {
+ const fishJob = createJob();
+ const outputBuffer: string[] = [];
+
+ return new Promise((resolve, reject) => {
+ handleStream(fishJob.stdout, (line) => {
+ if (line === 'FISH_COMPLETION_END') {
+ resolve(processOutput(outputBuffer));
+ } else {
+ outputBuffer.push(line);
+ }
+ }, () => resolve(processOutput(outputBuffer)));
+
+ handleStderr(fishJob);
+ handleClose(fishJob);
+
+ if (!fishJob.stdin) {
+ reject(new Error('stdin is not available'));
+ } else {
+ const stdin: Writable = fishJob.stdin;
+ const writeAsync = promisify(stdin.write.bind(stdin)) as (chunk: any, encoding?: BufferEncoding) => Promise;
+ writeAsync(input + '\n')
+ .then(() => stdin.end())
+ .catch(reject);
+ }
+ });
+}
+
+function processExecOutput(output: string): string[] {
+ return output.split('\n')
+ .filter(item => item.trim() !== '')
+ .map((item) => {
+ const index = item.indexOf('\t');
+ if (index !== -1) {
+ const label = item.substring(0, index);
+ const detail = item.substring(index + 1);
+ return `${label} (${detail})`;
+ }
+ return item;
+ });
+}
+
+export async function testExecCompletion(input: string): Promise {
+ try {
+ const { stdout, stderr } = await execAsync(`echo '${input.replace(/'/g, "'\\''")}' | fish -ic 'while read val -P ""; complete -C "$val"; end'`);
+ if (stderr) {
+ console.error(`stderr: ${stderr}`);
+ }
+ return processExecOutput(stdout);
+ } catch (error) {
+ console.error('Error executing command:', error);
+ throw error;
+ }
+}
+
+//
+//
+// Traversal
+//
+//
+// function* customTraversal(cursor: TreeCursor): Generator {
+// do {
+// yield cursor.currentNode;
+// if (cursor.gotoFirstChild()) {
+// yield* customTraversal(cursor);
+// cursor.gotoParent(); // go back to the parent after traversing children
+// }
+// } while (cursor.gotoNextSibling());
+// }
+//
+// async function traverseCustom(input: string) {
+// const traverseParser = await initializeParser();
+// const tree = traverseParser.parse(input);
+// const cursor = tree.rootNode.walk();
+//
+// for (const node of customTraversal(cursor)) {
+// console.log(node.type, node.text);
+// }
+// }
+//
+// //
+// //
+// // Custom Completion Targets
+// //
+// //
+// //
+// function extractCompletionTargets(node: Parser.SyntaxNode): string[] {
+// const targets: string[] = [];
+//
+// function traverse(node: Parser.SyntaxNode) {
+// if (node.type === 'command' || node.type === 'variable_name' || node.type === 'argument' || node.type === 'function') {
+// targets.push(node.type+":"+node.text);
+// }
+// for (let i = 0; i < node.childCount; i++) {
+// const current = node.child(i)
+// if (current) traverse(current);
+// }
+// }
+//
+// traverse(node);
+// return targets;
+// }
+
+// async function parseForCompletion(input: string) {
+// const cmpParser = await initializeParser();
+// const tree = cmpParser.parse(input);
+// const rootNode = tree.rootNode;
+//
+// if (!rootNode) return;
+//
+// const targets = extractCompletionTargets(rootNode);
+//
+// console.log(JSON.stringify({"Line": input, 'Completion Targets': targets}, null, 2));
+// }
+
+
+// async function parseCommandLine(input: string) {
+// const cparser = await initializeParser();
+// const tree = cparser.parse(input);
+// const rootNode = tree.rootNode;
+//
+// if (!rootNode) return;
+//
+// // Assume the cursor is at the end of the input
+// const cursorPosition = input.length-1;
+//
+// // Find the node at the cursor position
+// const currentNode = rootNode.descendantForIndex(cursorPosition);
+//
+// if (currentNode) {
+// const functionNode = findPreviousProcess(currentNode);
+// console.log('Previous Function Keyword Node:', functionNode?.text);
+// }
+// }
+
+
+function findPreviousProcess(node: Parser.SyntaxNode): Parser.SyntaxNode | null {
+ let current: SyntaxNode | null = node;
+ while (current && !isProcess(current)) {
+ current = getPreviousNode(current);
+ }
+ console.log('currentPROC', current?.text || 'null');
+ return current && isProcess(current) ? current : null;
+}
+
+function getPreviousNode(node: Parser.SyntaxNode): Parser.SyntaxNode | null {
+ let current: SyntaxNode | null = node;
+
+ while (current) {
+ if (isProcess(current)) {
+ current = current.previousSibling || current.parent;
+ } else {
+ break;
+ }
+ }
+
+ // Traverse to the previous sibling or parent if no previous sibling exists
+ if (current && current.previousSibling) {
+ return current.previousSibling;
+ } else if (current && current.parent) {
+ return current.parent;
+ }
+ return null;
+}
+
+function isProcess(node: Parser.SyntaxNode): boolean {
+ const processTypes = [ 'pipe', 'redirect_statement', 'subshell', '&&', '||', ';', 'function', 'command', 'for', 'while', 'if', 'switch', 'case' ];
+ return processTypes.includes(node.type) || node.type.endsWith('statement');
+}
+
+
+async function parseCommandLine(input: string) {
+ const parser = await initializeParser();
+ const tree = parser.parse(input);
+ const rootNode = tree.rootNode;
+
+ if (!rootNode) return;
+
+ // Assume the cursor is at the end of the input
+ const cursorPosition = input.length;
+
+ // Find the node at the cursor position
+ const currentNode = rootNode.descendantForIndex(cursorPosition);
+
+ if (currentNode) {
+ const currentLine = getLineAtPosition(input, cursorPosition);
+ const combinedLines = getCombinedLines(input, cursorPosition);
+
+ console.log('Combined Lines for Completion:', combinedLines);
+ // Use the combined lines for completions
+ parseForCompletion(combinedLines);
+ }
+}
+
+function getLineAtPosition(input: string, position: number): string {
+ const lines = input.split('\n');
+ let lineIndex = 0;
+ let charCount = 0;
+
+ for (let i = 0; i < lines.length; i++) {
+ charCount += lines[ i ]!.length + 1; // +1 for newline character
+ if (charCount > position) {
+ lineIndex = i;
+ break;
+ }
+ }
+
+ return lines[ lineIndex ] || '';
+}
+
+function getCombinedLines(input: string, position: number): string {
+ const lines = input.split('\n');
+ let lineIndex = 0;
+ let charCount = 0;
+ let combinedLines = '';
+
+ for (let i = 0; i < lines.length; i++) {
+ charCount += lines[ i ]!.length + 1; // +1 for newline character
+ if (charCount > position) {
+ lineIndex = i;
+ break;
+ }
+ }
+
+ // Combine lines upwards until no trailing backslash
+ for (let i = lineIndex - 1; i >= 0; i--) {
+ if (lines[ i ]!.endsWith('\\')) {
+ combinedLines = lines[ i ]!.slice(0, -1) + combinedLines;
+ } else {
+ break;
+ }
+ }
+
+ combinedLines += lines[ lineIndex ]; // Add the current line
+ return combinedLines;
+
+}
+//
+// Function to get Fish shell completion
+async function getFishCompletion(scriptContent: string): Promise {
+ // const command = `echo '${scriptContent.replace(/'/g, "'\\''")}' | fish -c 'source (string trim --right \\n); complete -C ""'`;
+ const command = `fish -c "string escape \'${scriptContent.replace(/'/g, "'\\''")}\' | read -t -a cmd; complete -C "$cmd"'`;
+ try {
+ const { stdout, stderr } = await execAsync(command);
+ if (stderr) {
+ return '';
+ }
+ return stdout;
+ } catch (e) {
+ console.log(e);
+ }
+ return '';
+}
+
+// Function to parse the completion result from Fish shell into LSP CompletionItems
+function parseCompletionItems(completionResult: string): CompletionItem[] {
+ return completionResult
+ .split('\n')
+ .filter((item) => item)
+ .map((item) => ({
+ label: item,
+ kind: 1, // 1 is for Text in LSP CompletionItemKind
+ }));
+}
+
+function parseForCompletion(line: string) {
+ // Here you can implement your logic to use the line for completions
+ console.log('Parsing line for completion:', line);
+}
+
+setLogger();
+// Example usage: Simulating a command line for autocompletion
+// const commandLine = 'echo "hello" && l';
+// parseForCompletion(commandLine);
+describe('inline-parser test suite', () => {
+
+ it('test inline-parser normal', async () => {
+ const input = 'function \\\\n --';
+ const result = await testCompletion(input);
+ console.log(result);
+ expect(result).toContain('--argument-names (Specify named arguments)');
+ expect(result).toContain('--description (Set function description)');
+ expect(result).toContain('--inherit-variable (Snapshot and define local variable)');
+ expect(result).toContain('--no-scope-shadowing (Do not shadow variable scope of calling function)');
+ expect(result).toContain('--on-event (Make the function a generic event handler)');
+ expect(result).toContain('--on-job-exit (Make the function a job exit event handler)');
+ expect(result).toContain('--on-process-exit (Make the function a process exit event handler)');
+ expect(result).toContain('--on-signal (Make the function a signal event handler)');
+ expect(result).toContain('--on-variable (Make the function a variable update event handler)');
+ expect(result).toContain('--wraps (Inherit completions from the given command)');
+ // expect(true).toBeTruthy()
+ });
+
+});
+
+it('test exec inline-parser normal', async () => {
+ const input = 'function --';
+ const result = await testExecCompletion(input);
+ console.log(result);
+
+ // expect(result).toContain('--argument-names (Specify named arguments)');
+ // expect(result).toContain('--description (Set function description)');
+ // expect(result).toContain('--inherit-variable (Snapshot and define local variable)');
+ // expect(result).toContain('--no-scope-shadowing (Do not shadow variable scope of calling function)');
+ // expect(result).toContain('--on-event (Make the function a generic event handler)');
+ // expect(result).toContain('--on-job-exit (Make the function a job exit event handler)');
+ // expect(result).toContain('--on-process-exit (Make the function a process exit event handler)');
+ // expect(result).toContain('--on-signal (Make the function a signal event handler)');
+ // expect(result).toContain('--on-variable (Make the function a variable update event handler)');
+ // expect(result).toContain('--wraps (Inherit completions from the given command)');
+ // expect(true).toBeTruthy()
+});
+
+it('source and `function foo \\ --argument`', async () => {
+ const fishSource = new FishSource();
+ // Does NOT Work!!
+ const result = await fishSource.sendLines(['function foo \\', '--argument']);
+ // expect(result).toContain('--argument-names (Specify named arguments)');
+ // expect(result).toContain('--description (Set function description)');
+ // expect(result).toContain('--inherit-variable (Snapshot and define local variable)');
+ // expect(result).toContain('--no-scope-shadowing (Do not shadow variable scope of calling function)');
+ // expect(result).toContain('--on-event (Make the function a generic event handler)');
+ // expect(result).toContain('--on-job-exit (Make the function a job exit event handler)');
+ // expect(result).toContain('--on-process-exit (Make the function a process exit event handler)');
+ // expect(result).toContain('--on-signal (Make the function a signal event handler)');
+ // expect(result).toContain('--on-variable (Make the function a variable update event handler)');
+ // expect(result).toContain('--wraps (Inherit completions from the given command)');
+ console.log(result);
+});
+
+// input.forEach(async line => {
+// const tree = parser.parse(line);
+// const {rootNode} = tree
+// const lastLeaf = getLastLeaf(rootNode)
+//
+// const prev = findPreviousProcess(lastLeaf)
+//
+// console.log(`lastLeaf: '${lastLeaf.text}', type: ${lastLeaf.type}`);
+// console.log(`process: '${prev?.text || 'null'}'`);
+//
+// console.log('____');
+// await parseCommandLine(line);
+
+// });
+// console.log({text: node.text, grammarId: node.grammarId, typeId: node.typeId, grammarType: node.grammarType, normalType: node.type, fieldName: parser.getLanguage().fieldNameForId(node.grammarId)})
+// console.log();
+// if (node.type === 'option' || node.type === 'argument') {
+// console.log('Has argument!!!!');
+// }
+//
+// }
+// console.log();
+// const res = await traverseCustom(line)
+// console.log(res);
+// console.log(parser.getLanguage());
+
+
+
+// await parseCommandLine(line)
+// console.log();
+
+// const children = getChildNodes(rootNode);
+// const lastChild = getLeafs(rootNode).pop()!;
+//
+// let current: SyntaxNode | null = lastChild;
+// while (current && !startNodes.includes(current.type)) {
+// current = current.previousSibling;
+// }
+// if (current) {
+// console.log('commandNode', current.text, current.type);
+// }
+
+// if (line.startsWith('function')) {
+
+// console.log('-'.repeat(Number.parseInt(process.env.COLUMNS || '80')));
+// console.log({line});
+// for (const child of children) {
+// console.log({text: child.text, type: child.type, grammarType: child.grammarType});
+// }
+
+// console.log({
+// firstChild: rootNode.firstChild?.text || '',
+// lastChild: rootNode.fieldNameForChild(-1) || ''
+// });
+// const outputLine = inline.parseCommand(line)
+// console.log(outputLine.command);
+
diff --git a/test-data/node-types.test.ts b/test-data/node-types.test.ts
index e30b81f8..121f7c6f 100644
--- a/test-data/node-types.test.ts
+++ b/test-data/node-types.test.ts
@@ -1,9 +1,8 @@
import Parser, { Tree, SyntaxNode } from 'web-tree-sitter';
import { initializeParser } from '../src/parser';
-import { getChildNodes } from '../src/utils/tree-sitter';
+import { findFirstSibling, getChildNodes, getCommandArgumentValue } from '../src/utils/tree-sitter';
import * as NodeTypes from '../src/utils/node-types'
import * as VarTypes from '../src/utils/variable-syntax-nodes'
-import assert from 'node:assert'
// import { assert } from 'chai';
import { expandEntireVariableLine } from '../src/utils/definition-scope'
import { skip } from 'node:test';
@@ -25,6 +24,35 @@ function skipSetQuery(node: SyntaxNode) {
return false;
}
+
+/*
+ * get first sibling
+ */
+function walkUpSiblings(n: SyntaxNode) {
+ let currentNode = n
+ while (currentNode.previousSibling !== null) {
+ currentNode = currentNode.previousSibling
+ }
+ return currentNode
+}
+
+function walkUpAndGather(n: SyntaxNode, predicate: (_: SyntaxNode) => boolean) {
+ const result: SyntaxNode[] = [];
+ let currentNode: SyntaxNode | null = n;
+ while (currentNode !== null) {
+ if (!predicate(currentNode)) break;
+ result.unshift(currentNode);
+ currentNode = currentNode.previousNamedSibling;
+ }
+ return result;
+}
+
+
+function logNode(nodeName: string, text: string, type: string, isNamed: boolean) {
+ console.log({name: nodeName, text, type, isNamed});
+
+}
+
function logNodes(nodes: SyntaxNode[]) {
nodes.forEach(n => console.log(n.text))
}
@@ -51,19 +79,28 @@ describe("node-types tests", () => {
it('isCommand', () => {
const commands = parseStringForNodeType('echo "hello world"', NodeTypes.isCommand);
//logNodes(commands)
- assert.equal(commands[0]?.text, 'echo "hello world"')
+ expect(commands[0]?.text).toEqual('echo "hello world"')
})
it('isCommandName', () => {
const commandsName = parseStringForNodeType('echo "hello world"', NodeTypes.isCommandName);
//logNodes(commandsName)
- assert.equal(commandsName[0]?.text, 'echo')
+ expect(commandsName[0]?.text).toEqual('echo')
})
it('isComment', () => {
const comments = parseStringForNodeType('# this is a comment', NodeTypes.isComment);
//logNodes(comments)
- assert.equal(comments[0]?.text, '# this is a comment')
+ expect(comments[0]?.text).toEqual('# this is a comment')
+
+ const multiComments = parseStringForNodeType([
+ '# line 1',
+ '# line 2',
+ '# line 3',
+ 'set -l value'
+ ].join('\n'), NodeTypes.isComment)
+
+ expect(multiComments.length).toBe(3)
})
it('isShebang', () => {
@@ -76,16 +113,20 @@ describe("node-types tests", () => {
const comments = parseStringForNodeType(testString, NodeTypes.isComment);
//logNodes(shebang)
//logNodes(comments)
- assert.equal(shebang.length, 1)
- assert.equal(comments.length, 2)
+ expect(shebang.length).toBe(1)
+ expect(comments.length).toBe(2)
})
- it('isProgram', () => {
- const input = 'echo "hello world"';
- const root = parser.parse(input).rootNode!
- const program = parseStringForNodeType(input, NodeTypes.isProgram);
- assert.equal(program[0]?.text, root.text)
- })
+ it('isProgram', () => {
+ const emptyText = parseStringForNodeType('', NodeTypes.isProgram)
+ expect(emptyText.length).toBe(1)
+
+ // program === tree.rootNode
+ const input = 'echo "hello world"';
+ const root = parser.parse(input).rootNode!
+ const program = parseStringForNodeType(input, NodeTypes.isProgram);
+ expect(program[0]?.text).toEqual(root.text)
+ })
it('isStatement', () => {
/**
@@ -101,7 +142,7 @@ describe("node-types tests", () => {
].join('\n');
const statement = parseStringForNodeType(input, NodeTypes.isStatement);
//logNodes(statement)
- assert.equal(statement.length, 5)
+ expect(statement.length).toBe(5)
})
it('isEnd', () => {
@@ -114,7 +155,7 @@ describe("node-types tests", () => {
].join('\n');
const ends = parseStringForNodeType(input, NodeTypes.isEnd);
//logNodes(ends)
- assert.equal(ends.length, 5)
+ expect(ends.length).toBe(5)
})
it('isString', () => {
@@ -124,7 +165,7 @@ describe("node-types tests", () => {
].join('\n');
const strings = parseStringForNodeType(input, NodeTypes.isString);
//logNodes(strings)
- assert.equal(strings.length, 2)
+ expect(strings.length).toBe(2)
})
it('isReturn', () => {
@@ -135,7 +176,7 @@ describe("node-types tests", () => {
].join('\n')
const returns = parseStringForNodeType(input, NodeTypes.isReturn);
//logNodes(returns)
- assert.equal(returns.length, 1)
+ expect(returns.length).toBe(1)
})
/**
@@ -153,7 +194,7 @@ describe("node-types tests", () => {
].join('\n')
const functionDefinitions = parseStringForNodeType(input, NodeTypes.isFunctionDefinition);
//logNodes(functionDefinitions)
- assert.equal(functionDefinitions.length, 3)
+ expect(functionDefinitions.length).toBe(3)
})
it('isFunctionDefinitionName', () => {
@@ -168,8 +209,8 @@ describe("node-types tests", () => {
].join('\n')
const functionDefinitionNames = parseStringForNodeType(input, NodeTypes.isFunctionDefinitionName);
//logNodes(functionDefinitionNames)
- assert.equal(functionDefinitionNames.length, 3)
- assert.deepEqual(functionDefinitionNames.map(n => n.text), ['foo', 'foo_2', 'foo_2_inner'])
+ expect(functionDefinitionNames.length).toBe(3)
+ expect(functionDefinitionNames.map(n => n.text)).toEqual(['foo', 'foo_2', 'foo_2_inner'])
})
// TODO
@@ -183,8 +224,9 @@ describe("node-types tests", () => {
'set -gx OS_NAME (set -l f "v" | echo $v) # check for mac or linux',
].join('\n');
const variableDefinitions = parseStringForNodeType(input, NodeTypes.isDefinition);
- assert.deepEqual(
- variableDefinitions.map((v) => v.text),
+ expect(
+ variableDefinitions.map((v) => v.text)
+ ).toEqual(
["set_foo", "read_foo", "func_foo", "func_foo_arg", "OS_NAME", "f"]
);
})
@@ -214,7 +256,517 @@ describe("node-types tests", () => {
if (skipSetQuery(def)) return
result.push(def)
})
- assert.deepEqual(result.map(d => d.text), ['set_foo', 'param_foo', 'i', 'read_foo'])
+ expect(result.map(d => d.text)).toEqual(['set_foo', 'param_foo', 'i', 'read_foo'])
+ })
+
+ it('isStatement "if" "else-if" "else"', () => {
+ const input = [
+ 'set out_of_scope',
+ 'if true',
+ ' set out_of_scope true',
+ 'else if false',
+ ' set out_of_scope false',
+ 'else',
+ ' set --erase out_of_scope',
+ 'end'
+ ].join('\n')
+ const nodes = parseStringForNodeType(input, NodeTypes.isStatement)
+ expect(nodes.length).toBe(1);
+ })
+
+ it('isBlock "if" "else-if" "else"', () => {
+ const input = [
+ 'set out_of_scope',
+ 'if true',
+ ' set out_of_scope true',
+ 'else if false',
+ ' set out_of_scope false',
+ 'else',
+ ' set --erase out_of_scope',
+ 'end'
+ ].join('\n')
+ const nodes = parseStringForNodeType(input, NodeTypes.isBlock)
+ // console.log(nodes.length);
+ expect(nodes.length).toBe(3);
+ })
+
+ it('isClause/isCaseClause "switch" "case" "case" "case"', () => {
+ const input = [
+ 'set os_name (uname -o)',
+ 'switch "$os_name"',
+ " case 'GNU/Linux'",
+ " echo 'good'",
+ " case 'OSX'",
+ " echo 'mid'",
+ " case 'Windows'",
+ " echo 'bad'",
+ 'end'
+ ].join('\n')
+
+ const clause_nodes = parseStringForNodeType(input, NodeTypes.isClause)
+ expect(clause_nodes.length).toBe(3);
+
+ const case_nodes = parseStringForNodeType(input, NodeTypes.isCaseClause)
+ expect(case_nodes.length).toBe(3);
+ })
+
+ it('isStringCharacter "" \'\'', () => {
+ const input = [
+ 'set os_name (uname -o)',
+ 'switch "$os_name"',
+ " case 'GNU/Linux'",
+ " echo 'good'",
+ " case 'OSX'",
+ " echo 'mid'",
+ " case 'Windows'",
+ " echo 'bad'",
+ 'end'
+ ].join('\n')
+
+ const stringCharNodes = parseStringForNodeType(input, NodeTypes.isStringCharacter)
+ expect(stringCharNodes.length).toBe(14);
+ })
+
+ it('isString "" \'\'', () => {
+ const input = [
+ 'set os_name (uname -o)',
+ 'switch "$os_name"',
+ " case 'GNU/Linux'",
+ " echo 'good'",
+ " case 'OSX'",
+ " echo 'mid'",
+ " case 'Windows'",
+ " echo 'bad'",
+ 'end'
+ ].join('\n')
+
+ const stringNodes = parseStringForNodeType(input, NodeTypes.isString)
+ expect(stringNodes.length).toBe(7);
+ })
+
+ it('isEnd "for" "if"', () => {
+ const endNodes = parseStringForNodeType([
+ 'for i in (seq 1 10)',
+ ' echo $i',
+ 'end',
+ 'if true',
+ ' echo "false"',
+ 'end'
+ ].join('\n'), NodeTypes.isEnd)
+ expect(endNodes.length).toBe(2)
+ })
+
+ it('isNewline "for" "if"', () => {
+ const endNodes = parseStringForNodeType([
+ 'for i in (seq 1 10)',
+ ' echo $i',
+ 'end',
+ 'if true',
+ ' echo "false"',
+ 'end'
+ ].join('\n'), NodeTypes.isNewline)
+ expect(endNodes.length).toBe(5)
+ })
+
+ it('isSemiColon', () => {
+ const colonNodes = parseStringForNodeType([
+ 'begin;',
+ " if test '$HOME' = (pwd); and string match -re '/home/username' \"$HOME\" ",
+ " echo 'in your home directory'; and return 0",
+ " end",
+ 'end;'
+
+ ].join('\n'), NodeTypes.isSemicolon)
+ expect(colonNodes.length).toBe(4)
+ })
+
+ it('isReturn', () => {
+ const returnNodes = parseStringForNodeType([
+ "function t_or_f",
+ " if test \"$argv\" = 't'",
+ " return 0",
+ " end",
+ " return 1",
+ "end"
+ ].join('\n'), NodeTypes.isReturn)
+
+ expect(returnNodes.length).toBe(2)
+ })
+
+ it('isIfOrElseIfConditional "if" "else-if" "else"', () => {
+ const condNodes = parseStringForNodeType([
+ "function t_or_f",
+ " if test \"$argv\" = 't'",
+ " return 0",
+ " else if test -n \"$argv\"",
+ " return 0",
+ " else",
+ " return 1",
+ " end",
+ "end"
+ ].join('\n'), NodeTypes.isIfOrElseIfConditional)
+ expect(condNodes.length).toBe(2)
+ })
+
+ it('isConditional "if" "else-if" "else"', () => {
+ const condNodes = parseStringForNodeType([
+ "function t_or_f",
+ " if test \"$argv\" = 't'",
+ " return 0",
+ " else if test -n \"$argv\"",
+ " return 0",
+ " else",
+ " return 1",
+ " end",
+ "end"
+ ].join('\n'), NodeTypes.isConditional)
+ expect(condNodes.length).toBe(3)
+ })
+
+ it('isOption "set --global --export --append PATH $HOME/.local/bin"; "set -gxa PATH $HOME/.cargo/bin"', () => {
+ const input = [
+ "set --global --export --append $PATH $HOME/.local/bin",
+ "set -gxa PATH $HOME/.cargo/bin"
+ ].join('\n')
+ const allOptionNodes = parseStringForNodeType(input, NodeTypes.isOption)
+ expect(allOptionNodes.length).toBe(4)
+ expect(allOptionNodes.map(n => n.text)).toEqual(['--global', '--export', '--append', '-gxa'])
+
+ const longOptionNodes = parseStringForNodeType(input, NodeTypes.isLongOption)
+ expect(longOptionNodes.map(n => n.text)).toEqual(['--global', '--export', '--append'])
+
+ const shortOptionNodes = parseStringForNodeType(input, NodeTypes.isShortOption)
+ expect(shortOptionNodes.map(n => n.text)).toEqual(['-gxa'])
+ })
+
+ it('isShortOption [WITH CHAR]', () => {
+ const shortOptionNodes = parseStringForNodeType("set -gxa PATH $HOME/.cargo/bin", NodeTypes.isShortOption)
+ expect(shortOptionNodes.map(n => n.text)).toEqual(['-gxa'])
+
+ const joinedShortNodes = parseStringForNodeType("set -gxa PATH $HOME/.cargo/bin", NodeTypes.isJoinedShortOption)
+ expect(joinedShortNodes.map(n => n.text)).toEqual(['-gxa'])
+
+ const globalOption = (n: SyntaxNode) => NodeTypes.hasShortOptionCharacter(n, 'g')
+ const exportOption = (n: SyntaxNode) => NodeTypes.hasShortOptionCharacter(n, 'x')
+ const appendOption = (n: SyntaxNode) => NodeTypes.hasShortOptionCharacter(n, 'a')
+ const hasAllThreeOptions = (n: SyntaxNode) => {return globalOption(n) || exportOption(n) || appendOption(n)}
+ expect(parseStringForNodeType("set -gxa PATH $HOME/.cargo/bin", (n: SyntaxNode) => hasAllThreeOptions(n))).toBeTruthy()
+ })
+
+ it('isMatchingOption', () => {
+ expect([
+ ...parseStringForNodeType("set -gxa PATH $HOME/.cargo/bin", (n: SyntaxNode) => NodeTypes.isMatchingOption(n, {shortOption: '-g'})),
+ ...parseStringForNodeType("set -gxa PATH $HOME/.cargo/bin", (n: SyntaxNode) => NodeTypes.isMatchingOption(n, {shortOption: '-x'})),
+ ...parseStringForNodeType("set -gxa PATH $HOME/.cargo/bin", (n: SyntaxNode) => NodeTypes.isMatchingOption(n, {shortOption: '-a'}))
+ ].map(n => n.text)).toEqual(['-gxa', '-gxa', '-gxa'])
+
+ const oldFlag = parseStringForNodeType('find -type d', (n: SyntaxNode) => NodeTypes.isMatchingOption(n, {oldUnixOption: '-type'}))
+ expect(oldFlag.map(n => n.text)).toEqual(['-type'])
+
+ expect(
+ parseStringForNodeType(
+ 'set --global PATH /bin',
+ (n: SyntaxNode) => NodeTypes.isMatchingOption(n, {longOption: '--global'})
+ ).map(n => n.text)
+ ).toEqual(['--global'])
+
+ const longOpt = parseStringForNodeType("command ls --ignore='install_scripts'", (n: SyntaxNode) => NodeTypes.isMatchingOption(n, {longOption: '--ignore'}))
+ expect(
+ longOpt.map(n => n.text.slice(0, n.text.indexOf('=')))
+ ).toEqual(['--ignore', '--ignore'])
+ })
+
+ it('isEndStdinCharacter `string match --regex --entire -- \'^\w+\s\w*\' "$argv"`', () => {
+ const charNodes = parseStringForNodeType(`string match --regex --entire -- '^\w+\s\w*' "$argv"`, NodeTypes.isEndStdinCharacter)
+ expect(charNodes.length).toBe(1)
+ })
+
+
+ it('isScope "program" "function" "for" "if" "else-if" "else" "switch" "case" "case"', () => {
+ const scopeNodes = parseStringForNodeType([
+ "function inner_function",
+ " for i in (seq 1 10)",
+ " echo $i",
+ " end",
+ " if test \"$argv\" = 't'",
+ " echo 0",
+ " else if test -n \"$argv\"",
+ " echo 0",
+ " else",
+ " echo 1",
+ " end",
+ ' switch "$argv"',
+ ' case "-*"',
+ ' return 1',
+ ' case "*"',
+ ' return 0',
+ ' end',
+ "end"
+ ].join('\n'), NodeTypes.isScope)
+ expect(scopeNodes.map(n => n.type)).toEqual([
+ 'program',
+ 'function_definition',
+ 'for_statement',
+ 'if_statement',
+ 'switch_statement',
+ ])
+ })
+
+ it('isString() -> string values `argparse "h/help" "v/value" -- $argv`', () => {
+ // const stringNodes = parseStringForNodeType([
+ // 'argparse "h/help" "v/value" -- $argv',
+ // 'or return'
+ // ].join('\n'), NodeTypes.isString)
+ // stringNodes.forEach(s => {
+ // console.log(s.text.slice(1, -1).split('/'));
+ // })
+
+ const argParseNodes = parseStringForNodeType([
+ 'argparse "h/help" "v/value" "other-value" "special-value=?"-- $argv',
+ 'or return'
+ ].join('\n'), (n: SyntaxNode) => {
+ if (NodeTypes.findParentCommand(n)?.firstChild?.text === 'argparse') {
+ return NodeTypes.isString(n)
+ }
+ return false
+ })
+ const parsedStrs = argParseNodes.map(n => {
+ const resultText = n.text.slice(1,-1)
+ return resultText.includes('=')
+ ? resultText.slice(0, resultText.indexOf('='))
+ : resultText
+ });
+
+ expect(parsedStrs).toEqual([
+ 'h/help',
+ 'v/value',
+ 'other-value',
+ 'special-value'
+ ])
+
+ /**
+ *
+ */
+ })
+
+ it('findPreviousSibling() - with find multiline comments', () => {
+ const [eNode, ...other] = parseStringForNodeType('set --local var a b c d e', (n: SyntaxNode) => n.text === 'e')
+ const firstNode = walkUpSiblings(eNode!)
+ expect(firstNode.text).toBe('set');
+
+ /**
+ * do previous sibling comment nodes
+ */
+ const commentNodes = parseStringForNodeType([
+ '# comment a',
+ '# comment b',
+ '# comment c',
+ 'set -l abc'
+ ].join('\n'), NodeTypes.isComment)
+
+ let lastComment = commentNodes.pop()!
+ const commentArr = walkUpAndGather(lastComment, (n) => NodeTypes.isComment(n) || NodeTypes.isNewline(n))
+ expect(
+ commentArr.map(c => c.text)
+ ).toEqual([
+ '# comment a',
+ '# comment b',
+ '# comment c',
+ ])
+
+ /*
+ * parse the last comment from the string
+ */
+ lastComment = parseStringForNodeType([
+ '# comment a',
+ '# comment b',
+ '# comment c',
+ 'set -l abc # comment to skip'
+ ].join('\n'), NodeTypes.isComment).pop()!
+ expect(lastComment.text).toEqual('# comment to skip')
+
+ /*
+ * parse the last definition
+ */
+ const lastDefinition = parseStringForNodeType([
+ '# comment a',
+ '# comment b',
+ '# comment c',
+ 'set -l abc # comment to skip'
+ ].join('\n'), NodeTypes.isVariableDefinition).pop()!
+ expect(lastDefinition.text).toEqual('abc')
+
+ /*
+ * find the parent of the last definition
+ */
+ const lastDefinitionCmd = NodeTypes.findParentCommand(lastDefinition)!
+ expect(lastDefinitionCmd.text).toEqual('set -l abc')
+
+ /*
+ * the gathered comments of the last comment should just be
+ * the last comment
+ */
+ expect(
+ walkUpAndGather(
+ lastComment,
+ (n) => NodeTypes.isComment(n) || NodeTypes.isNewline(n)
+ ).map(n => n.text)
+ ).toEqual([ '# comment to skip' ])
+
+ /*
+ * the gathered comments of the lastDefinition should just be nothing
+ * the lastDefinition's previous sibling is not a comment or newline char
+ */
+ expect(
+ walkUpAndGather(
+ lastDefinition,
+ (n) => NodeTypes.isComment(n) || NodeTypes.isNewline(n)
+ ).map(n => n.text)
+ ).toEqual([])
+
+ /*
+ * The gathered comments of the lastDefinitionCmd would also be empty because
+ * it is a command (NOT A COMMENT).
+ * However, the lastDefinitionCmd's previous sibling, should be a newline character
+ * and previousNamedSibling should be .type 'comment'
+ */
+ expect(
+ walkUpAndGather(
+ lastDefinitionCmd.previousNamedSibling!,
+ (n) => NodeTypes.isComment(n) || NodeTypes.isNewline(n)
+ ).map(n => n.text)
+ ).toEqual([
+ '# comment a',
+ '# comment b',
+ '# comment c'
+ ])
+ })
+
+ it('walkUpAndGather - inline-comment on preceding line', () => {
+ let node = parseStringForNodeType([
+ 'set -l a_1 "1" # preceding comment',
+ 'set --local a_2 "2"',
+ ].join('\n'), (n: SyntaxNode) => n.text === 'a_2').pop()!;
+ let commandNode = NodeTypes.findParentCommand(node)!;
+ let currentNode: SyntaxNode | null = commandNode?.previousNamedSibling!
+ expect(
+ walkUpAndGather(
+ currentNode,
+ (n) => !NodeTypes.isInlineComment(n) && (NodeTypes.isComment(n) || NodeTypes.isNewline(n))
+ ).map(n => n.text)
+ ).toEqual([])
+
+ node = parseStringForNodeType([
+ 'set -l A_2 # preceding comment',
+ '# comment a',
+ '# comment b',
+ 'set -l a_1 "1" # preceding comment',
+ 'set --local a_2 "2"'
+ ].join('\n'), (n: SyntaxNode) => n.text === 'a_1').pop()!
+
+ commandNode = NodeTypes.findParentCommand(node)!;
+ currentNode = commandNode?.previousNamedSibling!
+
+ expect(
+ walkUpAndGather(
+ currentNode,
+ (n) => !NodeTypes.isInlineComment(n) && (NodeTypes.isComment(n) || NodeTypes.isNewline(n))
+ ).map(n => n.text)
+ ).toEqual([
+ '# comment a',
+ '# comment b',
+ ])
+ })
+
+ it('[REGEX FLAG] string match -re "^-.*" "$argv"', () => {
+ const strNodes = parseStringForNodeType('string match -re "^-.*" "$argv"', NodeTypes.isString)
+ const lastStrNode = strNodes.pop()!
+ const parentNode = NodeTypes.findParentCommand(lastStrNode)
+ const regexOption = findFirstSibling(lastStrNode, n => NodeTypes.isMatchingOption(n, {shortOption: '-r', longOption: '--regex'}))
+ // if (parentNode?.firstChild?.text === 'string' && regexOption) {
+ // console.log("found");
+ // }
+ expect(parentNode?.firstChild?.text === 'string' && regexOption).toBeTruthy()
+ })
+
+ it('for loop', () => {
+ const input: string = [
+ 'for i in (seq 1 10)',
+ ' echo $i',
+ 'end',
+ 'function a',
+ ' for i in (seq 1 100)',
+ ' echo $i',
+ ' end',
+ 'end'
+ ].join('\n')
+ expect(parseStringForNodeType(input, NodeTypes.isForLoop).length).toBe(2)
+ expect(parseStringForNodeType(input, NodeTypes.isVariableDefinition).length).toBe(2)
+
+ /*
+ * BOTH , '$i' (variable_expansion) and 'i' (variable) are valid in NodeTypes.isVariable()
+ * i.e., `echo $i` creates both above types
+ */
+ expect(parseStringForNodeType(input, NodeTypes.isVariable).length).toBe(6)
+ })
+
+ /**
+ * Diagnostic for string expansion inside quotes
+ */
+ it('[WARN] string check variables in quotes', () => {
+ const strNodes = parseStringForNodeType([
+ 'set -l bad \'$argv\'',
+ 'set -l good "$argv"',
+ ].join('\n'), NodeTypes.isString);
+ expect(strNodes.length).toBe(2);
+
+ const warnNodes: SyntaxNode[] = strNodes.filter(node => node.text.includes('$') && node.text.startsWith("'"))
+ // for (const node of strNodes) {
+ // if (node.text.includes('$') && node.text.startsWith("'")) {
+ // console.log(node.text);
+ // }
+ // }
+ expect(warnNodes.length).toEqual(1)
+ });
+
+
+ it('check if $argv isFlagValue `test -z "$argv"`', () => {
+
+ const optValues = parseStringForNodeType([
+ 'test -z "$argv"',
+ // 'string split --field 2 "\\n" "h\\ni"',
+ "abbr -a -g gsc --set-cursor=% 'git stash create \'%\''",
+ `string split -f2 ' ' 'h i'`,
+ ].join('\n'), NodeTypes.isOption)
+
+ const valueMatch = (parent: SyntaxNode, node: SyntaxNode) => {
+ switch (parent.text) {
+ case 'test':
+ return NodeTypes.isMatchingOption(node, {shortOption: '-z'})
+ case 'string':
+ return NodeTypes.isMatchingOption(node, {shortOption: '-f', longOption: '--field'})
+ case 'abbr':
+ return NodeTypes.isMatchingOption(node, {longOption: '--set-cursor'})
+ default:
+ return null
+ }
+ }
+
+ optValues.forEach(o => {
+ // console.log(o.text);
+ const parentCmd = NodeTypes.findParentCommand(o)?.firstNamedChild
+ if (!parentCmd) {
+ console.log('ERRROR:', o.text)
+ return
+ }
+ const result = valueMatch(parentCmd, o)!
+ // console.log({result});
+
+ /** continiue testing getArgumentValue(parent, argName)
+ * ^- refactor to `shortOption | longOption | oldOption`
+ */
+ // console.log(parentCmd.text, o.text, result);
})
-})
\ No newline at end of file
+ })
+})
diff --git a/test-data/parser.test.ts b/test-data/parser.test.ts
index 2e21ef80..e9c22328 100644
--- a/test-data/parser.test.ts
+++ b/test-data/parser.test.ts
@@ -1,24 +1,104 @@
-import Parser from "web-tree-sitter";
+import Parser, { Language } from "web-tree-sitter";
// import {initializeParser} from '../src/parser'
//import fish from 'tree-sitter-fish'
-import {setLogger} from './helpers'
-import { initializeParser } from '../src/parser';
+import { setLogger } from './helpers';
+import { initializeParser } from '../src/parser';
-setLogger()
+
+export const nodeNamedTypes: string[] = [
+ 'word',
+ 'integer',
+ 'float',
+ 'break',
+ 'continue',
+ 'comment',
+ 'variable_name',
+ 'escape_sequence',
+ 'stream_redirect',
+ 'direction',
+ 'home_dir_expansion',
+ 'glob',
+ 'word',
+ 'program',
+ 'conditional_execution',
+ 'pipe',
+ 'redirect_statement',
+ 'negated_statement',
+ 'command_substitution',
+ 'function_definition',
+ 'return',
+ 'switch_statement',
+ 'case_clause',
+ 'for_statement',
+ 'while_statement',
+ 'if_statement',
+ 'else_if_clause',
+ 'else_clause',
+ 'begin_statement',
+ 'variable_expansion',
+ 'index',
+ 'range',
+ 'list_element_access',
+ 'brace_expansion',
+ 'double_quote_string',
+ 'single_quote_string',
+ 'command',
+ 'file_redirect',
+ 'concatenation',
+];
+
+export const nodeFieldTypes: string[] = [
+ 'null', 'argument',
+ 'condition', 'destination',
+ 'name', 'operator',
+ 'option', 'redirect',
+ 'value', 'variable'
+];
+
+
+setLogger();
describe('parser test-suite', () => {
- it('should be able to load the parser', async () => {
- // const fish = require('tree-sitter-fish');
- const parser = await initializeParser()
- const t = parser.parse('set -gx v "hello world"').rootNode
- expect(parser).toBeDefined()
- })
-
- it('should parse the fish string', async () => {
- // const fish = require('tree-sitter-fish');
- const parser = await initializeParser()
- const t = parser.parse('set -gx v "hello world"').rootNode
- expect(parser).toBeDefined()
- expect(t.children.length).toBeGreaterThanOrEqual(1);
- })
-})
+ it('should be able to load the parser', async () => {
+ // const fish = require('tree-sitter-fish');
+ const parser = await initializeParser();
+ const t = parser.parse('set -gx v "hello world"').rootNode;
+ expect(parser).toBeDefined();
+ });
+
+ it('should parse the fish string', async () => {
+ // const fish = require('tree-sitter-fish');
+ const parser = await initializeParser();
+ const t = parser.parse('set -gx v "hello world"').rootNode;
+ expect(parser).toBeDefined();
+ expect(t.children.length).toBeGreaterThanOrEqual(1);
+ });
+
+
+ it('filedCounts', async () => {
+ const parser = await initializeParser();
+ const { fieldCount } = parser.getLanguage();
+ const lang = parser.getLanguage();
+
+ expect(lang.fieldCount).toBe(9);
+ });
+
+ it('nodeTypeCount', async () => {
+ const parser = await initializeParser();
+ const lang = parser.getLanguage();
+ expect(lang.nodeTypeCount).toBe(103);
+ // console.log(lang);
+ });
+
+ it('nodeTypes', async () => {
+ const parser = await initializeParser();
+ const lang = parser.getLanguage();
+ for (let i = 0; i < lang.nodeTypeCount; ++i) {
+ if (lang.nodeTypeIsNamed(i)) {
+ const typeName = lang.nodeTypeForId(i);
+ expect(typeName).toBeTruthy()
+ // console.log(typeName);
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/test-data/semantic-tokens.test.ts b/test-data/semantic-tokens.test.ts
new file mode 100644
index 00000000..721482d9
--- /dev/null
+++ b/test-data/semantic-tokens.test.ts
@@ -0,0 +1,27 @@
+import { SemanticTokenTypes } from 'vscode-languageserver';
+
+
+const tokenTypes: string[] = [
+ SemanticTokenTypes.namespace,
+ SemanticTokenTypes.class,
+ SemanticTokenTypes.enum,
+ SemanticTokenTypes.interface,
+ SemanticTokenTypes.struct,
+ SemanticTokenTypes.typeParameter,
+ SemanticTokenTypes.type,
+ SemanticTokenTypes.parameter,
+ SemanticTokenTypes.variable,
+ SemanticTokenTypes.property,
+ SemanticTokenTypes.enumMember,
+ SemanticTokenTypes.event,
+ SemanticTokenTypes.function,
+ SemanticTokenTypes.method,
+ SemanticTokenTypes.macro,
+ SemanticTokenTypes.keyword,
+ SemanticTokenTypes.modifier,
+ SemanticTokenTypes.comment,
+ SemanticTokenTypes.string,
+ SemanticTokenTypes.number,
+ SemanticTokenTypes.regexp,
+ SemanticTokenTypes.operator,
+];
diff --git a/test-data/signature.test.ts b/test-data/signature.test.ts
new file mode 100644
index 00000000..9c0ddf32
--- /dev/null
+++ b/test-data/signature.test.ts
@@ -0,0 +1,224 @@
+import Parser, { SyntaxNode, Tree, Point } from 'web-tree-sitter';
+import { initializeParser } from '../src/parser';
+import { ExtendedBaseJson, PrebuiltDocumentationMap } from '../src/utils/snippets'
+import * as NodeTypes from '../src/utils/node-types'
+import * as TreeSitter from '../src/utils/tree-sitter'
+import { getAliasedCompletionItemSignature, getDefaultSignatures, lineSignatureBuilder, regexStringSignature } from '../src/signature'
+import {setLogger} from './helpers'
+import { MarkupContent, SignatureHelp, SignatureInformation } from 'vscode-languageserver';
+import { CompletionItemMap } from '../src/utils/completion/startup-cache';
+import { FishAliasCompletionItem } from '../src/utils/completion/types';
+import { getDocumentationResolver } from '../src/utils/completion/documentation';
+
+let parser: Parser;
+let completionMap: CompletionItemMap
+// const documentationMap = PrebuiltDocumentationMap;
+
+function analyzerParseCurrentLine(input: string) {
+ const line = input.trim();
+ const rootNode = parser.parse(line).rootNode
+ const lastNode = rootNode.descendantForPosition({row: 0, column: line.length - 1})
+ const wordAtPoint = rootNode.descendantForPosition({row: 0, column: Math.max(line.length - 1, 0)})
+ return {
+ line: input,
+ word: wordAtPoint,
+ lineRootNode: rootNode,
+ lineCurrentNode: lastNode
+ }
+}
+
+
+
+setLogger(
+ async () => {
+ parser = await initializeParser();
+ completionMap = await CompletionItemMap.initialize()
+ },
+ async () => {
+ if (parser) parser.delete();
+ }
+)
+
+function lineSignatureBuilder(lineRootNode: SyntaxNode, lineCurrentNode: SyntaxNode): SignatureHelp | null {
+ const currentCmd = NodeTypes.findParentCommand(lineCurrentNode);
+ const pipes = getPipes(lineRootNode);
+ const varNode = getVariableNode(lineRootNode);
+ const allCmds = getAllCommands(lineRootNode);
+ const regexOption = getRegexOption(lineRootNode);
+
+ if (!currentCmd && pipes.length === 1) return getPipesSignature(pipes);
+
+ switch (true) {
+ case currentCmd && isStringWithRegex(currentCmd.text, regexOption):
+ return getDefaultSignatures();
+
+ case varNode && isSetOrReadWithVarNode(currentCmd?.text || lineRootNode.text, varNode, lineRootNode, allCmds):
+ return getSignatureForVariable(varNode);
+
+ case currentCmd?.text.startsWith('return') || lineRootNode.text.startsWith('return'):
+ return getReturnStatusSignature();
+
+ // case currentCmd &&
+ case currentCmd && allCmds.length === 1:
+ return getCommandSignature(currentCmd);
+
+ default:
+ return null;
+ }
+}
+
+function getPipes(rootNode: SyntaxNode): ExtendedBaseJson[] {
+ const pipeNames = PrebuiltDocumentationMap.getByType('pipe');
+ return TreeSitter.getChildNodes(rootNode).reduce((acc: ExtendedBaseJson[], node) => {
+ const pipe = pipeNames.find(p => p.name === node.text);
+ if (pipe) acc.push(pipe);
+ return acc;
+ }, []);
+}
+
+function getVariableNode(rootNode: SyntaxNode): SyntaxNode | undefined {
+ return TreeSitter.getChildNodes(rootNode).find(c => NodeTypes.isVariableDefinition(c));
+}
+
+function getAllCommands(rootNode: SyntaxNode): SyntaxNode[] {
+ return TreeSitter.getChildNodes(rootNode).filter(c => NodeTypes.isCommand(c));
+}
+
+function getRegexOption(rootNode: SyntaxNode): SyntaxNode | undefined {
+ return TreeSitter.getChildNodes(rootNode).find(n => NodeTypes.isMatchingOption(n, { shortOption: '-r', longOption: '--regex' }));
+}
+
+function isStringWithRegex(line: string, regexOption: SyntaxNode | undefined): boolean {
+ return line.startsWith('string') && !!regexOption;
+}
+
+function isSetOrReadWithVarNode(line: string, varNode: SyntaxNode | undefined, rootNode: SyntaxNode, allCmds: SyntaxNode[]): boolean {
+ return !!varNode && (line.startsWith('set') || line.startsWith('read')) && allCmds.pop()?.text === rootNode.text.trim();
+}
+
+function getSignatureForVariable(varNode: SyntaxNode): SignatureHelp | null {
+ const output = getCurrentNodeType(varNode.text);
+ if (!output) return null;
+ return {
+ signatures: [buildSignature(output.name, output.description)],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+function getReturnStatusSignature(): SignatureHelp {
+ const output = PrebuiltDocumentationMap.getByType('status').map((o: ExtendedBaseJson) => `___${o.name}___ - _${o.description}_`).join('\n')
+ return {
+ signatures: [buildSignature('$status', output)],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+function getPipesSignature(pipes: ExtendedBaseJson[]): SignatureHelp {
+ return {
+ signatures: pipes.map((o: ExtendedBaseJson) => buildSignature(o.name, `${o.name} - _${o.description}_`)),
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+function getCommandSignature(firstCmd: SyntaxNode): SignatureHelp {
+ const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text);
+ return {
+ signatures: [buildSignature(firstCmd.text, output.map((o: ExtendedBaseJson) => `${o.name} - _${o.description}_`).join('\n'))],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+
+function getAliasedCompletionItemSignature(item: FishAliasCompletionItem): SignatureHelp {
+ // const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text);
+ return {
+ signatures: [buildSignature(item.label, [
+ '```fish',
+ `${item.fishKind} ${item.label} ${item.detail}`,
+ '```'
+ ].join('\n'))],
+ activeSignature: 0,
+ activeParameter: 0,
+ };
+}
+describe('signature test-suite', () => {
+
+ it('`variable` signature from snippets/*.json', () => {
+ const { line, lineRootNode, lineCurrentNode } = analyzerParseCurrentLine('set -gx fish_lsp_enabled_handlers')
+ const signature = lineSignatureBuilder(lineRootNode, lineCurrentNode, completionMap)!
+ // console.log(JSON.stringify(signature, null, 2));
+ expect(signature.signatures[0]!.label).toBe('fish_lsp_enabled_handlers')
+ })
+
+ it('`function` signature from snippets/*.json', () => {
+ const { line, lineRootNode, lineCurrentNode } = analyzerParseCurrentLine('fish_prompt ')
+ const signature = lineSignatureBuilder(lineRootNode, lineCurrentNode, completionMap)!
+
+ // console.log(JSON.stringify(signature, null, 2));
+ expect(signature.signatures[0]!.label).toBe('fish_prompt')
+ })
+
+ it('`pipes` signature from snippets/*.json', () => {
+ const { line, lineRootNode, lineCurrentNode } = analyzerParseCurrentLine('alias 2>> ')
+ const signature = lineSignatureBuilder(lineRootNode, lineCurrentNode, completionMap)!
+
+ // console.log(JSON.stringify(signature, null, 2));
+ expect(signature.signatures[0]!.label).toBe('2>>')
+
+ })
+
+ it('`return $status` from snippets/*.json', () => {
+ const { lineRootNode, lineCurrentNode } = analyzerParseCurrentLine('return ');
+ const signature = lineSignatureBuilder(lineRootNode, lineCurrentNode, completionMap)!;
+ console.log(JSON.stringify(signature, null, 2));
+ expect(signature.signatures[0]!.label).toEqual('$status');
+ })
+
+ it('`string --regex _`', () => {
+ const { line, lineRootNode, lineCurrentNode } = analyzerParseCurrentLine('if string match -re "^-.*" "$argv"')
+ const currentCmd = NodeTypes.findParentCommand(lineCurrentNode)!
+
+ // console.log(currentCmd.text)
+ const signature = lineSignatureBuilder(currentCmd, lineCurrentNode, completionMap)!
+ // console.log(JSON.stringify(signature, null, 2));
+ expect(signature.signatures.length).toBe(2)
+ })
+
+
+ // it('`function NAME --argument-names a b`', async () => {
+ // const completionMap = await CompletionItemMap.initialize()
+ // const fn = completionMap.allOfKinds('function').find(f => f.label === 'popd_duplicates')!
+ // const documentation = await getDocumentationResolver(fn)
+ // const signature = buildSignature(fn.label, documentation.value)
+ // console.log(signature);
+ // })
+
+ it('`alias NAME`', async () => {
+ const completionMap = await CompletionItemMap.initialize()
+ const aliases = completionMap.allOfKinds('alias')
+ console.log(JSON.stringify(getAliasedCompletionItemSignature(aliases.find(a => a.label === 'vimdiff')!), null, 2));
+ })
+
+ it('updates `activeSignature` and `activeParameter`', () => {
+ const { lineRootNode, lineCurrentNode } = analyzerParseCurrentLine('set -gx fish_lsp_enabled_handlers value1 value2');
+ const signature = lineSignatureBuilder(lineRootNode, lineCurrentNode, completionMap)!;
+ signature.activeSignature = 1; // Focusing on the second signature if multiple
+ signature.activeParameter = 2; // Focusing on the third parameter
+ expect(signature.activeSignature).toBe(1);
+ expect(signature.activeParameter).toBe(2);
+ });
+
+ it('does not show test command if set is the most recent', () => {
+ const { lineRootNode, lineCurrentNode } = analyzerParseCurrentLine('if test -n $argv; and set -q CMD_DURATION');
+ const currentCmd = NodeTypes.findParentCommand(lineCurrentNode)!
+ const signature = lineSignatureBuilder(currentCmd, lineCurrentNode, completionMap)!;
+ console.log(JSON.stringify(signature, null, 2));
+ expect(signature.signatures[0]!.label).toBe('CMD_DURATION'); // Expecting the `set` command signature
+ });
+
+})
+
+
diff --git a/test-data/tree-sitter.test.ts b/test-data/tree-sitter.test.ts
new file mode 100644
index 00000000..17cb8985
--- /dev/null
+++ b/test-data/tree-sitter.test.ts
@@ -0,0 +1,356 @@
+import Parser, { SyntaxNode, Tree, Point } from 'web-tree-sitter';
+import {
+ getChildNodes,
+ getNamedChildNodes,
+ findChildNodes,
+ getParentNodes,
+ findFirstParent,
+ getSiblingNodes,
+ findFirstNamedSibling,
+ findFirstSibling,
+ findEnclosingScope,
+ getNodeText,
+ getNodesTextAsSingleLine,
+ firstAncestorMatch,
+ ancestorMatch,
+ descendantMatch,
+ hasNode,
+ getNamedNeighbors,
+ getRange,
+ findNodeAt,
+ equalRanges,
+ getNodeAt,
+ getNodeAtRange,
+ positionToPoint,
+ pointToPosition,
+ rangeToPoint,
+ getRangeWithPrecedingComments,
+ getPrecedingComments,
+ isFishExtension,
+ isPositionWithinRange,
+ isPositionAfter,
+ isNodeWithinRange,
+ getLeafs,
+ getLastLeaf,
+ matchesArgument,
+ getCommandArgumentValue
+} from '../src/utils/tree-sitter';
+import { initializeParser } from '../src/parser';
+import * as NodeTypes from '../src/utils/node-types';
+
+function parseString(str: string): Parser.Tree {
+ const tree = parser.parse(str);
+ return tree;
+}
+
+function parseStringForNode(str: string, predicate: (n: SyntaxNode) => boolean) {
+ const tree = parseString(str);
+ const { rootNode } = tree;
+ return getChildNodes(rootNode).filter(predicate);
+}
+
+let parser: Parser;
+const jestConsole = console;
+
+beforeEach(async () => {
+ parser = await initializeParser();
+ global.console = require("console");
+});
+
+afterEach(() => {
+ global.console = jestConsole;
+ if (parser) parser.delete();
+});
+
+describe('tree-sitter.ts functions testing', () => {
+ let mockRootNode: SyntaxNode;
+
+ test('getChildNodes returns all child nodes', () => {
+ mockRootNode = parseString('set -gx a "1" "2" "3"').rootNode;
+ const result = getChildNodes(mockRootNode);
+ expect(result.length).toBe(15);
+ });
+
+ test('getNamedChildNodes returns all named child nodes', () => {
+ mockRootNode = parseString('set -gx a "1" "2" "3"').rootNode;
+ const result = getNamedChildNodes(mockRootNode);
+ expect(result.length).toBe(8);
+ expect(result.map(n => n.type)).toEqual([
+ 'program',
+ 'command',
+ 'word',
+ 'word',
+ 'word',
+ 'double_quote_string',
+ 'double_quote_string',
+ 'double_quote_string'
+ ]);
+ });
+ test('findChildNodes returns nodes matching predicate', () => {
+ // const predicate = (node: SyntaxNode) => node.type === 'targetType';
+ mockRootNode = parseString('set -gx a "1" "2" "3"').rootNode;
+ const result = findChildNodes(mockRootNode, NodeTypes.isCommand);
+ expect(result.map(f => f.text)).toEqual([ 'set -gx a "1" "2" "3"' ]);
+ const resultName = findChildNodes(mockRootNode, NodeTypes.isCommandName);
+ expect(resultName.map(f => f.text)).toEqual([ 'set' ]);
+ });
+
+ test('getParentNodes returns all parent nodes', () => {
+ const node = parseStringForNode('set -gx a "1" "2" "3"', (n: SyntaxNode) => n.text === '"3"').pop()!;
+ const results = getParentNodes(node);
+ expect(results.map(n => n.text)).toEqual([ '"3"', 'set -gx a "1" "2" "3"', 'set -gx a "1" "2" "3"' ]);
+ expect(results.map(n => n.type)).toEqual([ 'double_quote_string', 'command', 'program' ]);
+ });
+
+ test('findFirstParent returns first parent node matching predicate', () => {
+ const node = parseStringForNode('set -gx a "1" "2" "3"', (n: SyntaxNode) => n.text === '"3"').pop()!;
+ const result = findFirstParent(node, NodeTypes.isCommand);
+ expect(result?.text).toEqual('set -gx a "1" "2" "3"');
+ });
+
+ test('getSiblingNodes returns sibling nodes', () => {
+ const node = parseStringForNode('set -gx a "1" "2" "3"', (n: SyntaxNode) => n.text === '"3"').pop()!;
+ const result = getSiblingNodes(node, NodeTypes.isString, 'before');
+ expect(result.map(t => t.text)).toEqual([ '"2"', '"1"' ]);
+ });
+
+ test('findFirstNamedSibling returns first named sibling node', () => {
+ const node = parseStringForNode('set -gx a "1" "2" "3"', (n: SyntaxNode) => n.text === '"3"').pop()!;
+ const result = findFirstNamedSibling(node, NodeTypes.isVariableDefinitionName)!;
+ expect(result.text).toEqual('a');
+ });
+
+ test('findFirstSibling returns first sibling node', () => {
+ const node = parseStringForNode('set -gx a "1" "2" "3"', (n: SyntaxNode) => n.text === '"3"').pop()!;
+ const result = findFirstSibling(node, NodeTypes.isOption, 'before')!;
+ expect(result.text).toEqual('-gx');
+ });
+
+ test('findEnclosingScope returns enclosing scope node', () => {
+ const node = parseStringForNode([
+ 'function __func_1',
+ ' if test -z $argv',
+ ' return 0',
+ ' end',
+ ' set -gx a "1" "2" "3"',
+ 'end',
+ ].join('\n'), (n: SyntaxNode) => n.text === '"3"').pop()!;
+ const result = findEnclosingScope(node);
+ expect(result.type).toEqual('function_definition');
+ });
+
+ test('getNodeText returns text of the node', () => {
+ const input = [
+ 'function __func_1',
+ ' if test -z $argv',
+ ' return 0',
+ ' end',
+ ' set -gx a "1" "2" "3"',
+ 'end',
+ ].join('\n');
+ let node = parseStringForNode(input, (n: SyntaxNode) => n.text === '"3"').pop()!;
+ let result = getNodeText(node);
+ expect(result).toEqual('"3"');
+ node = parseStringForNode(input, (n: SyntaxNode) => n.text === '__func_1').pop()!;
+ result = getNodeText(node);
+ expect(result).toEqual('__func_1');
+
+ node = parseStringForNode(input, NodeTypes.isFunctionDefinition).pop()!;
+ result = getNodeText(node);
+ // console.log(result);
+ expect(result).toEqual('__func_1');
+
+ });
+
+ // test('getNodesTextAsSingleLine returns concatenated text of nodes', () => {
+ // const result = getNodesTextAsSingleLine([mockRootNode]);
+ // // Add assertions here
+ // });
+ //
+ test('firstAncestorMatch returns first ancestor matching predicate', () => {
+ const input = [
+ 'function __func_1',
+ ' if test -z $argv',
+ ' return 0',
+ ' end',
+ ' set -gx a "1" "2" "3"',
+ 'end',
+ ].join('\n');
+ let node = parseStringForNode(input, (n: SyntaxNode) => n.text === '"3"').pop()!;
+ const result = firstAncestorMatch(node, NodeTypes.isCommand)!;
+ expect(result.text).toEqual('set -gx a "1" "2" "3"');
+ });
+
+ test('ancestorMatch returns all matching ancestor nodes', () => {
+ let node = parseStringForNode('set -gx a "1" "2" "3"', (n: SyntaxNode) => n.text === '"3"').pop()!;
+ let result = ancestorMatch(node, NodeTypes.isOption, false);
+ expect(result.map(n => n.text)).toEqual([
+ '-gx',
+ '-gx'
+ ]);
+ });
+
+ test('descendantMatch returns all matching descendant nodes', () => {
+ const node = parseStringForNode('set -gx a "1" "2" "3"', NodeTypes.isCommand).pop()!;
+ const result = descendantMatch(node, NodeTypes.isVariableDefinitionName);
+ expect(result.map(n => n.text)).toEqual([ 'a' ]);
+ });
+
+ test('hasNode checks if array has the node', () => {
+ const root = parseString('set -gx a "1" "2" "3"').rootNode;
+ const node = getChildNodes(root).find(n => NodeTypes.isOption(n))!;
+ // const node = parseStringForNode('set -gx a "1" "2" "3"', NodeTypes.isCommand).pop()!
+ const result = hasNode(getChildNodes(root), node);
+ expect(result).toBeTruthy();
+ });
+
+ test('getNamedNeighbors returns named neighbors', () => {
+ const root = parseString('set -gx a "1" "2" "3"').rootNode;
+ const node = getChildNodes(root).find(n => NodeTypes.isOption(n))!;
+ const result = getNamedNeighbors(node);
+ expect(result.map(n => n.text)).toEqual([ 'set', '-gx', 'a', '"1"', '"2"', '"3"' ]);
+ });
+
+ test('getRange returns range of the node', () => {
+ const root = parseString('set -gx a "1" "2" "3"').rootNode;
+ const node = getChildNodes(root).find(n => NodeTypes.isOption(n))!;
+ expect(getRange(root)).toEqual({ start: { line: 0, character: 0 }, end: { line: 0, character: 21 } });
+ expect(getRange(node)).toEqual({ start: { line: 0, character: 4 }, end: { line: 0, character: 7 } });
+ });
+
+ test('findNodeAt finds node at position', () => {
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const result = findNodeAt(tree, 0, 5)!;
+ expect(result.text).toEqual('-gx');
+ });
+ //
+ test('equalRanges checks if ranges are equal', () => {
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const rootNode = tree!.rootNode;
+
+ const rangeA = { start: { line: 0, character: 0 }, end: { line: 0, character: 21 } };
+ const rangeB = getRange(rootNode);
+ const result = equalRanges(rangeA, rangeB);
+ expect(result).toBeTruthy();
+ });
+
+ test('getNodeAt finds node at position', () => {
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const result = getNodeAt(tree, 0, 0)!;
+ expect(result.text).toBe('set');
+ });
+
+ test('getNodeAtRange finds node at range', () => {
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const rootNode = tree!.rootNode;
+ const range = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
+ const result = getNodeAtRange(rootNode, range)!;
+ expect(result.text).toBe('set');
+ // console.log(result.text);
+ });
+
+ test('positionToPoint converts position to point', () => {
+ const position = { line: 0, character: 5 };
+ const start = positionToPoint(position);
+ const end = positionToPoint(position);
+ expect(positionToPoint(position)).toEqual({
+ row: 0,
+ column: 5
+ });
+ });
+
+ test('pointToPosition converts point to position', () => {
+ const point = { row: 0, column: 1 };
+ const result = pointToPosition(point);
+ expect(result).toEqual({
+ line: 0,
+ character: 1
+ });
+ });
+
+ test('rangeToPoint converts range to point', () => {
+ const range = { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } };
+ const result = rangeToPoint(range);
+ expect(result).toEqual({
+ row: 0,
+ column: 0
+ });
+ });
+
+ // test('getRangeWithPrecedingComments returns range with preceding comments', () => {
+ // const result = getRangeWithPrecedingComments(mockRootNode);
+ // // Add assertions here
+ // });
+ //
+ // test('getPrecedingComments returns preceding comments', () => {
+ // const result = getPrecedingComments(mockRootNode);
+ // // Add assertions here
+ // });
+ //
+ test('isFishExtension checks if path has fish extension', () => {
+ const result = isFishExtension('file:///home/user/.config/fish/functions/test.fish');
+ expect(result).toBeTruthy();
+ });
+
+ test('isPositionWithinRange checks if position is within range', () => {
+
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const rootNode = tree!.rootNode;
+ const position = { line: 0, character: 0 };
+ const range = getRange(rootNode);
+ const result = isPositionWithinRange(position, range);
+ expect(result).toBeTruthy();
+ });
+
+ test('isPositionAfter checks if position is after another position', () => {
+ const positionA = { line: 0, character: 0 };
+ const positionB = { line: 0, character: 5 };
+ const result = isPositionAfter(positionA, positionB);
+ expect(result).toBeTruthy();
+ });
+
+ test('isNodeWithinRange checks if node is within range', () => {
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const rootNode = tree!.rootNode;
+ const range = { start: { line: 0, character: 0 }, end: { line: 0, character: 21 } };
+ const result = isNodeWithinRange(rootNode.firstNamedChild!, range);
+ expect(result).toBeTruthy();
+ });
+
+ test('getLeafs returns leaf nodes', () => {
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const rootNode = tree!.rootNode;
+ const result = getLeafs(rootNode);
+
+ expect(result.map(m => m.text)).toEqual([
+ 'set', '-gx', 'a',
+ '"', '"', '"',
+ '"', '"', '"'
+ ]);
+ });
+
+ test('getLastLeaf returns last leaf node', () => {
+ const tree = parseString('set -gx a "1" "2" "3"');
+ const rootNode = tree!.rootNode;
+ const result = getLastLeaf(rootNode);
+ expect(result.text).toEqual('"');
+ });
+
+ // it('matchesTypes', () => {
+ // const tree = parseString('set -gx a "1" "2" "3"');
+ // const rootNode = tree!.rootNode;
+ // getChildNodes(rootNode).forEach((child) => {
+ // console.log(child.grammarType, child.grammarType);
+ // })
+ //
+ // });
+ // test('matchesArgument checks if node matches argument', () => {
+ // const result = matchesArgument(mockRootNode, 'arg');
+ // // Add assertions here
+ // });
+ //
+ // test('getCommandArgumentValue returns command argument value', () => {
+ // const result = getCommandArgumentValue(mockRootNode, 'arg');
+ // // Add assertions here
+ // });
+});
diff --git a/tsconfig.json b/tsconfig.json
index e71085d7..04ce76bf 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -12,7 +12,6 @@
"@package": ["./package.json"],
// "@snippets/*": ["./snippets/*"],
// "@src/*": ["./src/*"],
- "@utils/*": ["./src/utils/*"]
}
},
"include": [
@@ -23,4 +22,4 @@
"node_modules",
"out"
]
-}
\ No newline at end of file
+}