Contributing :bustsinsilhouette:
June 3, 2026 ยท View on GitHub
The fish-lsp aims to create an experience that aligns with the fish 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 :pray:
There are many ways that contributions can be made to the fish-lsp, including:
- fish-lsp.dev - Home website {url: https://fish-lsp.dev}
- Add new documentation, or improve existing documentation
- Add gifs/images that are reused across multiple of the projects repositories in the public/ directory
- fish-lsp-language-clients - Client configurations repository
- add or update how to configure a client on different branches
- vscode-fish-lsp - VSCode extension repository (VSCode client code base)
- Add or improve VSCode related client features/documentation
- fish-lsp - Main repository
- Add tests to verify expected behavior
- Implement new features (some are outlined in the ROADMAP.md file)
- Fix bugs/issues
- Improve documentation, by adding to the README.md or WIKI
- Add gh-actions/workflows to the project, that help automate the development process
Important
Below, we primarily focus on documenting how to contribute to the main fish-lsp repository.
Getting started :rocket:
-
Begin by forking the project, then build your local fork :card_file_box:.
INSTRUCTIONS: how to build from source - compiling your local version of the project
-
Step 1: Get the dependencies installed by running the command:
yarn install -
Step 2: Then, you can build the project by running the command:
yarn build # or `yarn build:watch` for continuously recompiling the project on any changes -
Step 3: Finally, you can verify the global
fish-lspcommand is linked to the local version of the project by running:fish-lsp info
-
-
Once you have installed the local fork of the project (i.e., you have a successfully compiled
fish-lspexecutable, and have a working client configuration), you can then begin testing locally :memo:. -
Upon completing a change, submit a PR :tada:.
Places to Start :checkered_flag:
- Roadmap - future ideas to support
- Issues and discussions - get ideas from others
- Sources - helpful insight about potential features you want to adapt
Note
Browsing both wiki/sources#vscode-extensions-examples and ROADMAP are the easiest method for understanding how to create future fish-lsp feature's
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). Many tree-sitter helper functions (tree-sitter.ts, and node-types.ts) have already been written, to aid in providing useful functionality for generic support of any possible combination need for future types.
Having said that, if you a need for a new definition in tree-sitter.ts or node-types.ts comes up, adding it to the proper file is fine (tree-sitter.ts generally 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)
Sceenshot

Integration Testing Workflow :exploding_head:
Test directly in the client 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 other more specific tasks.
Screenshot

