diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..91323fb --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,135 @@ +name: build and release + +permissions: + contents: write + +on: + release: + types: [ published ] + +jobs: + build-server: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check git version matches flake version + shell: bash + run: | + if ! [ $(echo '${{ github.ref }}' | cut -d'v' -f 2) = $(grep '# CI:CD-VERSION$' flake.nix | cut -d'"' -f 2) ]; + then + echo "Version mismatch between Git and flake" + exit 1 + fi + + - name: Check version in code matches flake version + shell: bash + run: | + if ! [ $(grep '// CI:CD-VERSION$' server/root-handler/common.go | cut -d'"' -f 2) = $(grep '# CI:CD-VERSION$' flake.nix | cut -d'"' -f 2) ]; + then + echo "Version mismatch between code and flake" + exit 1 + fi + + - name: Check vs code package.json version matches flake version + shell: bash + run: | + if ! [ $(grep '"version": "' vs-code-extension/package.json | cut -d'"' -f 4) = $(grep '# CI:CD-VERSION$' flake.nix | cut -d'"' -f 2) ]; + then + echo "Version mismatch between vs code package.json and flake" + exit 1 + fi + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GH_CONFIGLSP_TOKEN }} + + build-extension: + name: Build extension for ${{ matrix.target }} + runs-on: ubuntu-latest + needs: + # Wait for server to build so that we know the checks have passed + - build-server + strategy: + fail-fast: false + matrix: + include: + - goos: linux + goarch: amd64 + vscode_target: linux-x64 + - goos: linux + goarch: arm64 + vscode_target: linux-arm64 + + - goos: darwin + goarch: amd64 + vscode_target: darwin-x64 + - goos: darwin + goarch: arm64 + vscode_target: darwin-arm64 + + - goos: windows + goarch: amd64 + vscode_target: win32-x64 + - goos: windows + goarch: arm64 + vscode_target: win32-arm64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: cachix/install-nix-action@v27 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Create bare extension + run: nix build .#"vs-code-extension-bare" + + - name: Build extension + run: cd server && GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -a -gcflags=all="-l -B" -ldflags="-s -w" -o config-lsp + + - name: Prepare folder + run: cp -rL result dist && chmod -R 777 dist + + - name: Move binary to extension + run: mv server/config-lsp dist/out/ + + - name: Shrink binary + if: ${{ matrix.goos == 'linux' }} + run: nix develop .#"vs-code-extension" --command bash -c "upx dist/out/config-lsp" + + - name: Package extension + run: nix develop .#"vs-code-extension" --command bash -c "cd dist && vsce package --target ${{ matrix.vscode_target }}" + + - name: Move vsix to root + run: mv dist/*.vsix . + + - uses: softprops/action-gh-release@v2 + with: + files: '*.vsix' + + - name: Upload extension to VS Code Marketplace + run: nix develop .#"vs-code-extension" --command bash -c "vsce publish --packagePath *.vsix -p ${{ secrets.VSCE_PAT }}" + diff --git a/.gitignore b/.gitignore index 73e8f25..d09a1c6 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ config-lsp result bin debug.log + +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..4e7fece --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,57 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +project_name: config-lsp +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + dir: ./server + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + # use zip for windows archives + format_overrides: + - goos: windows + format: zip + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +brews: + - name: config-lsp + homepage: "https://github.com/Myzel394/config-lsp" + description: "Finally a LSP for your config files: gitconfig, fstab, aliases, hosts, wireguard, ssh_config, sshd_config, and more to come!" + url_template: "https://github.com/Myzel394/config-lsp/releases/download/{{ .Tag }}/{{ .ArtifactName }}" + repository: + owner: Myzel394 + name: homebrew-formulae + branch: main + commit_author: + name: goreleaserbot + email: bot@goreleaser.com + commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" + directory: Formula + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index acbcf86..c6debff 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ +# config-lsp + +A language server for configuration files. The goal is to make editing config files modern and easy. + ## Supported Features | | diagnostics | `completion` | `hover` | `code-action` | `definition` | `rename` | `signature-help` | |-------------|-------------|--------------|---------|---------------|--------------|----------|------------------| -| aliases | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| fstab | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | 🟡 | -| hosts | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | 🟡 | -| sshd_config | ✅ | ✅ | ✅ | ❓ | ✅ | ❓ | 🟡 | -| wireguard | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | 🟡 | +| aliases | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| fstab | ✅ | ✅ | ✅ | ❓ | ❓ | ❓ | 🟡 | +| hosts | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | ✅ | +| ssh_config | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| sshd_config | ✅ | ✅ | ✅ | ❓ | ✅ | ❓ | ✅ | +| wireguard | ✅ | ✅ | ✅ | ✅ | ❓ | ❓ | 🟡 | ✅ = Supported @@ -14,3 +19,89 @@ ❓ = No idea what to implement here, please let me know if you have any ideas +## What further configs will be supported? + +As config-lsp is a hobby project and I'm working completely alone on it, +I will first focus on widely used and well known config files. + +You are welcome to request any config file, as far as it's fairly well known. + +## Installation + +### VS Code Extension + +[Install the extension from the marketplace](https://marketplace.visualstudio.com/items?itemName=myzel394.config-lsp) + +Alternatively, you can also manually install the extension: + +1. Download the latest extension version from the [release page](https://github.com/Myzel394/config-lsp/releases) - You can find the extension under the "assets" section. The filename ends with `.vsix` +2. Open VS Code +3. Open the extensions sidebar +4. In the top bar, click on the three dots and select "Install from VSIX..." +5. Select the just downloaded `.vsix` file +6. You may need to restart VS Code +7. Enjoy! + +### Manual installation + +To use `config-lsp` in any other editor, you'll need to install it manually. +Don't worry, it's easy! + +#### Installing the latest Binary + +##### Brew + +```sh +brew install myzel394/formulae/config-lsp +``` + +##### Manual Binary + +Download the latest binary from the [releases page](https://github.com/Myzel394/config-lsp/releases) and put it in your PATH. + +##### Compiling + +You can either compile the binary using go: + +```sh +go build -o config-lsp +``` + +or build it using Nix: + +```sh +nix flake build +``` + +#### Neovim installation + +Using [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) you can add `config-lsp` by adding the following to your `lsp.lua` (filename might differ): + +```lua +if not configs.config_lsp then + configs.config_lsp = { + default_config = { + cmd = { 'config-lsp' }, + filetypes = { + "sshconfig", + "sshdconfig", + "fstab", + "aliases", + -- Matches wireguard configs and /etc/hosts + "conf", + }, + root_dir = vim.loop.cwd, + }, + } +end + +lspconfig.config_lsp.setup {} +````` + +## Supporting config-lsp + +You can either contribute to the project, [see CONTRIBUTING.md](CONTRIBUTING.md), or you can sponsor me via [GitHub Sponsors](https://github.com/sponsors/Myzel394) or via [crypto currencies](https://github.com/Myzel394/contact-me?tab=readme-ov-file#donations). + +Oh and spreading the word about config-lsp is also a great way to support the project :) + + diff --git a/flake.lock b/flake.lock index 77814e0..652efe4 100644 --- a/flake.lock +++ b/flake.lock @@ -26,11 +26,11 @@ ] }, "locked": { - "lastModified": 1722589758, - "narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=", + "lastModified": 1728509152, + "narHash": "sha256-tQo1rg3TlwgyI8eHnLvZSlQx9d/o2Rb4oF16TfaTOw0=", "owner": "tweag", "repo": "gomod2nix", - "rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1", + "rev": "d5547e530464c562324f171006fc8f639aa01c9f", "type": "github" }, "original": { @@ -41,11 +41,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1723637854, - "narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=", + "lastModified": 1728888510, + "narHash": "sha256-nsNdSldaAyu6PE3YUA+YQLqUDJh+gRbBooMMekZJwvI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9", + "rev": "a3c0b3b21515f74fd2665903d4ce6bc4dc81c77c", "type": "github" }, "original": { @@ -97,11 +97,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index be65ac1..8f466eb 100644 --- a/flake.nix +++ b/flake.nix @@ -12,8 +12,18 @@ }; outputs = { self, nixpkgs, utils, gomod2nix }: - utils.lib.eachDefaultSystem (system: + utils.lib.eachSystem [ + "x86_64-linux" + "aarch64-linux" + + "x86_64-darwin" + "aarch64-darwin" + + "x86_64-windows" + "aarch64-windows" + ] (system: let + version = "0.1.2"; # CI:CD-VERSION pkgs = import nixpkgs { inherit system; overlays = [ @@ -27,21 +37,39 @@ inputs = [ pkgs.go_1_22 ]; - server = pkgs.buildGoModule { + serverUncompressed = pkgs.buildGoModule { nativeBuildInputs = inputs; pname = "github.com/Myzel394/config-lsp"; - version = "v0.0.1"; + version = version; src = ./server; - vendorHash = "sha256-s+sjOVvqU20+mbEFKg+RCD+dhScqSatk9eseX2IioPI"; + vendorHash = "sha256-eO1eY+2XuOCd/dKwgFtu05+bnn/Cv8ZbUIwRjCwJF+U="; + ldflags = [ "-s" "-w" ]; checkPhase = '' go test -v $(pwd)/... ''; }; + server = pkgs.stdenv.mkDerivation { + name = "config-lsp-${version}"; + src = serverUncompressed; + buildInputs = [ + pkgs.upx + ]; + buildPhase = '' + mkdir -p $out/bin + cp $src/bin/config-lsp $out/bin/ + chmod +rw $out/bin/config-lsp + + # upx is currently not supported for darwin + if [ "${system}" != "x86_64-darwin" ] && [ "${system}" != "aarch64-darwin" ]; then + upx --ultra-brute $out/bin/config-lsp + fi + ''; + }; in { packages = { default = server; - "vs-code-extension" = let - name = "config-lsp-vs-code-extension"; + "vs-code-extension-bare" = let + name = "config-lsp"; node-modules = pkgs.mkYarnPackage { src = ./vs-code-extension; name = name; @@ -50,13 +78,56 @@ yarnNix = ./vs-code-extension/yarn.nix; buildPhase = '' - yarn --offline run compile + yarn --offline run compile:prod ''; installPhase = '' - mv deps/${name}/out $out - cp ${server}/bin/config-lsp $out/ + mkdir -p extension + + # No idea why this is being created + rm deps/${name}/config-lsp + + cp -rL deps/${name}/. extension + + mkdir -p $out + cp -r extension/. $out ''; distPhase = "true"; + + buildInputs = [ + pkgs.vsce + ]; + }; + in node-modules; + "vs-code-extension" = let + name = "config-lsp"; + node-modules = pkgs.mkYarnPackage { + src = ./vs-code-extension; + name = name; + packageJSON = ./vs-code-extension/package.json; + yarnLock = ./vs-code-extension/yarn.lock; + yarnNix = ./vs-code-extension/yarn.nix; + + buildPhase = '' + yarn --offline run compile:prod + ''; + installPhase = '' + mkdir -p extension + + # No idea why this is being created + rm deps/${name}/config-lsp + + cp -rL deps/${name}/. extension + cp ${server}/bin/config-lsp extension/out/config-lsp + + cd extension && ${pkgs.vsce}/bin/vsce package + mkdir -p $out + cp *.vsix $out + ''; + distPhase = "true"; + + buildInputs = [ + pkgs.vsce + ]; }; in node-modules; }; @@ -72,6 +143,8 @@ devShells."vs-code-extension" = pkgs.mkShell { buildInputs = [ pkgs.nodejs + pkgs.vsce + pkgs.yarn2nix ]; }; } diff --git a/server/common-documentation/mnt-vboxsf.go b/server/common-documentation/mnt-vboxsf.go new file mode 100644 index 0000000..a2f1020 --- /dev/null +++ b/server/common-documentation/mnt-vboxsf.go @@ -0,0 +1,22 @@ +package commondocumentation + +import docvalues "config-lsp/doc-values" + +var VboxsfDocumentationAssignable = docvalues.MergeKeyEnumAssignmentMaps(FatDocumentationAssignable, map[docvalues.EnumString]docvalues.DeprecatedValue{ + docvalues.CreateEnumStringWithDoc( + "iocharset", + "This option sets the character set used for I/O operations. Note that on Linux guests, if the iocharset option is not specified, then the Guest Additions driver will attempt to use the character set specified by the CONFIG_NLS_DEFAULT kernel option. If this option is not set either, then UTF-8 is used.", + ): docvalues.EnumValue{ + EnforceValues: true, + Values: AvailableCharsets, + }, + docvalues.CreateEnumStringWithDoc( + "convertcp", + "This option specifies the character set used for the shared folder name. This is UTF-8 by default.", + ): docvalues.EnumValue{ + EnforceValues: true, + Values: AvailableCharsets, + }, +}) + +var VboxsfDocumentationEnums = []docvalues.EnumString{} diff --git a/server/doc-values/value-array.go b/server/doc-values/value-array.go index 3d330f7..5d9c7c4 100644 --- a/server/doc-values/value-array.go +++ b/server/doc-values/value-array.go @@ -3,6 +3,7 @@ package docvalues import ( "config-lsp/utils" "fmt" + "regexp" "strings" protocol "github.com/tliron/glsp/protocol_3_16" @@ -42,6 +43,9 @@ type ArrayValue struct { // This is used to extract the value from the user input, // because you may want to preprocess the value before checking for duplicates DuplicatesExtractor *(func(string) string) + + // If true, array ArrayValue ignores the `Separator` if it's within quotes + RespectQuotes bool } func (v ArrayValue) GetTypeDescription() []string { @@ -53,9 +57,18 @@ func (v ArrayValue) GetTypeDescription() []string { ) } +// TODO: Add support for quotes func (v ArrayValue) DeprecatedCheckIsValid(value string) []*InvalidValue { errors := []*InvalidValue{} - values := strings.Split(value, v.Separator) + var values []string + + if v.RespectQuotes { + splitPattern := *regexp.MustCompile(fmt.Sprintf(`".+?"|[^%s]+`, v.Separator)) + + values = splitPattern.FindAllString(value, -1) + } else { + values = strings.Split(value, v.Separator) + } if *v.DuplicatesExtractor != nil { valuesOccurrences := utils.SliceToMap( @@ -122,9 +135,27 @@ func (v ArrayValue) getCurrentValue(line string, cursor uint32) (string, uint32) MIN := uint32(0) MAX := uint32(len(line) - 1) + var cursorSearchStart = cursor + var cursorSearchEnd = cursor + var start uint32 var end uint32 + // Hello,world,how,are,you + // Hello,"world,how",are,you + if v.RespectQuotes { + quotes := utils.GetQuoteRanges(line) + + if len(quotes) > 0 { + quote := quotes.GetQuoteForIndex(int(cursor)) + + if quote != nil { + cursorSearchStart = uint32(quote[0]) + cursorSearchEnd = uint32(quote[1]) + } + } + } + // hello,w[o]rld,and,more // [h]ello,world // hello,[w]orld @@ -135,7 +166,7 @@ func (v ArrayValue) getCurrentValue(line string, cursor uint32) (string, uint32) relativePosition, found := utils.FindPreviousCharacter( line, v.Separator, - int(cursor), + int(cursorSearchStart), ) if found { @@ -151,7 +182,7 @@ func (v ArrayValue) getCurrentValue(line string, cursor uint32) (string, uint32) relativePosition, found = utils.FindNextCharacter( line, v.Separator, - int(start), + int(cursorSearchEnd), ) if found { diff --git a/server/go.mod b/server/go.mod index cb117fe..886d90f 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,17 +5,18 @@ go 1.22.5 require ( github.com/antlr4-go/antlr/v4 v4.13.1 github.com/emirpasic/gods v1.18.1 + github.com/google/go-cmp v0.6.0 + github.com/k0kubun/pp v3.0.1+incompatible github.com/tliron/commonlog v0.2.17 github.com/tliron/glsp v0.2.2 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - github.com/google/go-cmp v0.6.0 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/iancoleman/strcase v0.3.0 // indirect - github.com/k0kubun/pp v3.0.1+incompatible // indirect + github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/server/go.sum b/server/go.sum index 70ef9af..779ad9c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -13,6 +13,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= diff --git a/server/handlers/aliases/handlers/completions.go b/server/handlers/aliases/handlers/completions.go index b7d43a7..db9b0ef 100644 --- a/server/handlers/aliases/handlers/completions.go +++ b/server/handlers/aliases/handlers/completions.go @@ -66,14 +66,14 @@ func GetCompletionsForEntry( return completions, nil } - switch (*value).(type) { + switch value.(type) { case ast.AliasValueUser: return getUserCompletions( i, excludedUsers, ), nil case ast.AliasValueError: - errorValue := (*value).(ast.AliasValueError) + errorValue := value.(ast.AliasValueError) isAtErrorCode := errorValue.Code == nil && errorValue.Location.IsPositionAfterStart(cursor) && diff --git a/server/handlers/aliases/handlers/get-value.go b/server/handlers/aliases/handlers/get-value.go index ce6219b..1e658e7 100644 --- a/server/handlers/aliases/handlers/get-value.go +++ b/server/handlers/aliases/handlers/get-value.go @@ -9,7 +9,7 @@ import ( func GetValueAtPosition( position common.Position, entry *ast.AliasEntry, -) *ast.AliasValueInterface { +) ast.AliasValueInterface { if entry.Values == nil || len(entry.Values.Values) == 0 { return nil } @@ -36,5 +36,5 @@ func GetValueAtPosition( return nil } - return &entry.Values.Values[index] + return entry.Values.Values[index] } diff --git a/server/handlers/aliases/handlers/signature_help.go b/server/handlers/aliases/handlers/signature_help.go index b85bf73..fad0625 100644 --- a/server/handlers/aliases/handlers/signature_help.go +++ b/server/handlers/aliases/handlers/signature_help.go @@ -28,8 +28,8 @@ func GetRootSignatureHelp( }, { Label: []uint32{ - uint32(len(":")), - uint32(len(":") + len("")), + uint32(len(": ")), + uint32(len(": ") + len("")), }, Documentation: "A value to associate with the alias", }, diff --git a/server/handlers/aliases/lsp/text-document-completion.go b/server/handlers/aliases/lsp/text-document-completion.go index 1d5d429..2b6da3a 100644 --- a/server/handlers/aliases/lsp/text-document-completion.go +++ b/server/handlers/aliases/lsp/text-document-completion.go @@ -28,11 +28,7 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa entry := rawEntry.(*ast.AliasEntry) - if entry.Key == nil { - return handlers.GetAliasesCompletions(d.Indexes), nil - } - - if entry.Key.Location.ContainsPosition(cursor) { + if entry.Key == nil || entry.Key.Location.ContainsPosition(cursor) { return handlers.GetAliasesCompletions(d.Indexes), nil } @@ -40,13 +36,9 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa return nil, nil } - if entry.Separator.IsPositionBeforeEnd(cursor) { - return handlers.GetCompletionsForEntry( - cursor, - entry, - d.Indexes, - ) - } - - return nil, nil + return handlers.GetCompletionsForEntry( + cursor, + entry, + d.Indexes, + ) } diff --git a/server/handlers/aliases/lsp/text-document-definition.go b/server/handlers/aliases/lsp/text-document-definition.go index 5976f5d..5018568 100644 --- a/server/handlers/aliases/lsp/text-document-definition.go +++ b/server/handlers/aliases/lsp/text-document-definition.go @@ -32,7 +32,7 @@ func TextDocumentDefinition(context *glsp.Context, params *protocol.DefinitionPa return handlers.GetDefinitionLocationForValue( *d.Indexes, - *rawValue, + rawValue, params.TextDocument.URI, ), nil } diff --git a/server/handlers/aliases/lsp/text-document-hover.go b/server/handlers/aliases/lsp/text-document-hover.go index a1c97fd..638f524 100644 --- a/server/handlers/aliases/lsp/text-document-hover.go +++ b/server/handlers/aliases/lsp/text-document-hover.go @@ -49,10 +49,10 @@ func TextDocumentHover( } contents := []string{} - contents = append(contents, handlers.GetAliasValueTypeInfo(*value)...) + contents = append(contents, handlers.GetAliasValueTypeInfo(value)...) contents = append(contents, "") contents = append(contents, "#### Value") - contents = append(contents, handlers.GetAliasValueHoverInfo(*document.Indexes, *value)) + contents = append(contents, handlers.GetAliasValueHoverInfo(*document.Indexes, value)) text := strings.Join(contents, "\n") diff --git a/server/handlers/aliases/lsp/text-document-prepare-rename.go b/server/handlers/aliases/lsp/text-document-prepare-rename.go index 56e2dd1..c44fc3e 100644 --- a/server/handlers/aliases/lsp/text-document-prepare-rename.go +++ b/server/handlers/aliases/lsp/text-document-prepare-rename.go @@ -34,9 +34,9 @@ func TextDocumentPrepareRename(context *glsp.Context, params *protocol.PrepareRe return nil, nil } - switch (*rawValue).(type) { + switch rawValue.(type) { case ast.AliasValueUser: - userValue := (*rawValue).(ast.AliasValueUser) + userValue := rawValue.(ast.AliasValueUser) return userValue.Location.ToLSPRange(), nil } diff --git a/server/handlers/aliases/lsp/text-document-rename.go b/server/handlers/aliases/lsp/text-document-rename.go index d51f092..61d1c69 100644 --- a/server/handlers/aliases/lsp/text-document-rename.go +++ b/server/handlers/aliases/lsp/text-document-rename.go @@ -44,9 +44,9 @@ func TextDocumentRename(context *glsp.Context, params *protocol.RenameParams) (* return nil, nil } - switch (*rawValue).(type) { + switch rawValue.(type) { case ast.AliasValueUser: - userValue := (*rawValue).(ast.AliasValueUser) + userValue := rawValue.(ast.AliasValueUser) changes := handlers.RenameAlias( *d.Indexes, diff --git a/server/handlers/aliases/lsp/text-document-signature-help.go b/server/handlers/aliases/lsp/text-document-signature-help.go index 913fe62..77ab565 100644 --- a/server/handlers/aliases/lsp/text-document-signature-help.go +++ b/server/handlers/aliases/lsp/text-document-signature-help.go @@ -14,7 +14,7 @@ func TextDocumentSignatureHelp(context *glsp.Context, params *protocol.Signature document := aliases.DocumentParserMap[params.TextDocument.URI] line := params.Position.Line - cursor := common.LSPCharacterAsCursorPosition(common.CursorToCharacterIndex(params.Position.Character)) + cursor := common.LSPCharacterAsCursorPosition(params.Position.Character) if _, found := document.Parser.CommentLines[line]; found { // Comment @@ -36,17 +36,15 @@ func TextDocumentSignatureHelp(context *glsp.Context, params *protocol.Signature if entry.Values != nil && entry.Values.Location.ContainsPosition(cursor) { value := handlers.GetValueAtPosition(cursor, entry) - if value == nil { + if value == nil || value.GetAliasValue().Value == "" { // For some reason, this does not really work, // When we return all, and then a user value is entered // and the `GetValueSignatureHelp` is called, still the old // signatures with all signature are shown - // return handlers.GetAllValuesSignatureHelp(), nil - - return nil, nil + return handlers.GetAllValuesSignatureHelp(), nil } - return handlers.GetValueSignatureHelp(cursor, *value), nil + return handlers.GetValueSignatureHelp(cursor, value), nil } return nil, nil diff --git a/server/handlers/fstab/Fstab.g4 b/server/handlers/fstab/Fstab.g4 index e5c6b1e..e01c2c4 100644 --- a/server/handlers/fstab/Fstab.g4 +++ b/server/handlers/fstab/Fstab.g4 @@ -20,9 +20,7 @@ mountPoint ; fileSystem - : ADFS | AFFS | BTRFS | EXFAT - // Still match unknown file systems - | STRING | QUOTED_STRING + : STRING | QUOTED_STRING ; mountOptions @@ -57,20 +55,3 @@ QUOTED_STRING : '"' WHITESPACE? (STRING WHITESPACE)* STRING? ('"')? ; -// ///// Supported file systems ///// - -ADFS - : ('A' | 'a') ('D' | 'd') ('F' | 'f') ('S' | 's') - ; - -AFFS - : ('A' | 'a') ('F' | 'f') ('F' | 'f') ('S' | 's') - ; - -BTRFS - : ('B' | 'b') ('T' | 't') ('R' | 'r') ('F' | 'f') ('S' | 's') - ; - -EXFAT - : ('E' | 'e') ('X' | 'x') ('F' | 'f') ('A' | 'a') ('T' | 't') - ; diff --git a/server/handlers/fstab/analyzer/analyzer.go b/server/handlers/fstab/analyzer/analyzer.go index 465ead5..77c9d98 100644 --- a/server/handlers/fstab/analyzer/analyzer.go +++ b/server/handlers/fstab/analyzer/analyzer.go @@ -14,17 +14,23 @@ type analyzerContext struct { func Analyze( document *shared.FstabDocument, ) []protocol.Diagnostic { - ctx := analyzerContext{ + ctx := &analyzerContext{ document: document, } - analyzeFieldAreFilled(&ctx) + analyzeFieldAreFilled(ctx) if len(ctx.diagnostics) > 0 { return ctx.diagnostics } - analyzeValuesAreValid(&ctx) + analyzeValuesAreValid(ctx) + + if len(ctx.diagnostics) > 0 { + return ctx.diagnostics + } + + analyzeFSCKField(ctx) return ctx.diagnostics } diff --git a/server/handlers/fstab/analyzer/fsck.go b/server/handlers/fstab/analyzer/fsck.go new file mode 100644 index 0000000..5150b23 --- /dev/null +++ b/server/handlers/fstab/analyzer/fsck.go @@ -0,0 +1,49 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/fstab/ast" + "config-lsp/handlers/fstab/fields" + "fmt" + "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeFSCKField(ctx *analyzerContext) { + it := ctx.document.Config.Entries.Iterator() + + var rootEntry *ast.FstabEntry + + for it.Next() { + entry := it.Value().(*ast.FstabEntry) + + if entry.Fields != nil && entry.Fields.Fsck != nil && entry.Fields.Fsck.Value.Value == "1" { + fileSystem := strings.ToLower(entry.Fields.FilesystemType.Value.Value) + + if _, found := fields.FsckOneDisabledFilesystems[fileSystem]; found { + // From https://wiki.archlinux.org/title/Fstab + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: entry.Fields.Fsck.ToLSPRange(), + Message: "If the root file system is btrfs or XFS, the fsck order should be set to 0 instead of 1. See fsck.btrfs(8) and fsck.xfs(8).", + Severity: &common.SeverityWarning, + }) + + continue + } + + if entry.Fields.Fsck.Value.Value == "1" { + if rootEntry != nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: entry.Fields.Fsck.ToLSPRange(), + Message: fmt.Sprintf("Only the root file system should have a fsck of 1. Other file systems should have a fsck of 2 or 0. The root file system is already using a fsck=1 on line %d", rootEntry.Fields.Start.Line), + Severity: &common.SeverityWarning, + }) + } else { + rootEntry = entry + } + } + } + } +} diff --git a/server/handlers/fstab/analyzer/fsck_test.go b/server/handlers/fstab/analyzer/fsck_test.go new file mode 100644 index 0000000..3e50ab0 --- /dev/null +++ b/server/handlers/fstab/analyzer/fsck_test.go @@ -0,0 +1,44 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/fstab/test_utils" + "testing" +) + +func TestFSCKMultipleRoots( + t *testing.T, +) { + document := testutils_test.DocumentFromInput(t, ` +UUID=12345678-1234-1234-1234-123456789012 /boot ext4 defaults 0 1 +UUID=12345678-1234-1234-1234-123456789012 / btrfs defaults 0 1 +UUID=12345678-1234-1234-1234-123456789012 /home ext4 defaults 0 2 +`) + + ctx := &analyzerContext{ + document: document, + } + + analyzeFSCKField(ctx) + + if len(ctx.diagnostics) != 1 { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} + +func TestFSCKBtrfsUsingRoot( + t *testing.T, +) { + document := testutils_test.DocumentFromInput(t, ` +UUID=12345678-1234-1234-1234-123456789012 /boot btrfs defaults 0 1 +`) + + ctx := &analyzerContext{ + document: document, + } + + analyzeFSCKField(ctx) + + if len(ctx.diagnostics) != 1 { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} diff --git a/server/handlers/fstab/analyzer/values.go b/server/handlers/fstab/analyzer/values.go index bd7d8ba..62d9fac 100644 --- a/server/handlers/fstab/analyzer/values.go +++ b/server/handlers/fstab/analyzer/values.go @@ -33,8 +33,8 @@ func analyzeValuesAreValid( checkField(ctx, entry.Fields.Freq, fields.FreqField) } - if entry.Fields.Pass != nil { - checkField(ctx, entry.Fields.Pass, fields.PassField) + if entry.Fields.Fsck != nil { + checkField(ctx, entry.Fields.Fsck, fields.FsckField) } } } diff --git a/server/handlers/fstab/ast/fstab.go b/server/handlers/fstab/ast/fstab.go index 8286ba0..8355956 100644 --- a/server/handlers/fstab/ast/fstab.go +++ b/server/handlers/fstab/ast/fstab.go @@ -14,7 +14,7 @@ const ( FstabFieldFileSystemType FstabFieldName = "filesystemtype" FstabFieldOptions FstabFieldName = "options" FstabFieldFreq FstabFieldName = "freq" - FstabFieldPass FstabFieldName = "pass" + FstabFieldFsck FstabFieldName = "fsck" ) type FstabField struct { @@ -29,7 +29,7 @@ type FstabFields struct { FilesystemType *FstabField Options *FstabField Freq *FstabField - Pass *FstabField + Fsck *FstabField } type FstabEntry struct { diff --git a/server/handlers/fstab/ast/fstab_fields.go b/server/handlers/fstab/ast/fstab_fields.go index 77679b5..19dff2a 100644 --- a/server/handlers/fstab/ast/fstab_fields.go +++ b/server/handlers/fstab/ast/fstab_fields.go @@ -20,7 +20,7 @@ import ( // LABEL=test ext4 defaults 0 0 func (e FstabEntry) GetFieldAtPosition(position common.Position) FstabFieldName { // No fields defined, empty line - if e.Fields.Spec == nil && e.Fields.MountPoint == nil && e.Fields.FilesystemType == nil && e.Fields.Options == nil && e.Fields.Freq == nil && e.Fields.Pass == nil { + if e.Fields.Spec == nil && e.Fields.MountPoint == nil && e.Fields.FilesystemType == nil && e.Fields.Options == nil && e.Fields.Freq == nil && e.Fields.Fsck == nil { return FstabFieldSpec } @@ -41,8 +41,8 @@ func (e FstabEntry) GetFieldAtPosition(position common.Position) FstabFieldName if e.Fields.Freq != nil && e.Fields.Freq.ContainsPosition(position) { return FstabFieldFreq } - if e.Fields.Pass != nil && e.Fields.Pass.ContainsPosition(position) { - return FstabFieldPass + if e.Fields.Fsck != nil && e.Fields.Fsck.ContainsPosition(position) { + return FstabFieldFsck } // Okay let's try to fetch the field by assuming the user is typing from left to right normally @@ -63,8 +63,8 @@ func (e FstabEntry) GetFieldAtPosition(position common.Position) FstabFieldName return FstabFieldFreq } - if e.Fields.Freq != nil && e.Fields.Freq.IsPositionAfterEnd(position) && (e.Fields.Pass == nil || e.Fields.Pass.IsPositionBeforeEnd(position)) { - return FstabFieldPass + if e.Fields.Freq != nil && e.Fields.Freq.IsPositionAfterEnd(position) && (e.Fields.Fsck == nil || e.Fields.Fsck.IsPositionBeforeEnd(position)) { + return FstabFieldFsck } // Okay shit no idea, let's just give whatever is missing @@ -89,7 +89,7 @@ func (e FstabEntry) GetFieldAtPosition(position common.Position) FstabFieldName return FstabFieldFreq } - return FstabFieldPass + return FstabFieldFsck } // LABEL=test /mnt/test btrfs subvol=backup,fat=32 [0] [0] @@ -122,7 +122,7 @@ func (e FstabEntry) getDefinedFieldsAmount() uint8 { if e.Fields.Freq != nil { definedAmount++ } - if e.Fields.Pass != nil { + if e.Fields.Fsck != nil { definedAmount++ } diff --git a/server/handlers/fstab/ast/listener.go b/server/handlers/fstab/ast/listener.go index a9f16b4..4cc4625 100644 --- a/server/handlers/fstab/ast/listener.go +++ b/server/handlers/fstab/ast/listener.go @@ -128,7 +128,7 @@ func (s *fstabParserListener) EnterPass(ctx *parser.PassContext) { text := ctx.GetText() value := commonparser.ParseRawString(text, commonparser.FullFeatures) - s.fstabContext.currentEntry.Fields.Pass = &FstabField{ + s.fstabContext.currentEntry.Fields.Fsck = &FstabField{ LocationRange: location, Value: value, } diff --git a/server/handlers/fstab/ast/parser.go b/server/handlers/fstab/ast/parser.go index f24a6fa..3d17a87 100644 --- a/server/handlers/fstab/ast/parser.go +++ b/server/handlers/fstab/ast/parser.go @@ -161,7 +161,7 @@ func (c *FstabConfig) parseStatement( // FilesystemType: filesystemType, // Options: options, // Freq: freq, -// Pass: pass, +// Fsck: pass, // }, // } // diff --git a/server/handlers/fstab/ast/parser_test.go b/server/handlers/fstab/ast/parser_test.go index ac37136..9d5abdb 100644 --- a/server/handlers/fstab/ast/parser_test.go +++ b/server/handlers/fstab/ast/parser_test.go @@ -27,7 +27,7 @@ LABEL=example /mnt/example fat32 defaults 0 2 rawFirstEntry, _ := c.Entries.Get(uint32(0)) firstEntry := rawFirstEntry.(*FstabEntry) - if !(firstEntry.Fields.Spec.Value.Value == "LABEL=test" && firstEntry.Fields.MountPoint.Value.Value == "/mnt/test" && firstEntry.Fields.FilesystemType.Value.Value == "ext4" && firstEntry.Fields.Options.Value.Value == "defaults" && firstEntry.Fields.Freq.Value.Value == "0" && firstEntry.Fields.Pass.Value.Value == "0") { + if !(firstEntry.Fields.Spec.Value.Value == "LABEL=test" && firstEntry.Fields.MountPoint.Value.Value == "/mnt/test" && firstEntry.Fields.FilesystemType.Value.Value == "ext4" && firstEntry.Fields.Options.Value.Value == "defaults" && firstEntry.Fields.Freq.Value.Value == "0" && firstEntry.Fields.Fsck.Value.Value == "0") { t.Fatalf("Expected entry to be LABEL=test /mnt/test ext4 defaults 0 0, got %v", firstEntry) } @@ -71,8 +71,8 @@ LABEL=example /mnt/example fat32 defaults 0 2 t.Errorf("Expected freq end to be 0:36, got %v", firstEntry.Fields.Freq.LocationRange.End) } - if !(firstEntry.Fields.Pass.LocationRange.Start.Line == 0 && firstEntry.Fields.Pass.LocationRange.Start.Character == 37) { - t.Errorf("Expected pass start to be 0:37, got %v", firstEntry.Fields.Pass.LocationRange.Start) + if !(firstEntry.Fields.Fsck.LocationRange.Start.Line == 0 && firstEntry.Fields.Fsck.LocationRange.Start.Character == 37) { + t.Errorf("Expected pass start to be 0:37, got %v", firstEntry.Fields.Fsck.LocationRange.Start) } field := firstEntry.GetFieldAtPosition(common.IndexPosition(0)) diff --git a/server/handlers/fstab/fields/fsck.go b/server/handlers/fstab/fields/fsck.go new file mode 100644 index 0000000..31cc7c6 --- /dev/null +++ b/server/handlers/fstab/fields/fsck.go @@ -0,0 +1,40 @@ +package fields + +import docvalues "config-lsp/doc-values" + +var FsckField = docvalues.EnumValue{ + EnforceValues: false, + Values: []docvalues.EnumString{ + docvalues.CreateEnumStringWithDoc( + "0", + "Defaults to zero (don’t check the filesystem) if not present.", + ), + docvalues.CreateEnumStringWithDoc( + "1", + "The root filesystem should be specified with a fs_passno of 1.", + ), + docvalues.CreateEnumStringWithDoc( + "2", + "Other filesystems [than the root filesystem] should have a fs_passno of 2.", + ), + }, +} + +var FsckFieldWhenDisabledFilesystems = docvalues.EnumValue{ + EnforceValues: false, + Values: []docvalues.EnumString{ + docvalues.CreateEnumStringWithDoc( + "0", + "Defaults to zero (don’t check the filesystem) if not present.", + ), + docvalues.CreateEnumStringWithDoc( + "2", + "Other filesystems [than the root filesystem] should have a fs_passno of 2.", + ), + }, +} + +var FsckOneDisabledFilesystems = map[string]struct{}{ + "btrfs": {}, + "xfs": {}, +} diff --git a/server/handlers/fstab/fields/mountoptions.go b/server/handlers/fstab/fields/mountoptions.go index 2ebf666..b2e9f26 100644 --- a/server/handlers/fstab/fields/mountoptions.go +++ b/server/handlers/fstab/fields/mountoptions.go @@ -470,6 +470,10 @@ var MountOptionsMapField = map[string]optionField{ Enums: commondocumentation.UmsdosDocumentationEnums, Assignable: commondocumentation.UmsdosDocumentationAssignable, }, + "vboxsf": { + Enums: commondocumentation.VboxsfDocumentationEnums, + Assignable: commondocumentation.VboxsfDocumentationAssignable, + }, "vfat": { Enums: commondocumentation.VfatDocumentationEnums, Assignable: commondocumentation.VfatDocumentationAssignable, diff --git a/server/handlers/fstab/fields/pass.go b/server/handlers/fstab/fields/pass.go deleted file mode 100644 index 5306a11..0000000 --- a/server/handlers/fstab/fields/pass.go +++ /dev/null @@ -1,26 +0,0 @@ -package fields - -import docvalues "config-lsp/doc-values" - -var PassField = docvalues.OrValue{ - Values: []docvalues.DeprecatedValue{ - docvalues.EnumValue{ - EnforceValues: false, - Values: []docvalues.EnumString{ - docvalues.CreateEnumStringWithDoc( - "0", - "Defaults to zero (don’t check the filesystem) if not present.", - ), - docvalues.CreateEnumStringWithDoc( - "1", - "The root filesystem should be specified with a fs_passno of 1.", - ), - docvalues.CreateEnumStringWithDoc( - "2", - "Other filesystems [than the root filesystem] should have a fs_passno of 2.", - ), - }, - }, - docvalues.NumberValue{}, - }, -} diff --git a/server/handlers/fstab/handlers/completions.go b/server/handlers/fstab/handlers/completions.go index c2d00f5..40c62bf 100644 --- a/server/handlers/fstab/handlers/completions.go +++ b/server/handlers/fstab/handlers/completions.go @@ -4,7 +4,9 @@ import ( "config-lsp/common" "config-lsp/handlers/fstab/ast" "config-lsp/handlers/fstab/fields" + "config-lsp/utils" "fmt" + "strings" "github.com/tliron/glsp/protocol_3_16" ) @@ -84,13 +86,21 @@ func GetCompletion( value, cursor, ), nil - case ast.FstabFieldPass: - value, cursor := getFieldSafely(entry.Fields.Pass, cursor) + case ast.FstabFieldFsck: + value, cursor := getFieldSafely(entry.Fields.Fsck, cursor) - return fields.PassField.DeprecatedFetchCompletions( - value, - cursor, - ), nil + if entry.Fields.FilesystemType != nil && + utils.KeyExists(fields.FsckOneDisabledFilesystems, strings.ToLower(entry.Fields.FilesystemType.Value.Value)) { + return fields.FsckFieldWhenDisabledFilesystems.DeprecatedFetchCompletions( + value, + cursor, + ), nil + } else { + return fields.FsckField.DeprecatedFetchCompletions( + value, + cursor, + ), nil + } } return nil, nil diff --git a/server/handlers/fstab/handlers/hover.go b/server/handlers/fstab/handlers/hover.go index 0216a62..f825ffe 100644 --- a/server/handlers/fstab/handlers/hover.go +++ b/server/handlers/fstab/handlers/hover.go @@ -42,7 +42,7 @@ func GetHoverInfo( return &hover, nil case ast.FstabFieldFreq: return &FreqHoverField, nil - case ast.FstabFieldPass: + case ast.FstabFieldFsck: return &PassHoverField, nil } diff --git a/server/handlers/ssh_config/analyzer/analyzer.go b/server/handlers/ssh_config/analyzer/analyzer.go index 48adb23..447bb34 100644 --- a/server/handlers/ssh_config/analyzer/analyzer.go +++ b/server/handlers/ssh_config/analyzer/analyzer.go @@ -35,7 +35,27 @@ func Analyze( d.Indexes = i + analyzeIncludeValues(ctx) + + if len(ctx.diagnostics) == 0 { + for _, include := range d.Indexes.Includes { + for _, value := range include.Values { + for _, path := range value.Paths { + _, err := parseFile(string(path)) + + if err != nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: value.LocationRange.ToLSPRange(), + Message: err.Error(), + }) + } + } + } + } + } + analyzeValuesAreValid(ctx) + analyzeTokens(ctx) analyzeIgnoreUnknownHasNoUnnecessary(ctx) analyzeDependents(ctx) analyzeBlocks(ctx) diff --git a/server/handlers/ssh_config/analyzer/include.go b/server/handlers/ssh_config/analyzer/include.go new file mode 100644 index 0000000..8cb5115 --- /dev/null +++ b/server/handlers/ssh_config/analyzer/include.go @@ -0,0 +1,143 @@ +package analyzer + +import ( + "config-lsp/common" + sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/ast" + "config-lsp/handlers/ssh_config/fields" + "config-lsp/handlers/ssh_config/indexes" + "config-lsp/utils" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +var whitespacePattern = regexp.MustCompile(`\S+`) +var environmtalVariablePattern = regexp.MustCompile(`\${.+?}`) + +func analyzeIncludeValues( + ctx *analyzerContext, +) { + for _, include := range ctx.document.Indexes.Includes { + for _, value := range include.Values { + if !canBeAnalyzed(value.Value) { + continue + } + + validPaths, err := createIncludePaths(value.Value) + + if err != nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: value.LocationRange.ToLSPRange(), + Message: err.Error(), + Severity: &common.SeverityError, + }) + } else { + value.Paths = validPaths + } + } + } +} + +// We can't evaluate environmental variables or tokens as we don't know the actual +// values +func canBeAnalyzed( + path string, +) bool { + if environmtalVariablePattern.MatchString(path) { + return false + } + + for token := range fields.AvailableTokens { + if strings.Contains(path, token) { + return false + } + } + + return true +} + +func createIncludePaths( + suggestedPath string, +) ([]indexes.ValidPath, error) { + var absolutePath string + + if path.IsAbs(suggestedPath) { + absolutePath = suggestedPath + } else if strings.HasPrefix(suggestedPath, "~") { + homeFolder, err := os.UserHomeDir() + + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not find home folder (error: %s)", err)) + } + + absolutePath = path.Join(homeFolder, suggestedPath[1:]) + } else { + homeFolder, err := os.UserHomeDir() + + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not find home folder (error: %s)", err)) + } + + absolutePath = path.Join(homeFolder, ".ssh", suggestedPath) + } + + files, err := filepath.Glob(absolutePath) + + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not find file %s (error: %s)", absolutePath, err)) + } + + if len(files) == 0 { + return nil, errors.New(fmt.Sprintf("Could not find file %s", absolutePath)) + } + + return utils.Map( + files, + func(file string) indexes.ValidPath { + return indexes.ValidPath(file) + }, + ), nil +} + +func parseFile( + filePath string, +) (*sshconfig.SSHDocument, error) { + if d, ok := sshconfig.DocumentParserMap[filePath]; ok { + return d, nil + } + + c := ast.NewSSHConfig() + + content, err := os.ReadFile(filePath) + + if err != nil { + return nil, err + } + + parseErrors := c.Parse(string(content)) + + if len(parseErrors) > 0 { + return nil, errors.New(fmt.Sprintf("Errors in %s", filePath)) + } + + d := &sshconfig.SSHDocument{ + Config: c, + } + + errs := Analyze(d) + + if len(errs) > 0 { + return nil, errors.New(fmt.Sprintf("Errors in %s", filePath)) + } + + sshconfig.DocumentParserMap[filePath] = d + + return d, nil +} diff --git a/server/handlers/ssh_config/analyzer/quotes.go b/server/handlers/ssh_config/analyzer/quotes.go index 945bd1a..2155fe9 100644 --- a/server/handlers/ssh_config/analyzer/quotes.go +++ b/server/handlers/ssh_config/analyzer/quotes.go @@ -30,7 +30,7 @@ func checkIsUsingDoubleQuotes( singleQuotePosition := strings.Index(value.Raw, "'") // Single quote - if singleQuotePosition != -1 && !quoteRanges.IsCharInside(singleQuotePosition) { + if singleQuotePosition != -1 && !quoteRanges.IsIndexInsideQuotes(singleQuotePosition) { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Range: valueRange.ToLSPRange(), Message: "ssh_config does not support single quotes. Use double quotes (\") instead.", diff --git a/server/handlers/ssh_config/analyzer/tokens.go b/server/handlers/ssh_config/analyzer/tokens.go new file mode 100644 index 0000000..6d35585 --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tokens.go @@ -0,0 +1,49 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/ssh_config/fields" + "config-lsp/utils" + "fmt" + "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeTokens( + ctx *analyzerContext, +) { + for _, info := range ctx.document.Config.GetAllOptions() { + if info.Option.Key == nil || info.Option.OptionValue == nil { + continue + } + + key := info.Option.Key.Key + text := info.Option.OptionValue.Value.Value + var tokens []string + + if foundTokens, found := fields.OptionsTokensMap[key]; found { + tokens = foundTokens + } else { + tokens = []string{} + } + + disallowedTokens := utils.Without(utils.KeysOfMap(fields.AvailableTokens), tokens) + + for _, token := range disallowedTokens { + if strings.Contains(text, token) { + optionName := string(key) + + if formatted, found := fields.FieldsNameFormattedMap[key]; found { + optionName = formatted + } + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: info.Option.OptionValue.ToLSPRange(), + Message: fmt.Sprintf("Token '%s' is not allowed for option '%s'", token, optionName), + Severity: &common.SeverityError, + }) + } + } + } +} diff --git a/server/handlers/ssh_config/analyzer/tokens_test.go b/server/handlers/ssh_config/analyzer/tokens_test.go new file mode 100644 index 0000000..cd2358e --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tokens_test.go @@ -0,0 +1,62 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/ssh_config/test_utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestInvalidTokensForNonExisting( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist Hello%%World +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTokens(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} + +func TestInvalidTokensForExistingOption( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Tunnel Hello%%World +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTokens(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} + +func TestValidTokens( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +LocalCommand Hello World %% and %d +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTokens(ctx) + + if len(ctx.diagnostics) > 0 { + t.Fatalf("Expected no errors, but got %v", len(ctx.diagnostics)) + } +} diff --git a/server/handlers/ssh_config/fields/fields.go b/server/handlers/ssh_config/fields/fields.go index 59caccf..179beb1 100644 --- a/server/handlers/ssh_config/fields/fields.go +++ b/server/handlers/ssh_config/fields/fields.go @@ -98,6 +98,7 @@ var Options = map[NormalizedOptionName]docvalues.DocumentationValue{ Value: docvalues.ArrayValue{ Separator: ",", DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor, + RespectQuotes: true, SubValue: docvalues.StringValue{}, }, }, @@ -127,6 +128,7 @@ rsa-sha2-512,rsa-sha2-256 SubValue: docvalues.ArrayValue{ Separator: ",", DuplicatesExtractor: &docvalues.DuplicatesAllowedExtractor, + RespectQuotes: true, // TODO: Add SubValue: docvalues.StringValue{}, }, @@ -170,6 +172,7 @@ The default is not to expire channels of any type for inactivity.`, Value: docvalues.ArrayValue{ Separator: " ", DuplicatesExtractor: &channelTimeoutExtractor, + RespectQuotes: true, SubValue: docvalues.KeyValueAssignmentValue{ ValueIsOptional: false, Separator: "=", @@ -361,6 +364,7 @@ aes128-gcm@openssh.com,aes256-gcm@openssh.com Value: docvalues.ArrayValue{ Separator: " ", DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor, + RespectQuotes: true, SubValue: docvalues.PathValue{ RequiredType: docvalues.PathTypeFile, }, @@ -479,6 +483,7 @@ rsa-sha2-512,rsa-sha2-256 Value: docvalues.ArrayValue{ Separator: " ", DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor, + RespectQuotes: true, SubValue: docvalues.StringValue{}, }, }, @@ -493,7 +498,9 @@ rsa-sha2-512,rsa-sha2-256 }, }, docvalues.ArrayValue{ - Separator: " ", + Separator: " ", + DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor, + RespectQuotes: true, SubValue: docvalues.EnumValue{ EnforceValues: true, Values: []docvalues.EnumString{ @@ -539,6 +546,7 @@ rsa-sha2-512,rsa-sha2-256 Value: docvalues.ArrayValue{ Separator: ",", DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor, + RespectQuotes: true, SubValue: docvalues.EnumValue{ EnforceValues: true, Values: []docvalues.EnumString{ @@ -951,7 +959,9 @@ rsa-sha2-512,rsa-sha2-256 ~/.ssh/known_hosts, ~/.ssh/known_hosts2.`, Value: docvalues.ArrayValue{ - Separator: " ", + Separator: " ", + DuplicatesExtractor: &docvalues.SimpleDuplicatesExtractor, + RespectQuotes: true, SubValue: docvalues.PathValue{ RequiredType: docvalues.PathTypeFile, }, diff --git a/server/handlers/ssh_config/fields/tokens.go b/server/handlers/ssh_config/fields/tokens.go new file mode 100644 index 0000000..42b0f54 --- /dev/null +++ b/server/handlers/ssh_config/fields/tokens.go @@ -0,0 +1,73 @@ +package fields + +import "config-lsp/utils" + +var AvailableTokens = map[string]string{ + "%%": "A literal ‘%’.", + "%C": "Hash of %l%h%p%r%j.", + "%d": "Local user's home directory.", + "%f": "The fingerprint of the server's host key.", + "%H": "The known_hosts hostname or address that is being searched for.", + "%h": "The remote hostname.", + "%I": "A string describing the reason for a KnownHostsCommand execution: either ADDRESS when looking up a host by address (only when CheckHostIP is enabled), HOSTNAME when searching by hostname, or ORDER when preparing the host key algorithm preference list to use for the destination host.", + "%i": "The local user ID.", + "%j": "The contents of the ProxyJump option, or the empty string if this option is unset.", + "%K": "The base64 encoded host key.", + "%k": "The host key alias if specified, otherwise the original remote hostname given on the command line.", + "%L": "The local hostname.", + "%l": "The local hostname, including the domain name.", + "%n": "The original remote hostname, as given on the command line.", + "%p": "The remote port.", + "%r": "The remote username.", + "%T": "The local tun(4) or tap(4) network interface assigned if tunnel forwarding was requested, or \"NONE\" otherwise.", + "%t": "The type of the server host key, e.g. ssh-ed25519.", + "%u": "The local username.", +} + +// A map of