How to Build using these Workflows :building_construction:
-
Pull up some Documentation :microscope:
- lsif - The official Language Server Protocol specification
- wiki/sources - Sources that are similar to this project
- roadmap - Ideas/Documentation for future plans
-
Create a
filein the tests/ directory :construction_worker:-
START WITH VERY BASIC EXAMPLES!!! Pure functions are your friend
-
Checkout ./tests/helpers.ts,
setLogger()which is provided forloggingtests -
Test your
FILE.test.tswith command:yarn test FILE --run -
Feel free to overwrite any existing
tests/file -
Use
import { initializeParser } from '../src/parserfor buildingSyntaxNode[]composite object arrays (aka trees). -
(Recommended instead of directly using parser) Use
import { Analyzer, analyzer } from '../src/analyzer'for setting up a test suite's analyzer object, which exposes how the server actual parses input using tree-sitter. An example test suite is shown below:import * as Parser from 'web-tree-sitter' import { initializeParser } from '../src/parser' import { Analyzer, analyzer } from '../src/analyzer' import { TestFile, TestWorkspace } from './test-workspace'; describe('example test suite', () => { let parser: Parser; let analyzer: Analyzer; beforeEach(async () => { // In most tests, there is no reason to use the parser directly because the analyzer function wraps it /** parser = await initializeParser() */ await Analyzer.initialize() }) // EXAMPLE to setup a test workspace with files, we can parse. // This handles creating a temporary directory, and cleaning it up after the tests // and makes sure that code related to `Workspace` and `Document` // behave the same as a normal server session expects. const workspace = TestWorkspace.create({name: 'example_folder'}).addFiles( TestFile.create('example_file.fish', 'echo "hello world"'), // example_folder/example_file.fish TestFile.function('name', 'function name; echo "hello from name function"; end;'), // example_folder/example_file.fish TestFile.completion('name', 'complete -c name -f'), // example_folder/completions/name.fish ).initialize() it('example test case', () => { const doc = workspace.getDocument('example_file.fish') const { tree } = analyzer.analyze(doc) // your test code here expect(tree.rootNode.type).toBe('source_file') }) }) -
More examples of setting up test workspaces, in a test suite
import { setLogger } from './helpers'; import { TestFile, TestWorkspace } from './test-workspace'; describe('example test suite', () => { setLogger(); // console.log() will be captured in test output // logger.setSilent(); // will suppress all logging output describe('test workspace 1', () => { const workspace = TestWorkspace.create({name: 'example_folder'}) .inheritFilesFromExistingAutoloadedWorkspace('$__fish_data_dir') .initialize() it('see files in workspace', () => { const files = workspace.getAllFiles() for (const file of files) { console.log(file.uri) } expect(files.length).toBeGreaterThan(0) }) }) describe('test workspace 2', () => { // assuming a fish workspace exists already in `tests/workspaces/<FOLDER_NAME>` const workspace = TestWorkspace.read('test_workspace_2') .initialize() it('see files in workspace', () => { const files = workspace.getAllFiles() for (const file of files) { console.log(file.uri) } expect(files.length).toBeGreaterThan(0) }) }) /* ... more tests ... */ })
-
-
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)# display the logs tail -f $(fish-lsp info --log-file) -
Alternatively, you can mock the data-type from the
vscode-languageserveror refer to the same documentation on lsif
-
-
Add your feature to a server.ts
handler:handshake:- Document your handler, if necessary.
- Feel free to submit your server handler in separate working release stages, instead of trying to build entire feature's independently. (i.e., if your
CodeAction'sonly support a singularCodeActionType) - Submit your PR :champagne:
Helpful Topics and Concepts :books:
Currying is a useful design pattern, that makes iterating through the Abstract Syntax Trees (ASTs) 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 functors 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 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
Child process execution via sub-shells. Sub-shell environment's are extensively relied on throughout the code base.
Markdown formatting syntax, and nested language support via triple backticks.
Asynchronous processes and race conditions. Especially during src/server.ts startup.
Prefetching relevant information and caching it for global use.
Important Tooling Provided :toolbox:
-
tree-sitter - used for data structures/algorithms, prevalent to the shell language.
@ndonfris/tree-sitter-fish@3.6.0- handles installing the actual tree-sitter-fish.wasm packageweb-tree-sitter- is the API forSyntaxNode[],Parser,Range, etc...
-
eslint - used for linting and formatting
yarn lint- lint and fix the current project (husky pre-pushhook)yarn lint:check- check the current projectyarn lint:verbose- lint, and display output
-
knip - used for tree-shaking and checking unused dependencies
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 - used for src/cli.ts and other tooling to start the server
- Handles parsing the ./dist/fish-lsp
stdin, in a structured manor
- Handles parsing the ./dist/fish-lsp
-
zod - parses the
envinto a typesafe object- handles parsing the
fish_lsp*variables in our nodeprocess.envobject - builds the result object in the global variable
config
- handles parsing the
-
vscode-languageserver - the SPEC for defining our LSP.
Objects&Interfacesspecific tofish-lsptypically extend this base specificationType Definitionsuseful for handler's are defined throughout this package
-
husky - the git-hooks for interacting with project's source code
- lints the project
on-push - removes dependencies before commit
pre-commit - initializes yarn
post-merge
- lints the project
-
vitest - testing the project
- relevant locations: tests/*.test.ts, vitest.config.ts
yarn testruns the tests, watching for any changes, of all thetests/*.test.tsfiles,yarn test tests/someFile.test.tsis the designated method for watching a specific test's changesyarn test:runis a shorthand for running vitest on all tests, or you can specify a file to test viayarn test:run tests/someFile.test.ts(this will not watch for changes)yarn test:coverageis used to generate a coverage report for the project's testsyarn test:coverage:uiis used to display the coverage report in a browser
-
esbuild - used for bundling the project
- relevant locations: scripts/build.ts, scripts/esbuild/*
yarn devrunstsx scripts/build.tsto create a production build of the project in thedist/directory. Use this command to pass flags to thescripts/build.tsfile (see,yarn dev --help)yarn buildruns all build steps, and build all esbuild files, potentially with the modules and dependencies bundledyarn build:watchrunsyarn build, and watches for changes to recompile the project automaticallyyarn build:watch-allis similar toyarn build:watch, but will re-run the entireyarn buildcommand on any changes in the project- NOTE: depending on the version you of the server you are targeting when building (
bin/fish-lsp,dist/fish-lsp, etc...), esbuild may or may not bundle and/or embed dependencies into the final output file.
Other Noteworthy Tooling :hammer_and_wrench:
Becoming familiar with using the src/utils/{tree-sitter,node-types}.ts code, is significantly easier while using the previously mentioned TDD Workflow.
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 command, to help you with debugging and testing your changes!
Some noteworthy use cases include:
fish-lsp start --dump,fish-lsp env --show-defaults,fish-lsp info --time-startup,fish-lsp info --check-health,fish-lsp url --sources,fish-lsp complete, + more...See the wiki's
abbrpage for speeding up interactions with thefish-lspcommand.
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 settings, as seen in the JSON example, is only for ease of use.
Adding new client configurations, to the fish-lsp-client's repo, is greatly appreciated!
Contributing Github Actions :recycle:
If you're trying to add a new github action to the project, please take a close look at the scripts/* directory, along with package.json.
A github action that uses that compiles the project, requires fish to be installed and setup, before yarn in the action.
The current workflow actions, are the best place to see how this is achieved.
Running the Test Suite workflow locally
Use gh act when you want to verify the workflow bootstrap from an act runner image, not just the already-configured local machine:
gh act push \
-W .github/workflows/test-suite.yml \
-j test-suite \
-P ubuntu-latest=catthehacker/ubuntu:act-latest \
--container-architecture linux/amd64
The workflow itself runs on GitHub's ubuntu-latest runner, installs fish 4.x, Node from .nvmrc, Yarn 1.22.22, manpage dependencies, generated build assets, and then yarn test:ci:run. For faster test iteration while editing tests/**, run yarn test:ci:run directly and reserve gh act for checking the CI bootstrap.
If gh act should test only committed files after the workflow is merged, add --no-skip-checkout. Leave that flag off when validating uncommitted local changes.
Got helpful scripts? :passport_control:
Show & tell 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 :)