mirror of
https://github.com/Myzel394/config-lsp.git
synced 2025-06-18 15:05:28 +02:00
Merge pull request #36 from Myzel394/server/add-suggested-words
Add suggestion words
This commit is contained in:
commit
e2c2fac98c
9
.github/workflows/pr-tests.yaml
vendored
9
.github/workflows/pr-tests.yaml
vendored
@ -22,3 +22,12 @@ jobs:
|
|||||||
- name: Build app
|
- name: Build app
|
||||||
run: nix develop --command bash -c "cd server && go build"
|
run: nix develop --command bash -c "cd server && go build"
|
||||||
|
|
||||||
|
- name: Build VS Code extension
|
||||||
|
run: nix build .#vs-code-extension
|
||||||
|
|
||||||
|
- name: Upload VS Code extension
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: vs-code-extension
|
||||||
|
path: result/config-lsp-*.vsix
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ builds:
|
|||||||
dir: ./server
|
dir: ./server
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- format: tar.gz
|
- formats: [ 'tar.gz' ]
|
||||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||||
name_template: >-
|
name_template: >-
|
||||||
{{ .ProjectName }}_
|
{{ .ProjectName }}_
|
||||||
@ -31,7 +31,7 @@ archives:
|
|||||||
# use zip for windows archives
|
# use zip for windows archives
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
formats: [ 'zip' ]
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
|
12
flake.lock
generated
12
flake.lock
generated
@ -26,11 +26,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1733668782,
|
"lastModified": 1741396135,
|
||||||
"narHash": "sha256-tPsqU00FhgdFr0JiQUiBMgPVbl1jbPCY5gbFiJycL3I=",
|
"narHash": "sha256-wqmdLr7h4Bk8gyKutgaApJKOM8JVvywI5P48NuqJ9Jg=",
|
||||||
"owner": "tweag",
|
"owner": "tweag",
|
||||||
"repo": "gomod2nix",
|
"repo": "gomod2nix",
|
||||||
"rev": "514283ec89c39ad0079ff2f3b1437404e4cba608",
|
"rev": "0983848bf2a7ccbfe24d874065adb8fd0f23729b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -41,11 +41,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739214665,
|
"lastModified": 1741513245,
|
||||||
"narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
|
"narHash": "sha256-7rTAMNTY1xoBwz0h7ZMtEcd8LELk9R5TzBPoHuhNSCk=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
|
"rev": "e3e32b642a31e6714ec1b712de8c91a3352ce7e1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
"aarch64-windows"
|
"aarch64-windows"
|
||||||
] (system:
|
] (system:
|
||||||
let
|
let
|
||||||
version = "0.1.4"; # CI:CD-VERSION
|
version = "0.2.0"; # CI:CD-VERSION
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
overlays = [
|
overlays = [
|
||||||
@ -42,7 +42,7 @@
|
|||||||
pname = "github.com/Myzel394/config-lsp";
|
pname = "github.com/Myzel394/config-lsp";
|
||||||
version = version;
|
version = version;
|
||||||
src = ./server;
|
src = ./server;
|
||||||
vendorHash = "sha256-eO1eY+2XuOCd/dKwgFtu05+bnn/Cv8ZbUIwRjCwJF+U=";
|
vendorHash = "sha256-ttr45N8i86mSJX9Scy/Cf+YlxU5wAKMVb0YhKg28JKM=";
|
||||||
ldflags = [ "-s" "-w" ];
|
ldflags = [ "-s" "-w" ];
|
||||||
checkPhase = ''
|
checkPhase = ''
|
||||||
go test -v $(pwd)/...
|
go test -v $(pwd)/...
|
||||||
@ -136,6 +136,7 @@
|
|||||||
mailutils
|
mailutils
|
||||||
wireguard-tools
|
wireguard-tools
|
||||||
antlr
|
antlr
|
||||||
|
just
|
||||||
]) ++ (if pkgs.stdenv.isLinux then with pkgs; [
|
]) ++ (if pkgs.stdenv.isLinux then with pkgs; [
|
||||||
postfix
|
postfix
|
||||||
] else []);
|
] else []);
|
||||||
|
30
justfile
Normal file
30
justfile
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env just --justfile
|
||||||
|
|
||||||
|
set dotenv-load := true
|
||||||
|
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Lint whole project
|
||||||
|
lint:
|
||||||
|
cd server && gofmt -s -w .
|
||||||
|
# cd vs-code-extension && yarn run lint
|
||||||
|
|
||||||
|
# Build config-lsp and test it in nvim (config-lsp will be loaded automatically)
|
||||||
|
[working-directory: "./server"]
|
||||||
|
test-nvim file:
|
||||||
|
go build -o ./result/bin/config-lsp && rm -rf ~/.local/state/nvim/lsp.log && DOTFILES_IGNORE_CONFIG_LSP=1 nvim {{file}} -c ':source nvim-lsp-debug.lua'
|
||||||
|
|
||||||
|
# Show Mason Logs
|
||||||
|
show-nvim-logs:
|
||||||
|
bat ~/.local/state/nvim/lsp.log
|
||||||
|
|
||||||
|
[working-directory: "./server"]
|
||||||
|
test:
|
||||||
|
nix develop --command bash -c 'go test ./... -count=1'
|
||||||
|
|
||||||
|
# Ready for a PR? Run this recipe before opening the PR!
|
||||||
|
ready:
|
||||||
|
just lint
|
||||||
|
just test
|
||||||
|
|
@ -6,7 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ClearDiagnostics(context *glsp.Context, uri protocol.DocumentUri) {
|
func ClearDiagnostics(context *glsp.Context, uri protocol.DocumentUri) {
|
||||||
go context.Notify(
|
// Diagnostics are sent synchronously, as sending them async
|
||||||
|
// could result in a race condition when we send diagnostics
|
||||||
|
// to the client.
|
||||||
|
context.Notify(
|
||||||
protocol.ServerTextDocumentPublishDiagnostics,
|
protocol.ServerTextDocumentPublishDiagnostics,
|
||||||
protocol.PublishDiagnosticsParams{
|
protocol.PublishDiagnosticsParams{
|
||||||
URI: uri,
|
URI: uri,
|
||||||
|
@ -13,13 +13,30 @@ type ServerOptionsType struct {
|
|||||||
// we show a native warning. The error message boxes just clutter
|
// we show a native warning. The error message boxes just clutter
|
||||||
// the interface.
|
// the interface.
|
||||||
NoUndetectableErrors bool
|
NoUndetectableErrors bool
|
||||||
|
|
||||||
|
// If true, the server will not detect typos and suggest
|
||||||
|
// the correct keywords.
|
||||||
|
// Since the server finds typos using the Damerau-Levenshtein distance,
|
||||||
|
// and this is done each time code actions are requested
|
||||||
|
// (which happens quite often), these suggestions can eat a lot of resources.
|
||||||
|
// You may want to enable this option if you are dealing with little
|
||||||
|
// resources or if you're low on battery.
|
||||||
|
NoTypoSuggestions bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var ServerOptions = new(ServerOptionsType)
|
var ServerOptions = new(ServerOptionsType)
|
||||||
|
|
||||||
func InitServerOptions() {
|
func InitServerOptions() {
|
||||||
|
ServerOptions.NoUndetectableErrors = false
|
||||||
|
ServerOptions.NoTypoSuggestions = false
|
||||||
|
|
||||||
if slices.Contains(os.Args, "--no-undetectable-errors") {
|
if slices.Contains(os.Args, "--no-undetectable-errors") {
|
||||||
Log.Info("config-lsp will not return errors for undetectable files")
|
Log.Info("config-lsp will not return errors for undetectable files")
|
||||||
ServerOptions.NoUndetectableErrors = true
|
ServerOptions.NoUndetectableErrors = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if slices.Contains(os.Args, "--no-typo-suggestions") {
|
||||||
|
Log.Info("config-lsp will not detect typos for keywords")
|
||||||
|
ServerOptions.NoTypoSuggestions = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,11 +48,15 @@ func (v PathValue) GetTypeDescription() []string {
|
|||||||
|
|
||||||
func (v PathValue) DeprecatedCheckIsValid(value string) []*InvalidValue {
|
func (v PathValue) DeprecatedCheckIsValid(value string) []*InvalidValue {
|
||||||
if !utils.DoesPathExist(value) {
|
if !utils.DoesPathExist(value) {
|
||||||
return []*InvalidValue{{
|
if v.RequiredType == PathTypeExistenceOptional {
|
||||||
Err: PathDoesNotExistError{},
|
return nil
|
||||||
Start: 0,
|
} else {
|
||||||
End: uint32(len(value)),
|
return []*InvalidValue{{
|
||||||
}}
|
Err: PathDoesNotExistError{},
|
||||||
|
Start: 0,
|
||||||
|
End: uint32(len(value)),
|
||||||
|
}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid := false
|
isValid := false
|
||||||
|
27
server/fetch_tags.js
Normal file
27
server/fetch_tags.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Creates a JSON object in the form of:
|
||||||
|
// {
|
||||||
|
// [<Option name>]: documentation
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Searches for <dl> elements with <dt> and <dd> children based
|
||||||
|
// on the currently selected element in the Elements tab of the
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const content = {}
|
||||||
|
let currentOption = ""
|
||||||
|
|
||||||
|
const $elements = $0.querySelectorAll(":scope > dt,dd")
|
||||||
|
|
||||||
|
for (const $element of $elements) {
|
||||||
|
switch ($element.tagName) {
|
||||||
|
case "DT":
|
||||||
|
currentOption = $element.textContent.trim()
|
||||||
|
break
|
||||||
|
case "DD":
|
||||||
|
content[currentOption] = $element.textContent.trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(content)
|
||||||
|
})()
|
@ -15,6 +15,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/hbollon/go-edlib v1.6.0 // indirect
|
||||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
@ -9,6 +9,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E=
|
||||||
|
github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM=
|
||||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
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 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||||
|
@ -68,7 +68,7 @@ func (s *aliasesParserListener) EnterValues(ctx *parser.ValuesContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Value === //
|
// === Name === //
|
||||||
|
|
||||||
func (s *aliasesParserListener) EnterUser(ctx *parser.UserContext) {
|
func (s *aliasesParserListener) EnterUser(ctx *parser.UserContext) {
|
||||||
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
|
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
|
||||||
|
@ -51,7 +51,7 @@ func TextDocumentHover(
|
|||||||
contents := []string{}
|
contents := []string{}
|
||||||
contents = append(contents, handlers.GetAliasValueTypeInfo(value)...)
|
contents = append(contents, handlers.GetAliasValueTypeInfo(value)...)
|
||||||
contents = append(contents, "")
|
contents = append(contents, "")
|
||||||
contents = append(contents, "#### Value")
|
contents = append(contents, "#### Name")
|
||||||
contents = append(contents, handlers.GetAliasValueHoverInfo(*document.Indexes, value))
|
contents = append(contents, handlers.GetAliasValueHoverInfo(*document.Indexes, value))
|
||||||
|
|
||||||
text := strings.Join(contents, "\n")
|
text := strings.Join(contents, "\n")
|
||||||
|
38
server/handlers/fstab/analyzer/spec.go
Normal file
38
server/handlers/fstab/analyzer/spec.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package analyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"config-lsp/handlers/fstab/ast"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
var volatileBlockFields = regexp.MustCompile(`^/dev/(sd|nvme|mmcblk|sr|vd|loop|cdrom)[a-zA-Z0-9]*$`)
|
||||||
|
|
||||||
|
func analyzeSpecField(
|
||||||
|
ctx *analyzerContext,
|
||||||
|
field *ast.FstabField,
|
||||||
|
) {
|
||||||
|
if field == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Value.Value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !volatileBlockFields.MatchString(field.Value.Value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
codeDescription := protocol.CodeDescription{
|
||||||
|
HRef: protocol.URI("https://wiki.archlinux.org/title/Persistent_block_device_naming"),
|
||||||
|
}
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Range: field.ToLSPRange(),
|
||||||
|
Message: "Kernel name descriptors for block devices are not persistent and can change each boot, they should not be used in configuration files. Prefer device UUIDs or LABELs instead.",
|
||||||
|
CodeDescription: &codeDescription,
|
||||||
|
Severity: &common.SeverityWarning,
|
||||||
|
})
|
||||||
|
}
|
@ -21,6 +21,8 @@ func analyzeValuesAreValid(
|
|||||||
checkField(ctx, entry.Fields.MountPoint, fields.MountPointField)
|
checkField(ctx, entry.Fields.MountPoint, fields.MountPointField)
|
||||||
checkField(ctx, entry.Fields.FilesystemType, fields.FileSystemTypeField)
|
checkField(ctx, entry.Fields.FilesystemType, fields.FileSystemTypeField)
|
||||||
|
|
||||||
|
analyzeSpecField(ctx, entry.Fields.Spec)
|
||||||
|
|
||||||
if entry.Fields.Options != nil {
|
if entry.Fields.Options != nil {
|
||||||
mountOptions := entry.FetchMountOptionsField(true)
|
mountOptions := entry.FetchMountOptionsField(true)
|
||||||
|
|
||||||
|
@ -6,7 +6,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var UuidField = docvalues.RegexValue{
|
var UuidField = docvalues.RegexValue{
|
||||||
Regex: *regexp.MustCompile(`[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`),
|
// Can either be a UUID or UID
|
||||||
|
Regex: *regexp.MustCompile(`(?i)([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|[a-f0-9]{4}-[a-f0-9]{4})`),
|
||||||
}
|
}
|
||||||
var LabelField = docvalues.RegexValue{
|
var LabelField = docvalues.RegexValue{
|
||||||
Regex: *regexp.MustCompile(`\S+`),
|
Regex: *regexp.MustCompile(`\S+`),
|
||||||
|
@ -38,27 +38,43 @@ func checkOption(
|
|||||||
option *ast.SSHOption,
|
option *ast.SSHOption,
|
||||||
block ast.SSHBlock,
|
block ast.SSHBlock,
|
||||||
) {
|
) {
|
||||||
|
if option.Key == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
///// General checks
|
||||||
checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange)
|
checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange)
|
||||||
checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange)
|
checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange)
|
||||||
|
|
||||||
docOption, found := fields.Options[option.Key.Key]
|
if option.Separator == nil || option.Separator.Value.Value == "" {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Range: option.Key.LocationRange.ToLSPRange(),
|
||||||
|
Message: "There should be a separator between an option and its value",
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange)
|
||||||
|
checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange)
|
||||||
|
}
|
||||||
|
|
||||||
if !found {
|
///// Check if the key is valid
|
||||||
|
docOption, optionFound := fields.Options[option.Key.Key]
|
||||||
|
|
||||||
|
if !optionFound {
|
||||||
// Diagnostics will be handled by `values.go`
|
// Diagnostics will be handled by `values.go`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for values that are not allowed in Host blocks
|
// Check for values that are not allowed in Host blocks
|
||||||
if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost {
|
if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost && utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) {
|
||||||
if utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) {
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
Range: option.Key.LocationRange.ToLSPRange(),
|
||||||
Range: option.Key.LocationRange.ToLSPRange(),
|
Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key),
|
||||||
Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key),
|
Severity: &common.SeverityError,
|
||||||
Severity: &common.SeverityError,
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///// Check if the value is valid
|
||||||
if option.OptionValue != nil {
|
if option.OptionValue != nil {
|
||||||
checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
||||||
checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
||||||
@ -75,17 +91,6 @@ func checkOption(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if option.Separator == nil || option.Separator.Value.Value == "" {
|
|
||||||
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
|
||||||
Range: option.Key.LocationRange.ToLSPRange(),
|
|
||||||
Message: fmt.Sprintf("There should be a separator between an option and its value"),
|
|
||||||
Severity: &common.SeverityError,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange)
|
|
||||||
checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBlock(
|
func checkBlock(
|
||||||
|
@ -9,18 +9,6 @@ import (
|
|||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
func analyzeQuotesAreValid(
|
|
||||||
ctx *analyzerContext,
|
|
||||||
) {
|
|
||||||
for _, info := range ctx.document.Config.GetAllOptions() {
|
|
||||||
checkIsUsingDoubleQuotes(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
|
|
||||||
checkIsUsingDoubleQuotes(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)
|
|
||||||
|
|
||||||
checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
|
|
||||||
checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkIsUsingDoubleQuotes(
|
func checkIsUsingDoubleQuotes(
|
||||||
ctx *analyzerContext,
|
ctx *analyzerContext,
|
||||||
value commonparser.ParsedString,
|
value commonparser.ParsedString,
|
||||||
|
@ -7,6 +7,18 @@ import (
|
|||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testQuotes(
|
||||||
|
ctx *analyzerContext,
|
||||||
|
) {
|
||||||
|
for _, info := range ctx.document.Config.GetAllOptions() {
|
||||||
|
checkIsUsingDoubleQuotes(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
|
||||||
|
checkIsUsingDoubleQuotes(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)
|
||||||
|
|
||||||
|
checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
|
||||||
|
checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSimpleInvalidQuotesExample(
|
func TestSimpleInvalidQuotesExample(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
@ -17,7 +29,7 @@ PermitRootLogin 'yes'
|
|||||||
document: d,
|
document: d,
|
||||||
diagnostics: make([]protocol.Diagnostic, 0),
|
diagnostics: make([]protocol.Diagnostic, 0),
|
||||||
}
|
}
|
||||||
analyzeQuotesAreValid(ctx)
|
testQuotes(ctx)
|
||||||
|
|
||||||
if !(len(ctx.diagnostics) == 1) {
|
if !(len(ctx.diagnostics) == 1) {
|
||||||
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
|
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
|
||||||
@ -34,7 +46,7 @@ func TestSingleQuotesKeyAndOptionExample(
|
|||||||
document: d,
|
document: d,
|
||||||
diagnostics: make([]protocol.Diagnostic, 0),
|
diagnostics: make([]protocol.Diagnostic, 0),
|
||||||
}
|
}
|
||||||
analyzeQuotesAreValid(ctx)
|
testQuotes(ctx)
|
||||||
|
|
||||||
if !(len(ctx.diagnostics) == 2) {
|
if !(len(ctx.diagnostics) == 2) {
|
||||||
t.Errorf("Expected 2 ctx.diagnostics, got %v", len(ctx.diagnostics))
|
t.Errorf("Expected 2 ctx.diagnostics, got %v", len(ctx.diagnostics))
|
||||||
@ -51,7 +63,7 @@ PermitRootLogin "yes
|
|||||||
document: d,
|
document: d,
|
||||||
diagnostics: make([]protocol.Diagnostic, 0),
|
diagnostics: make([]protocol.Diagnostic, 0),
|
||||||
}
|
}
|
||||||
analyzeQuotesAreValid(ctx)
|
testQuotes(ctx)
|
||||||
|
|
||||||
if !(len(ctx.diagnostics) == 1) {
|
if !(len(ctx.diagnostics) == 1) {
|
||||||
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
|
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
|
||||||
@ -68,7 +80,7 @@ func TestIncompleteQuotesExample(
|
|||||||
document: d,
|
document: d,
|
||||||
diagnostics: make([]protocol.Diagnostic, 0),
|
diagnostics: make([]protocol.Diagnostic, 0),
|
||||||
}
|
}
|
||||||
analyzeQuotesAreValid(ctx)
|
testQuotes(ctx)
|
||||||
|
|
||||||
if !(len(ctx.diagnostics) == 1) {
|
if !(len(ctx.diagnostics) == 1) {
|
||||||
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
|
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
|
||||||
|
@ -2,8 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
sshconfig "config-lsp/handlers/ssh_config"
|
sshconfig "config-lsp/handlers/ssh_config"
|
||||||
"config-lsp/handlers/ssh_config/diagnostics"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
@ -12,49 +10,12 @@ func FetchCodeActions(
|
|||||||
d *sshconfig.SSHDocument,
|
d *sshconfig.SSHDocument,
|
||||||
params *protocol.CodeActionParams,
|
params *protocol.CodeActionParams,
|
||||||
) []protocol.CodeAction {
|
) []protocol.CodeAction {
|
||||||
line := params.Range.Start.Line
|
|
||||||
|
|
||||||
if d.Indexes == nil {
|
if d.Indexes == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if unknownOption, found := d.Indexes.UnknownOptions[line]; found {
|
actions := getAddToUnknownCodeAction(d, params)
|
||||||
var blockLine *uint32
|
actions = append(actions, getKeywordTypoFixes(d, params)...)
|
||||||
|
|
||||||
if unknownOption.Block != nil {
|
return actions
|
||||||
blockLineValue := uint32(unknownOption.Block.GetLocation().Start.Line)
|
|
||||||
blockLine = &blockLineValue
|
|
||||||
}
|
|
||||||
|
|
||||||
commandID := "sshconfig." + CodeActionAddToUnknown
|
|
||||||
command := protocol.Command{
|
|
||||||
Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key),
|
|
||||||
Command: string(commandID),
|
|
||||||
Arguments: []any{
|
|
||||||
codeActionAddToUnknownArgs{
|
|
||||||
URI: params.TextDocument.URI,
|
|
||||||
OptionLine: unknownOption.Option.Start.Line,
|
|
||||||
BlockLine: blockLine,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
kind := protocol.CodeActionKindQuickFix
|
|
||||||
codeAction := &protocol.CodeAction{
|
|
||||||
Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key),
|
|
||||||
Command: &command,
|
|
||||||
Kind: &kind,
|
|
||||||
Diagnostics: []protocol.Diagnostic{
|
|
||||||
diagnostics.GenerateUnknownOption(
|
|
||||||
unknownOption.Option.Key.ToLSPRange(),
|
|
||||||
unknownOption.Option.Key.Value.Value,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return []protocol.CodeAction{
|
|
||||||
*codeAction,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
sshconfig "config-lsp/handlers/ssh_config"
|
||||||
|
"config-lsp/handlers/ssh_config/diagnostics"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getAddToUnknownCodeAction(
|
||||||
|
d *sshconfig.SSHDocument,
|
||||||
|
params *protocol.CodeActionParams,
|
||||||
|
) []protocol.CodeAction {
|
||||||
|
line := params.Range.Start.Line
|
||||||
|
|
||||||
|
if unknownOption, found := d.Indexes.UnknownOptions[line]; found {
|
||||||
|
var blockLine *uint32
|
||||||
|
|
||||||
|
if unknownOption.Block != nil {
|
||||||
|
blockLineValue := uint32(unknownOption.Block.GetLocation().Start.Line)
|
||||||
|
blockLine = &blockLineValue
|
||||||
|
}
|
||||||
|
|
||||||
|
commandID := "sshconfig." + CodeActionAddToUnknown
|
||||||
|
command := protocol.Command{
|
||||||
|
Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key),
|
||||||
|
Command: string(commandID),
|
||||||
|
Arguments: []any{
|
||||||
|
codeActionAddToUnknownArgs{
|
||||||
|
URI: params.TextDocument.URI,
|
||||||
|
OptionLine: unknownOption.Option.Start.Line,
|
||||||
|
BlockLine: blockLine,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
kind := protocol.CodeActionKindQuickFix
|
||||||
|
codeAction := protocol.CodeAction{
|
||||||
|
Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key),
|
||||||
|
Command: &command,
|
||||||
|
Kind: &kind,
|
||||||
|
Diagnostics: []protocol.Diagnostic{
|
||||||
|
diagnostics.GenerateUnknownOption(
|
||||||
|
unknownOption.Option.Key.ToLSPRange(),
|
||||||
|
unknownOption.Option.Key.Value.Value,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return []protocol.CodeAction{
|
||||||
|
codeAction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
103
server/handlers/ssh_config/handlers/fetch-code-actions_typos.go
Normal file
103
server/handlers/ssh_config/handlers/fetch-code-actions_typos.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
sshconfig "config-lsp/handlers/ssh_config"
|
||||||
|
"config-lsp/handlers/ssh_config/diagnostics"
|
||||||
|
"config-lsp/handlers/ssh_config/fields"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hbollon/go-edlib"
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getKeywordTypoFixes(
|
||||||
|
d *sshconfig.SSHDocument,
|
||||||
|
params *protocol.CodeActionParams,
|
||||||
|
) []protocol.CodeAction {
|
||||||
|
if common.ServerOptions.NoTypoSuggestions {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
line := params.Range.Start.Line
|
||||||
|
|
||||||
|
if typoOption, found := d.Indexes.UnknownOptions[line]; found {
|
||||||
|
name := typoOption.Option.Key.Value.Value
|
||||||
|
|
||||||
|
suggestedOptions := findSimilarOptions(name)
|
||||||
|
|
||||||
|
actions := make([]protocol.CodeAction, 0, len(suggestedOptions))
|
||||||
|
|
||||||
|
kind := protocol.CodeActionKindQuickFix
|
||||||
|
for index, normalizedOptionName := range suggestedOptions {
|
||||||
|
isPreferred := index == 0
|
||||||
|
optionName := fields.FieldsNameFormattedMap[normalizedOptionName]
|
||||||
|
|
||||||
|
actions = append(actions, protocol.CodeAction{
|
||||||
|
Title: fmt.Sprintf("Typo Fix: %s", optionName),
|
||||||
|
IsPreferred: &isPreferred,
|
||||||
|
Kind: &kind,
|
||||||
|
Diagnostics: []protocol.Diagnostic{
|
||||||
|
diagnostics.GenerateUnknownOption(
|
||||||
|
typoOption.Option.Key.ToLSPRange(),
|
||||||
|
typoOption.Option.Key.Value.Value,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Edit: &protocol.WorkspaceEdit{
|
||||||
|
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
|
||||||
|
params.TextDocument.URI: {
|
||||||
|
{
|
||||||
|
Range: typoOption.Option.Key.ToLSPRange(),
|
||||||
|
NewText: optionName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find options that are similar to the given option name.
|
||||||
|
// This is used to find typos & suggest the correct option name.
|
||||||
|
// Once an option is found that has a Damerau-Levenshtein distance of 1, it is immediately returned.
|
||||||
|
// If not, then the next 2 options of similarity 2, or 3 options of similarity 3 are returned.
|
||||||
|
// If no options with similarity <= 3 are found, then an empty slice is returned.
|
||||||
|
func findSimilarOptions(
|
||||||
|
optionName string,
|
||||||
|
) []fields.NormalizedOptionName {
|
||||||
|
normalizedOptionName := string(fields.CreateNormalizedName(optionName))
|
||||||
|
|
||||||
|
optionsPerSimilarity := map[uint8][]fields.NormalizedOptionName{
|
||||||
|
2: make([]fields.NormalizedOptionName, 0, 2),
|
||||||
|
3: make([]fields.NormalizedOptionName, 0, 3),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range fields.Options {
|
||||||
|
normalizedName := string(name)
|
||||||
|
similarity := edlib.DamerauLevenshteinDistance(normalizedName, normalizedOptionName)
|
||||||
|
|
||||||
|
switch similarity {
|
||||||
|
case 1:
|
||||||
|
return []fields.NormalizedOptionName{name}
|
||||||
|
case 2:
|
||||||
|
optionsPerSimilarity[2] = append(optionsPerSimilarity[2], name)
|
||||||
|
|
||||||
|
if len(optionsPerSimilarity[2]) >= 2 {
|
||||||
|
return optionsPerSimilarity[2]
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
optionsPerSimilarity[3] = append(optionsPerSimilarity[3], name)
|
||||||
|
|
||||||
|
if len(optionsPerSimilarity[3]) >= 3 {
|
||||||
|
return optionsPerSimilarity[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(optionsPerSimilarity[2], optionsPerSimilarity[3]...)
|
||||||
|
}
|
@ -33,7 +33,7 @@ func FormatDocument(
|
|||||||
// it := d.Config.Options.Iterator()
|
// it := d.Config.Options.Iterator()
|
||||||
// for it.Next() {
|
// for it.Next() {
|
||||||
// line := it.Key().(uint32)
|
// line := it.Key().(uint32)
|
||||||
// entry := it.Value().(ast.SSHEntry)
|
// entry := it.Name().(ast.SSHEntry)
|
||||||
//
|
//
|
||||||
// if !(line >= textRange.Start.Line && line <= textRange.End.Line) {
|
// if !(line >= textRange.Start.Line && line <= textRange.End.Line) {
|
||||||
// continue
|
// continue
|
||||||
|
@ -20,6 +20,7 @@ func TextDocumentDidChange(
|
|||||||
document := sshconfig.DocumentParserMap[params.TextDocument.URI]
|
document := sshconfig.DocumentParserMap[params.TextDocument.URI]
|
||||||
document.Config.Clear()
|
document.Config.Clear()
|
||||||
|
|
||||||
|
println("reparsing everything")
|
||||||
diagnostics := make([]protocol.Diagnostic, 0)
|
diagnostics := make([]protocol.Diagnostic, 0)
|
||||||
errors := document.Config.Parse(content)
|
errors := document.Config.Parse(content)
|
||||||
|
|
||||||
|
@ -4,7 +4,9 @@ import (
|
|||||||
"config-lsp/common"
|
"config-lsp/common"
|
||||||
docvalues "config-lsp/doc-values"
|
docvalues "config-lsp/doc-values"
|
||||||
"config-lsp/handlers/sshd_config/ast"
|
"config-lsp/handlers/sshd_config/ast"
|
||||||
|
"config-lsp/handlers/sshd_config/diagnostics"
|
||||||
"config-lsp/handlers/sshd_config/fields"
|
"config-lsp/handlers/sshd_config/fields"
|
||||||
|
"config-lsp/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
@ -20,7 +22,7 @@ func analyzeStructureIsValid(
|
|||||||
|
|
||||||
switch entry.(type) {
|
switch entry.(type) {
|
||||||
case *ast.SSHDOption:
|
case *ast.SSHDOption:
|
||||||
checkOption(ctx, entry.(*ast.SSHDOption), false)
|
checkOption(ctx, entry.(*ast.SSHDOption), nil)
|
||||||
case *ast.SSHDMatchBlock:
|
case *ast.SSHDMatchBlock:
|
||||||
matchBlock := entry.(*ast.SSHDMatchBlock)
|
matchBlock := entry.(*ast.SSHDMatchBlock)
|
||||||
checkMatchBlock(ctx, matchBlock)
|
checkMatchBlock(ctx, matchBlock)
|
||||||
@ -31,36 +33,52 @@ func analyzeStructureIsValid(
|
|||||||
func checkOption(
|
func checkOption(
|
||||||
ctx *analyzerContext,
|
ctx *analyzerContext,
|
||||||
option *ast.SSHDOption,
|
option *ast.SSHDOption,
|
||||||
isInMatchBlock bool,
|
matchBlock *ast.SSHDMatchBlock,
|
||||||
) {
|
) {
|
||||||
if option.Key == nil {
|
if option.Key == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///// General checks
|
||||||
checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange)
|
checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange)
|
||||||
checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange)
|
checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange)
|
||||||
|
|
||||||
key := option.Key.Key
|
if option.Separator == nil || option.Separator.Value.Value == "" {
|
||||||
docOption, found := fields.Options[key]
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
Range: option.Key.ToLSPRange(),
|
Range: option.Key.LocationRange.ToLSPRange(),
|
||||||
Message: fmt.Sprintf("Unknown option: %s", option.Key.Key),
|
Message: "There should be a separator between an option and its value",
|
||||||
Severity: &common.SeverityError,
|
Severity: &common.SeverityError,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange)
|
||||||
|
checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
///// Check if the key is valid
|
||||||
|
docOption, optionFound := fields.Options[option.Key.Key]
|
||||||
|
|
||||||
|
if !optionFound {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, diagnostics.GenerateUnknownOption(
|
||||||
|
option.Key.ToLSPRange(),
|
||||||
|
option.Key.Value.Value,
|
||||||
|
))
|
||||||
|
ctx.document.Indexes.UnknownOptions[option.Start.Line] = ast.SSHDOptionInfo{
|
||||||
|
Option: option,
|
||||||
|
MatchBlock: matchBlock,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we don't know the option, we can't verify the value
|
||||||
return
|
return
|
||||||
}
|
// Check for values that are not allowed in Match blocks
|
||||||
|
} else if matchBlock != nil && !utils.KeyExists(fields.MatchAllowedOptions, option.Key.Key) {
|
||||||
if _, found := fields.MatchAllowedOptions[key]; !found && isInMatchBlock {
|
|
||||||
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
Range: option.Key.ToLSPRange(),
|
Range: option.Key.ToLSPRange(),
|
||||||
Message: fmt.Sprintf("Option '%s' is not allowed inside Match blocks", option.Key.Key),
|
Message: fmt.Sprintf("Option '%s' is not allowed in Match blocks", option.Key.Key),
|
||||||
Severity: &common.SeverityError,
|
Severity: &common.SeverityError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///// Check if the value is valid
|
||||||
if option.OptionValue != nil {
|
if option.OptionValue != nil {
|
||||||
checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
||||||
checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
||||||
@ -78,16 +96,6 @@ func checkOption(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if option.Separator == nil || option.Separator.Value.Value == "" {
|
|
||||||
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
|
||||||
Range: option.Key.LocationRange.ToLSPRange(),
|
|
||||||
Message: fmt.Sprintf("There should be a separator between an option and its value"),
|
|
||||||
Severity: &common.SeverityError,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange)
|
|
||||||
checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMatchBlock(
|
func checkMatchBlock(
|
||||||
@ -99,6 +107,6 @@ func checkMatchBlock(
|
|||||||
for it.Next() {
|
for it.Next() {
|
||||||
option := it.Value().(*ast.SSHDOption)
|
option := it.Value().(*ast.SSHDOption)
|
||||||
|
|
||||||
checkOption(ctx, option, true)
|
checkOption(ctx, option, matchBlock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
server/handlers/sshd_config/analyzer/options_test.go
Normal file
34
server/handlers/sshd_config/analyzer/options_test.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package analyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
testutils_test "config-lsp/handlers/sshd_config/test_utils"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnknownOptionExample(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
d := testutils_test.DocumentFromInput(t, `
|
||||||
|
ThisOptionDoesNotExist okay
|
||||||
|
`)
|
||||||
|
ctx := &analyzerContext{
|
||||||
|
document: d,
|
||||||
|
diagnostics: make([]protocol.Diagnostic, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeStructureIsValid(ctx)
|
||||||
|
|
||||||
|
if !(len(ctx.diagnostics) == 1) {
|
||||||
|
t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(len(ctx.document.Indexes.UnknownOptions) == 1) {
|
||||||
|
t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") {
|
||||||
|
t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value)
|
||||||
|
}
|
||||||
|
}
|
@ -11,12 +11,12 @@ import (
|
|||||||
func analyzeQuotesAreValid(
|
func analyzeQuotesAreValid(
|
||||||
ctx *analyzerContext,
|
ctx *analyzerContext,
|
||||||
) {
|
) {
|
||||||
for _, option := range ctx.document.Config.GetAllOptions() {
|
for _, info := range ctx.document.Config.GetAllOptions() {
|
||||||
checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange)
|
checkIsUsingDoubleQuotes(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
|
||||||
checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
checkIsUsingDoubleQuotes(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)
|
||||||
|
|
||||||
checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange)
|
checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange)
|
||||||
checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange)
|
checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,13 +13,13 @@ import (
|
|||||||
func analyzeTokens(
|
func analyzeTokens(
|
||||||
ctx *analyzerContext,
|
ctx *analyzerContext,
|
||||||
) {
|
) {
|
||||||
for _, option := range ctx.document.Config.GetAllOptions() {
|
for _, info := range ctx.document.Config.GetAllOptions() {
|
||||||
if option.Key == nil || option.OptionValue == nil {
|
if info.Option.Key == nil || info.Option.OptionValue == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := option.Key.Key
|
key := info.Option.Key.Key
|
||||||
text := option.OptionValue.Value.Value
|
text := info.Option.OptionValue.Value.Value
|
||||||
var tokens []string
|
var tokens []string
|
||||||
|
|
||||||
if foundTokens, found := fields.OptionsTokensMap[key]; found {
|
if foundTokens, found := fields.OptionsTokensMap[key]; found {
|
||||||
@ -39,7 +39,7 @@ func analyzeTokens(
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
Range: option.OptionValue.ToLSPRange(),
|
Range: info.Option.OptionValue.ToLSPRange(),
|
||||||
Message: fmt.Sprintf("Token '%s' is not allowed for option '%s'", token, optionName),
|
Message: fmt.Sprintf("Token '%s' is not allowed for option '%s'", token, optionName),
|
||||||
Severity: &common.SeverityError,
|
Severity: &common.SeverityError,
|
||||||
})
|
})
|
||||||
|
8
server/handlers/sshd_config/ast/sshd_config_ast_utils.go
Normal file
8
server/handlers/sshd_config/ast/sshd_config_ast_utils.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Contains structs that are used as utilities, but are
|
||||||
|
// not used for the AST itself
|
||||||
|
package ast
|
||||||
|
|
||||||
|
type SSHDOptionInfo struct {
|
||||||
|
MatchBlock *SSHDMatchBlock
|
||||||
|
Option *SSHDOption
|
||||||
|
}
|
@ -64,26 +64,32 @@ func (c SSHDConfig) FindOption(line uint32) (*SSHDOption, *SSHDMatchBlock) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c SSHDConfig) GetAllOptions() []*SSHDOption {
|
func (c SSHDConfig) GetAllOptions() []SSHDOptionInfo {
|
||||||
options := make(
|
infos := make(
|
||||||
[]*SSHDOption,
|
[]SSHDOptionInfo,
|
||||||
0,
|
0,
|
||||||
// Approximation, this does not need to be exact
|
// Approximation, this does not need to be exact
|
||||||
c.Options.Size()+10,
|
c.Options.Size()+10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var currentMatchBlock *SSHDMatchBlock = nil
|
||||||
|
|
||||||
for _, rawEntry := range c.Options.Values() {
|
for _, rawEntry := range c.Options.Values() {
|
||||||
switch entry := rawEntry.(type) {
|
switch entry := rawEntry.(type) {
|
||||||
case *SSHDOption:
|
case *SSHDOption:
|
||||||
options = append(options, entry)
|
infos = append(infos, SSHDOptionInfo{
|
||||||
|
Option: entry,
|
||||||
|
MatchBlock: currentMatchBlock,
|
||||||
|
})
|
||||||
case *SSHDMatchBlock:
|
case *SSHDMatchBlock:
|
||||||
options = append(options, entry.MatchOption)
|
currentMatchBlock = entry
|
||||||
|
|
||||||
for _, rawOption := range entry.Options.Values() {
|
infos = append(infos, SSHDOptionInfo{
|
||||||
options = append(options, rawOption.(*SSHDOption))
|
Option: entry.MatchOption,
|
||||||
}
|
MatchBlock: currentMatchBlock,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options
|
return infos
|
||||||
}
|
}
|
||||||
|
19
server/handlers/sshd_config/diagnostics/diagnostics.go
Normal file
19
server/handlers/sshd_config/diagnostics/diagnostics.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package diagnostics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateUnknownOption(
|
||||||
|
diagnosticRange protocol.Range,
|
||||||
|
optionName string,
|
||||||
|
) protocol.Diagnostic {
|
||||||
|
return protocol.Diagnostic{
|
||||||
|
Range: diagnosticRange,
|
||||||
|
Message: fmt.Sprintf("Unknown option: %s", optionName),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
}
|
||||||
|
}
|
20
server/handlers/sshd_config/handlers/fetch-code-actions.go
Normal file
20
server/handlers/sshd_config/handlers/fetch-code-actions.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
sshdconfig "config-lsp/handlers/sshd_config"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FetchCodeActions(
|
||||||
|
d *sshdconfig.SSHDDocument,
|
||||||
|
params *protocol.CodeActionParams,
|
||||||
|
) []protocol.CodeAction {
|
||||||
|
if d.Indexes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actions := getKeywordTypoFixes(d, params)
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
110
server/handlers/sshd_config/handlers/fetch-code-actions_typos.go
Normal file
110
server/handlers/sshd_config/handlers/fetch-code-actions_typos.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
sshdconfig "config-lsp/handlers/sshd_config"
|
||||||
|
"config-lsp/handlers/sshd_config/diagnostics"
|
||||||
|
"config-lsp/handlers/sshd_config/fields"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hbollon/go-edlib"
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getKeywordTypoFixes(
|
||||||
|
d *sshdconfig.SSHDDocument,
|
||||||
|
params *protocol.CodeActionParams,
|
||||||
|
) []protocol.CodeAction {
|
||||||
|
if common.ServerOptions.NoTypoSuggestions {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
line := params.Range.Start.Line
|
||||||
|
|
||||||
|
if typoOption, found := d.Indexes.UnknownOptions[line]; found {
|
||||||
|
name := typoOption.Option.Key.Value.Value
|
||||||
|
|
||||||
|
suggestedOptions := findSimilarOptions(name, typoOption.MatchBlock != nil)
|
||||||
|
|
||||||
|
actions := make([]protocol.CodeAction, 0, len(suggestedOptions))
|
||||||
|
|
||||||
|
kind := protocol.CodeActionKindQuickFix
|
||||||
|
for index, normalizedOptionName := range suggestedOptions {
|
||||||
|
isPreferred := index == 0
|
||||||
|
optionName := fields.FieldsNameFormattedMap[normalizedOptionName]
|
||||||
|
|
||||||
|
actions = append(actions, protocol.CodeAction{
|
||||||
|
Title: fmt.Sprintf("Typo Fix: %s", optionName),
|
||||||
|
IsPreferred: &isPreferred,
|
||||||
|
Kind: &kind,
|
||||||
|
Diagnostics: []protocol.Diagnostic{
|
||||||
|
diagnostics.GenerateUnknownOption(
|
||||||
|
typoOption.Option.Key.ToLSPRange(),
|
||||||
|
typoOption.Option.Key.Value.Value,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Edit: &protocol.WorkspaceEdit{
|
||||||
|
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
|
||||||
|
params.TextDocument.URI: {
|
||||||
|
{
|
||||||
|
Range: typoOption.Option.Key.ToLSPRange(),
|
||||||
|
NewText: optionName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find options that are similar to the given option name.
|
||||||
|
// This is used to find typos & suggest the correct option name.
|
||||||
|
// Once an option is found that has a Damerau-Levenshtein distance of 1, it is immediately returned.
|
||||||
|
// If not, then the next 2 options of similarity 2, or 3 options of similarity 3 are returned.
|
||||||
|
// If no options with similarity <= 3 are found, then an empty slice is returned.
|
||||||
|
func findSimilarOptions(
|
||||||
|
optionName string,
|
||||||
|
restrictToMatchOptions bool,
|
||||||
|
) []fields.NormalizedOptionName {
|
||||||
|
normalizedOptionName := string(fields.CreateNormalizedName(optionName))
|
||||||
|
|
||||||
|
optionsPerSimilarity := map[uint8][]fields.NormalizedOptionName{
|
||||||
|
2: make([]fields.NormalizedOptionName, 0, 2),
|
||||||
|
3: make([]fields.NormalizedOptionName, 0, 3),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name := range fields.Options {
|
||||||
|
if restrictToMatchOptions {
|
||||||
|
if _, found := fields.MatchAllowedOptions[name]; !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedName := string(name)
|
||||||
|
similarity := edlib.DamerauLevenshteinDistance(normalizedName, normalizedOptionName)
|
||||||
|
|
||||||
|
switch similarity {
|
||||||
|
case 1:
|
||||||
|
return []fields.NormalizedOptionName{name}
|
||||||
|
case 2:
|
||||||
|
optionsPerSimilarity[2] = append(optionsPerSimilarity[2], name)
|
||||||
|
|
||||||
|
if len(optionsPerSimilarity[2]) >= 2 {
|
||||||
|
return optionsPerSimilarity[2]
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
optionsPerSimilarity[3] = append(optionsPerSimilarity[3], name)
|
||||||
|
|
||||||
|
if len(optionsPerSimilarity[3]) >= 3 {
|
||||||
|
return optionsPerSimilarity[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(optionsPerSimilarity[2], optionsPerSimilarity[3]...)
|
||||||
|
}
|
@ -37,4 +37,6 @@ type SSHDIndexes struct {
|
|||||||
AllOptionsPerName map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption))
|
AllOptionsPerName map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption))
|
||||||
|
|
||||||
Includes map[uint32]*SSHDIndexIncludeLine
|
Includes map[uint32]*SSHDIndexIncludeLine
|
||||||
|
|
||||||
|
UnknownOptions map[uint32]ast.SSHDOptionInfo
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ func CreateIndexes(config ast.SSHDConfig) (*SSHDIndexes, []common.LSPError) {
|
|||||||
indexes := &SSHDIndexes{
|
indexes := &SSHDIndexes{
|
||||||
AllOptionsPerName: make(map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption))),
|
AllOptionsPerName: make(map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption))),
|
||||||
Includes: make(map[uint32]*SSHDIndexIncludeLine),
|
Includes: make(map[uint32]*SSHDIndexIncludeLine),
|
||||||
|
UnknownOptions: make(map[uint32]ast.SSHDOptionInfo),
|
||||||
}
|
}
|
||||||
|
|
||||||
it := config.Options.Iterator()
|
it := config.Options.Iterator()
|
||||||
|
16
server/handlers/sshd_config/lsp/text-document-code-action.go
Normal file
16
server/handlers/sshd_config/lsp/text-document-code-action.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package lsp
|
||||||
|
|
||||||
|
import (
|
||||||
|
sshdconfig "config-lsp/handlers/sshd_config"
|
||||||
|
"config-lsp/handlers/sshd_config/handlers"
|
||||||
|
|
||||||
|
"github.com/tliron/glsp"
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
|
||||||
|
d := sshdconfig.DocumentParserMap[params.TextDocument.URI]
|
||||||
|
actions := handlers.FetchCodeActions(d, params)
|
||||||
|
|
||||||
|
return actions, nil
|
||||||
|
}
|
45
server/handlers/wireguard/analyzer/analyzer.go
Normal file
45
server/handlers/wireguard/analyzer/analyzer.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package analyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/indexes"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
type analyzerContext struct {
|
||||||
|
document *wireguard.WGDocument
|
||||||
|
diagnostics []protocol.Diagnostic
|
||||||
|
}
|
||||||
|
|
||||||
|
func Analyze(
|
||||||
|
d *wireguard.WGDocument,
|
||||||
|
) []protocol.Diagnostic {
|
||||||
|
ctx := &analyzerContext{
|
||||||
|
document: d,
|
||||||
|
diagnostics: make([]protocol.Diagnostic, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeStructureIsValid(ctx)
|
||||||
|
|
||||||
|
if len(ctx.diagnostics) > 0 {
|
||||||
|
return ctx.diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
i, indexErrors := indexes.CreateIndexes(d.Config)
|
||||||
|
|
||||||
|
if len(indexErrors) > 0 {
|
||||||
|
return common.ErrsToDiagnostics(indexErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Indexes = i
|
||||||
|
|
||||||
|
analyzeInterfaceSection(ctx)
|
||||||
|
analyzeDNSPropertyContainsFallback(ctx)
|
||||||
|
analyzeKeepAlivePropertyIsSet(ctx)
|
||||||
|
analyzeSymmetricPropertiesSet(ctx)
|
||||||
|
analyzeDuplicateAllowedIPs(ctx)
|
||||||
|
|
||||||
|
return ctx.diagnostics
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
package handlers
|
package analyzer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"config-lsp/handlers/wireguard/parser"
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
"config-lsp/utils"
|
"config-lsp/utils"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -14,12 +15,14 @@ PrivateKey = abc
|
|||||||
[Interface]
|
[Interface]
|
||||||
PrivateKey = def
|
PrivateKey = def
|
||||||
`)
|
`)
|
||||||
p := parser.CreateWireguardParser()
|
d := &wireguard.WGDocument{
|
||||||
p.ParseFromString(content)
|
Config: ast.NewWGConfig(),
|
||||||
|
}
|
||||||
|
d.Config.Parse(content)
|
||||||
|
|
||||||
diagnostics := Analyze(p)
|
diagnostics := Analyze(d)
|
||||||
|
|
||||||
if len(diagnostics) == 0 {
|
if !(len(diagnostics) > 0) {
|
||||||
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,12 +32,14 @@ func TestInvalidValue(t *testing.T) {
|
|||||||
[Interface]
|
[Interface]
|
||||||
DNS = nope
|
DNS = nope
|
||||||
`)
|
`)
|
||||||
p := parser.CreateWireguardParser()
|
d := &wireguard.WGDocument{
|
||||||
p.ParseFromString(content)
|
Config: ast.NewWGConfig(),
|
||||||
|
}
|
||||||
|
d.Config.Parse(content)
|
||||||
|
|
||||||
diagnostics := Analyze(p)
|
diagnostics := Analyze(d)
|
||||||
|
|
||||||
if len(diagnostics) == 0 {
|
if !(len(diagnostics) > 0) {
|
||||||
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,12 +51,15 @@ PrivateKey = abc
|
|||||||
DNS = 1.1.1.1
|
DNS = 1.1.1.1
|
||||||
PrivateKey = def
|
PrivateKey = def
|
||||||
`)
|
`)
|
||||||
p := parser.CreateWireguardParser()
|
|
||||||
p.ParseFromString(content)
|
|
||||||
|
|
||||||
diagnostics := Analyze(p)
|
d := &wireguard.WGDocument{
|
||||||
|
Config: ast.NewWGConfig(),
|
||||||
|
}
|
||||||
|
d.Config.Parse(content)
|
||||||
|
|
||||||
if len(diagnostics) == 0 {
|
diagnostics := Analyze(d)
|
||||||
|
|
||||||
|
if !(len(diagnostics) > 0) {
|
||||||
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
t.Errorf("Expected diagnostic errors, got %d", len(diagnostics))
|
||||||
}
|
}
|
||||||
}
|
}
|
146
server/handlers/wireguard/analyzer/property.go
Normal file
146
server/handlers/wireguard/analyzer/property.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
package analyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"config-lsp/utils"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func analyzeDNSPropertyContainsFallback(
|
||||||
|
ctx *analyzerContext,
|
||||||
|
) {
|
||||||
|
sections, found := ctx.document.Indexes.SectionsByName["Interface"]
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceSection := sections[0]
|
||||||
|
|
||||||
|
property := interfaceSection.FindFirstPropertyByName("DNS")
|
||||||
|
|
||||||
|
if property == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsAmount := len(strings.Split(property.Value.Value, ","))
|
||||||
|
|
||||||
|
if dnsAmount == 1 {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "There is only one DNS server specified. It is recommended to set up fallback DNS servers",
|
||||||
|
Severity: &common.SeverityHint,
|
||||||
|
Range: property.Value.ToLSPRange(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzeKeepAlivePropertyIsSet(
|
||||||
|
ctx *analyzerContext,
|
||||||
|
) {
|
||||||
|
for _, section := range ctx.document.Indexes.SectionsByName["Peer"] {
|
||||||
|
// If an endpoint is set, then we should only check for the keepalive property
|
||||||
|
if section.FindFirstPropertyByName("Endpoint") != nil && section.FindFirstPropertyByName("PersistentKeepalive") == nil {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "PersistentKeepalive is not set. It is recommended to set this property, as it helps to maintain the connection when users are behind NAT",
|
||||||
|
Severity: &common.SeverityHint,
|
||||||
|
Range: section.Header.ToLSPRange(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzeSymmetricPropertiesSet(
|
||||||
|
ctx *analyzerContext,
|
||||||
|
) {
|
||||||
|
for _, section := range ctx.document.Indexes.SectionsByName["Interface"] {
|
||||||
|
preUpProperty := section.FindFirstPropertyByName("PreUp")
|
||||||
|
preDownProperty := section.FindFirstPropertyByName("PreDown")
|
||||||
|
|
||||||
|
postUpProperty := section.FindFirstPropertyByName("PostUp")
|
||||||
|
postDownProperty := section.FindFirstPropertyByName("PostDown")
|
||||||
|
|
||||||
|
if preUpProperty != nil && preDownProperty == nil {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "PreUp is set, but PreDown is not. It is recommended to set both properties symmetrically",
|
||||||
|
Range: preUpProperty.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityHint,
|
||||||
|
})
|
||||||
|
} else if preUpProperty == nil && preDownProperty != nil {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "PreDown is set, but PreUp is not. It is recommended to set both properties symmetrically",
|
||||||
|
Range: preDownProperty.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityHint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if postUpProperty != nil && postDownProperty == nil {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "PostUp is set, but PostDown is not. It is recommended to set both properties symmetrically",
|
||||||
|
Range: postUpProperty.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityHint,
|
||||||
|
})
|
||||||
|
} else if postUpProperty == nil && postDownProperty != nil {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "PostDown is set, but PostUp is not. It is recommended to set both properties symmetrically",
|
||||||
|
Range: postDownProperty.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityHint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type key int
|
||||||
|
|
||||||
|
const (
|
||||||
|
lineKey key = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strategy
|
||||||
|
// Simply compare the host bits of the IP addresses.
|
||||||
|
// Use a binary tree to store the host bits.
|
||||||
|
func analyzeDuplicateAllowedIPs(
|
||||||
|
ctx *analyzerContext,
|
||||||
|
) {
|
||||||
|
ipHostSet := utils.CreateIPv4HostSet()
|
||||||
|
|
||||||
|
for _, section := range ctx.document.Indexes.SectionsByName["Peer"] {
|
||||||
|
property := section.FindFirstPropertyByName("AllowedIPs")
|
||||||
|
|
||||||
|
if property == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddress, err := netip.ParsePrefix(property.Value.Value)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// This should not happen...
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipContext, _ := ipHostSet.ContainsIP(ipAddress); ipContext != nil {
|
||||||
|
definedLine := (*ipContext).Value(lineKey).(uint32)
|
||||||
|
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine+1),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
Range: property.ToLSPRange(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ipContext := context.WithValue(
|
||||||
|
context.Background(),
|
||||||
|
lineKey,
|
||||||
|
property.Start.Line,
|
||||||
|
)
|
||||||
|
|
||||||
|
ipHostSet.AddIP(
|
||||||
|
ipAddress,
|
||||||
|
ipContext,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
server/handlers/wireguard/analyzer/section.go
Normal file
18
server/handlers/wireguard/analyzer/section.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package analyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func analyzeInterfaceSection(ctx *analyzerContext) {
|
||||||
|
sections := ctx.document.Indexes.SectionsByName["Interface"]
|
||||||
|
if len(sections) > 1 {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "Only one [Interface] section is allowed",
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
Range: sections[1].Header.ToLSPRange(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
107
server/handlers/wireguard/analyzer/structure.go
Normal file
107
server/handlers/wireguard/analyzer/structure.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package analyzer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
docvalues "config-lsp/doc-values"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
|
"config-lsp/handlers/wireguard/fields"
|
||||||
|
"config-lsp/utils"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func analyzeStructureIsValid(ctx *analyzerContext) {
|
||||||
|
for _, section := range ctx.document.Config.Sections {
|
||||||
|
normalizedHeaderName := fields.CreateNormalizedName(section.Header.Name)
|
||||||
|
// Whether to check if the property is allowed in the section
|
||||||
|
checkAllowedProperty := true
|
||||||
|
|
||||||
|
if section.Header.Name == "" {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "This section is missing a name",
|
||||||
|
Range: section.Header.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
})
|
||||||
|
} else if !utils.KeyExists(fields.OptionsHeaderMap, normalizedHeaderName) {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: fmt.Sprintf("Unknown section '%s'. It must be one of: [Interface], [Peer]", section.Header.Name),
|
||||||
|
Range: section.Header.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
})
|
||||||
|
// Do not check as the section is unknown
|
||||||
|
checkAllowedProperty = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if section.Properties.Size() == 0 {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "This section is empty",
|
||||||
|
Range: section.Header.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityInformation,
|
||||||
|
Tags: []protocol.DiagnosticTag{
|
||||||
|
protocol.DiagnosticTagUnnecessary,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
existingProperties := make(map[fields.NormalizedName]*ast.WGProperty)
|
||||||
|
|
||||||
|
it := section.Properties.Iterator()
|
||||||
|
for it.Next() {
|
||||||
|
property := it.Value().(*ast.WGProperty)
|
||||||
|
normalizedPropertyName := fields.CreateNormalizedName(property.Key.Name)
|
||||||
|
|
||||||
|
if property.Key.Name == "" {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "This property is missing a name",
|
||||||
|
Range: property.Key.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if property.Value == nil || property.Value.Value == "" {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: "This property is missing a value",
|
||||||
|
Range: property.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
})
|
||||||
|
checkAllowedProperty = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkAllowedProperty {
|
||||||
|
availableOptions := fields.OptionsHeaderMap[normalizedHeaderName]
|
||||||
|
|
||||||
|
// Duplicate check
|
||||||
|
if existingProperty, found := existingProperties[normalizedPropertyName]; found {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: fmt.Sprintf("Property '%s' has already been defined on line %d", property.Key.Name, existingProperty.Start.Line+1),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
Range: existingProperty.ToLSPRange(),
|
||||||
|
})
|
||||||
|
// Check if value is valid
|
||||||
|
} else if option, found := availableOptions[normalizedPropertyName]; found {
|
||||||
|
invalidValues := option.DeprecatedCheckIsValid(property.Value.Value)
|
||||||
|
|
||||||
|
for _, invalidValue := range invalidValues {
|
||||||
|
err := docvalues.LSPErrorFromInvalidValue(property.Start.Line, *invalidValue).ShiftCharacter(property.Value.Start.Character)
|
||||||
|
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Range: err.Range.ToLSPRange(),
|
||||||
|
Message: err.Err.Error(),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Unknown property
|
||||||
|
} else {
|
||||||
|
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
|
||||||
|
Message: fmt.Sprintf("Unknown property '%s'", property.Key.Name),
|
||||||
|
Range: property.Key.ToLSPRange(),
|
||||||
|
Severity: &common.SeverityError,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
existingProperties[normalizedPropertyName] = property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
246
server/handlers/wireguard/ast/parser.go
Normal file
246
server/handlers/wireguard/ast/parser.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"config-lsp/utils"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/emirpasic/gods/maps/treemap"
|
||||||
|
gods "github.com/emirpasic/gods/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewWGConfig() *WGConfig {
|
||||||
|
config := &WGConfig{}
|
||||||
|
config.Clear()
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WGConfig) Clear() {
|
||||||
|
c.Sections = make([]*WGSection, 0, 2)
|
||||||
|
c.CommentLines = make(map[uint32]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var commentPattern = regexp.MustCompile(`^\s*([;#])`)
|
||||||
|
var emptyPattern = regexp.MustCompile(`^\s*$`)
|
||||||
|
var headerPattern = regexp.MustCompile(`^\s*\[(\w+)?]?`)
|
||||||
|
var linePattern = regexp.MustCompile(`^\s*(?P<key>.+?)\s*(?P<separator>=)\s*(?P<value>\S.*?)?\s*(?:[;#].*)?\s*$`)
|
||||||
|
|
||||||
|
func (c *WGConfig) Parse(input string) []common.LSPError {
|
||||||
|
errors := make([]common.LSPError, 0)
|
||||||
|
lines := utils.SplitIntoLines(input)
|
||||||
|
|
||||||
|
var currentSection *WGSection
|
||||||
|
|
||||||
|
for rawLineNumber, line := range lines {
|
||||||
|
lineNumber := uint32(rawLineNumber)
|
||||||
|
|
||||||
|
if emptyPattern.MatchString(line) {
|
||||||
|
// Set end of last section
|
||||||
|
if currentSection != nil {
|
||||||
|
currentSection.End.Line = lineNumber
|
||||||
|
currentSection.End.Character = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if commentPattern.MatchString(line) {
|
||||||
|
c.CommentLines[lineNumber] = struct{}{}
|
||||||
|
// Set end of last section
|
||||||
|
if currentSection != nil {
|
||||||
|
currentSection.End.Line = lineNumber
|
||||||
|
currentSection.End.Character = uint32(len(line))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if headerPattern.MatchString(line) {
|
||||||
|
name := headerPattern.FindStringSubmatch(line)[1]
|
||||||
|
|
||||||
|
currentSection = &WGSection{
|
||||||
|
LocationRange: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: uint32(len(line)) + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Header: WGHeader{
|
||||||
|
LocationRange: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: uint32(len(line)) + 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Properties: treemap.NewWith(gods.UInt32Comparator),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Sections = append(c.Sections, currentSection)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else property
|
||||||
|
|
||||||
|
// Set end of last section
|
||||||
|
if currentSection != nil {
|
||||||
|
currentSection.End.Line = lineNumber
|
||||||
|
currentSection.End.Character = uint32(len(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSection == nil {
|
||||||
|
// Root properties are not allowed
|
||||||
|
errors = append(errors, common.LSPError{
|
||||||
|
Range: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: uint32(len(line)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err: fmt.Errorf("A header is missing before a property. This property has no header above it."),
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(line, "=") {
|
||||||
|
// Incomplete property
|
||||||
|
indexes := utils.GetTrimIndex(line)
|
||||||
|
|
||||||
|
newProperty := &WGProperty{
|
||||||
|
Key: WGPropertyKey{
|
||||||
|
LocationRange: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: uint32(indexes[0]),
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: uint32(indexes[1]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: line[indexes[0]:indexes[1]],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSection.Properties.Put(lineNumber, newProperty)
|
||||||
|
} else {
|
||||||
|
// Fully written out property
|
||||||
|
|
||||||
|
indexes := linePattern.FindStringSubmatchIndex(line)
|
||||||
|
|
||||||
|
if len(indexes) == 0 {
|
||||||
|
// Error
|
||||||
|
errors = append(errors, common.LSPError{
|
||||||
|
Range: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: uint32(len(line)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Err: fmt.Errorf("This property seems to be malformed"),
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct key
|
||||||
|
keyStart := uint32(indexes[2])
|
||||||
|
keyEnd := uint32(indexes[3])
|
||||||
|
key := WGPropertyKey{
|
||||||
|
LocationRange: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: keyStart,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: keyEnd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: line[keyStart:keyEnd],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct separator
|
||||||
|
separatorStart := uint32(indexes[4])
|
||||||
|
separatorEnd := uint32(indexes[5])
|
||||||
|
separator := WGPropertySeparator{
|
||||||
|
LocationRange: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: separatorStart,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: separatorEnd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct value
|
||||||
|
var value *WGPropertyValue
|
||||||
|
propertyEnd := uint32(len(line))
|
||||||
|
|
||||||
|
if indexes[6] != -1 && indexes[7] != -1 {
|
||||||
|
// value exists
|
||||||
|
valueStart := uint32(indexes[6])
|
||||||
|
valueEnd := uint32(indexes[7])
|
||||||
|
propertyEnd = valueEnd
|
||||||
|
|
||||||
|
value = &WGPropertyValue{
|
||||||
|
LocationRange: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: valueStart,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: valueEnd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: line[valueStart:valueEnd],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And lastly, add the property
|
||||||
|
newProperty := &WGProperty{
|
||||||
|
LocationRange: common.LocationRange{
|
||||||
|
Start: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: keyStart,
|
||||||
|
},
|
||||||
|
End: common.Location{
|
||||||
|
Line: lineNumber,
|
||||||
|
Character: propertyEnd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RawValue: line,
|
||||||
|
Key: key,
|
||||||
|
Separator: &separator,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
currentSection.Properties.Put(lineNumber, newProperty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
68
server/handlers/wireguard/ast/parser_test.go
Normal file
68
server/handlers/wireguard/ast/parser_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/utils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExample1Works(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
sample := utils.Dedent(`
|
||||||
|
# A comment at the very top
|
||||||
|
|
||||||
|
|
||||||
|
[Interface]
|
||||||
|
PrivateKey = 1234567890 # Some comment
|
||||||
|
Address = 10.0.0.1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = 1234567890
|
||||||
|
|
||||||
|
; I'm a comment
|
||||||
|
`)
|
||||||
|
|
||||||
|
config := NewWGConfig()
|
||||||
|
|
||||||
|
errors := config.Parse(sample)
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
t.Fatalf("Parse: Expected no errors, but got %v", errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(utils.KeyExists(config.CommentLines, 0) && utils.KeyExists(config.CommentLines, 12)) {
|
||||||
|
t.Errorf("Parse: Expected comments to be present on lines 0 and 12")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(config.Sections[0].Start.Line == 3 && config.Sections[0].End.Line == 8) {
|
||||||
|
t.Errorf("Parse: Expected section 0 to be present on lines 3 and 6, but it is: %v", config.Sections[0].End)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(config.Sections[0].Start.Character == 0 && config.Sections[0].End.Character == 0) {
|
||||||
|
t.Errorf("Parse: Expected section 0 to be present on characters 0 and 0, but it is: %v", config.Sections[0].End)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(config.Sections[0].Header.Name == "Interface" && config.Sections[1].Header.Name == "Peer") {
|
||||||
|
t.Errorf("Parse: Expected sections to be present on lines 0, 1, and 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawFourthProperty, _ := config.Sections[0].Properties.Get(uint32(4))
|
||||||
|
fourthProperty := rawFourthProperty.(*WGProperty)
|
||||||
|
if !(fourthProperty.Key.Name == "PrivateKey" && fourthProperty.Value.Value == "1234567890") {
|
||||||
|
t.Errorf("Parse: Expected property line 4 to be correct")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawFifthProperty, _ := config.Sections[0].Properties.Get(uint32(5))
|
||||||
|
fifthProperty := rawFifthProperty.(*WGProperty)
|
||||||
|
if !(fifthProperty.Key.Name == "Address" && fifthProperty.Value.Value == "10.0.0.1") {
|
||||||
|
t.Errorf("Parse: Expected property line 5 to be correct")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawTenthProperty, _ := config.Sections[1].Properties.Get(uint32(10))
|
||||||
|
tenthProperty := rawTenthProperty.(*WGProperty)
|
||||||
|
if !(tenthProperty.Key.Name == "PublicKey" && tenthProperty.Value.Value == "1234567890") {
|
||||||
|
t.Errorf("Parse: Expected property line 10 to be correct")
|
||||||
|
}
|
||||||
|
}
|
46
server/handlers/wireguard/ast/wireguard.go
Normal file
46
server/handlers/wireguard/ast/wireguard.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"github.com/emirpasic/gods/maps/treemap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WGPropertyKey struct {
|
||||||
|
common.LocationRange
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WGPropertyValue struct {
|
||||||
|
common.LocationRange
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WGPropertySeparator struct {
|
||||||
|
common.LocationRange
|
||||||
|
}
|
||||||
|
|
||||||
|
type WGProperty struct {
|
||||||
|
common.LocationRange
|
||||||
|
RawValue string
|
||||||
|
Key WGPropertyKey
|
||||||
|
Separator *WGPropertySeparator
|
||||||
|
Value *WGPropertyValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type WGHeader struct {
|
||||||
|
common.LocationRange
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WGSection struct {
|
||||||
|
common.LocationRange
|
||||||
|
Header WGHeader
|
||||||
|
// [uint32]WGProperty: line number -> WGProperty
|
||||||
|
Properties *treemap.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
type WGConfig struct {
|
||||||
|
Sections []*WGSection
|
||||||
|
// Used to identify where not to show diagnostics
|
||||||
|
CommentLines map[uint32]struct{}
|
||||||
|
}
|
77
server/handlers/wireguard/ast/wireguard_fields.go
Normal file
77
server/handlers/wireguard/ast/wireguard_fields.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *WGConfig) FindSectionByLine(line uint32) *WGSection {
|
||||||
|
index, found := slices.BinarySearchFunc(
|
||||||
|
c.Sections,
|
||||||
|
line,
|
||||||
|
func(current *WGSection, target uint32) int {
|
||||||
|
if target < current.Start.Line {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
if target > current.End.Line {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Sections[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WGConfig) FindPropertyByLine(line uint32) *WGProperty {
|
||||||
|
section := c.FindSectionByLine(line)
|
||||||
|
|
||||||
|
if section == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if property, found := section.Properties.Get(line); found {
|
||||||
|
return property.(*WGProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WGSection) FindFirstPropertyByName(name string) *WGProperty {
|
||||||
|
it := s.Properties.Iterator()
|
||||||
|
for it.Next() {
|
||||||
|
property := it.Value().(*WGProperty)
|
||||||
|
if property.Key.Name == name {
|
||||||
|
return property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WGSection) FindPropertyByName(name string) *WGProperty {
|
||||||
|
it := s.Properties.Iterator()
|
||||||
|
for it.Next() {
|
||||||
|
property := it.Value().(*WGProperty)
|
||||||
|
if property.Key.Name == name {
|
||||||
|
return property
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WGSection) GetLastProperty() *WGProperty {
|
||||||
|
if s.Properties.Size() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastLine, _ := s.Properties.Max()
|
||||||
|
lastProperty, _ := s.Properties.Get(lastLine)
|
||||||
|
return lastProperty.(*WGProperty)
|
||||||
|
}
|
9
server/handlers/wireguard/fields/common.go
Normal file
9
server/handlers/wireguard/fields/common.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type NormalizedName string
|
||||||
|
|
||||||
|
func CreateNormalizedName(s string) NormalizedName {
|
||||||
|
return NormalizedName(strings.ToLower(s))
|
||||||
|
}
|
@ -25,8 +25,8 @@ var maxPortValue = 65535
|
|||||||
var minMTUValue = 68
|
var minMTUValue = 68
|
||||||
var maxMTUValue = 1500
|
var maxMTUValue = 1500
|
||||||
|
|
||||||
var InterfaceOptions = map[string]docvalues.DocumentationValue{
|
var InterfaceOptions = map[NormalizedName]docvalues.DocumentationValue{
|
||||||
"Address": {
|
"address": {
|
||||||
Documentation: `Defines what address range the local node should route traffic for. Depending on whether the node is a simple client joining the VPN subnet, or a bounce server that's relaying traffic between multiple clients, this can be set to a single IP of the node itself (specified with CIDR notation), e.g. 192.0.2.3/32), or a range of IPv4/IPv6 subnets that the node can route traffic for.
|
Documentation: `Defines what address range the local node should route traffic for. Depending on whether the node is a simple client joining the VPN subnet, or a bounce server that's relaying traffic between multiple clients, this can be set to a single IP of the node itself (specified with CIDR notation), e.g. 192.0.2.3/32), or a range of IPv4/IPv6 subnets that the node can route traffic for.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -49,7 +49,7 @@ You can also specify multiple subnets or IPv6 subnets like so:
|
|||||||
AllowRange: true,
|
AllowRange: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ListenPort": {
|
"listenport": {
|
||||||
Documentation: `When the node is acting as a public bounce server, it should hardcode a port to listen for incoming VPN connections from the public internet. Clients not acting as relays should not set this value. If not specified, chosen randomly.
|
Documentation: `When the node is acting as a public bounce server, it should hardcode a port to listen for incoming VPN connections from the public internet. Clients not acting as relays should not set this value. If not specified, chosen randomly.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -66,14 +66,14 @@ Using custom WireGuard port
|
|||||||
Max: &maxPortValue,
|
Max: &maxPortValue,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"PrivateKey": {
|
"privatekey": {
|
||||||
Documentation: `This is the private key for the local node, never shared with other servers. All nodes must have a private key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN.
|
Documentation: `This is the private key for the local node, never shared with other servers. All nodes must have a private key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN.
|
||||||
|
|
||||||
This key can be generated with [wg genkey > example.key]
|
This key can be generated with [wg genkey > example.key]
|
||||||
`,
|
`,
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
"DNS": {
|
"dns": {
|
||||||
Documentation: `The DNS server(s) to announce to VPN clients via DHCP, most clients will use this server for DNS requests over the VPN, but clients can also override this value locally on their nodes
|
Documentation: `The DNS server(s) to announce to VPN clients via DHCP, most clients will use this server for DNS requests over the VPN, but clients can also override this value locally on their nodes
|
||||||
|
|
||||||
The value can be left unconfigured to use the system's default DNS servers
|
The value can be left unconfigured to use the system's default DNS servers
|
||||||
@ -97,7 +97,7 @@ or multiple DNS servers can be provided
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Table": {
|
"table": {
|
||||||
Documentation: `Optionally defines which routing table to use for the WireGuard routes, not necessary to configure for most setups.
|
Documentation: `Optionally defines which routing table to use for the WireGuard routes, not necessary to configure for most setups.
|
||||||
|
|
||||||
There are two special values: ‘off’ disables the creation of routes altogether, and ‘auto’ (the default) adds routes to the default table and enables special handling of default routes.
|
There are two special values: ‘off’ disables the creation of routes altogether, and ‘auto’ (the default) adds routes to the default table and enables special handling of default routes.
|
||||||
@ -127,7 +127,7 @@ https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"MTU": {
|
"mtu": {
|
||||||
Documentation: `Optionally defines the maximum transmission unit (MTU, aka packet/frame size) to use when connecting to the peer, not necessary to configure for most setups.
|
Documentation: `Optionally defines the maximum transmission unit (MTU, aka packet/frame size) to use when connecting to the peer, not necessary to configure for most setups.
|
||||||
|
|
||||||
The MTU is automatically determined from the endpoint addresses or the system default route, which is usually a sane choice.
|
The MTU is automatically determined from the endpoint addresses or the system default route, which is usually a sane choice.
|
||||||
@ -142,7 +142,7 @@ https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
|
|||||||
Max: &maxMTUValue,
|
Max: &maxMTUValue,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"PreUp": {
|
"preup": {
|
||||||
Documentation: `Optionally run a command before the interface is brought up. This option can be specified multiple times, with commands executed in the order they appear in the file.
|
Documentation: `Optionally run a command before the interface is brought up. This option can be specified multiple times, with commands executed in the order they appear in the file.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -152,7 +152,7 @@ Add an IP route
|
|||||||
PreUp = ip rule add ipproto tcp dport 22 table 1234
|
PreUp = ip rule add ipproto tcp dport 22 table 1234
|
||||||
`, Value: docvalues.StringValue{},
|
`, Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
"PostUp": {
|
"postup": {
|
||||||
Documentation: `Optionally run a command after the interface is brought up. This option can appear multiple times, as with PreUp
|
Documentation: `Optionally run a command after the interface is brought up. This option can appear multiple times, as with PreUp
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -182,7 +182,7 @@ Force WireGuard to re-resolve IP address for peer domain
|
|||||||
`,
|
`,
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
"PreDown": {
|
"predown": {
|
||||||
Documentation: `Optionally run a command before the interface is brought down. This option can appear multiple times, as with PreUp
|
Documentation: `Optionally run a command before the interface is brought down. This option can appear multiple times, as with PreUp
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -196,7 +196,7 @@ Hit a webhook on another server
|
|||||||
`,
|
`,
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
"PostDown": {
|
"postdown": {
|
||||||
Documentation: `Optionally run a command after the interface is brought down. This option can appear multiple times, as with PreUp
|
Documentation: `Optionally run a command after the interface is brought down. This option can appear multiple times, as with PreUp
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -215,21 +215,21 @@ Remove the iptables rule that forwards packets on the WireGuard interface
|
|||||||
`,
|
`,
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
"FwMark": {
|
"fwmark": {
|
||||||
Documentation: "a 32-bit fwmark for outgoing packets. If set to 0 or \"off\", this option is disabled. May be specified in hexadecimal by prepending \"0x\". Optional",
|
Documentation: "a 32-bit fwmark for outgoing packets. If set to 0 or \"off\", this option is disabled. May be specified in hexadecimal by prepending \"0x\". Optional",
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var InterfaceAllowedDuplicateFields = map[string]struct{}{
|
var InterfaceAllowedDuplicateFields = map[NormalizedName]struct{}{
|
||||||
"PreUp": {},
|
"preup": {},
|
||||||
"PostUp": {},
|
"postup": {},
|
||||||
"PreDown": {},
|
"predown": {},
|
||||||
"PostDown": {},
|
"postdown": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
var PeerOptions = map[string]docvalues.DocumentationValue{
|
var PeerOptions = map[NormalizedName]docvalues.DocumentationValue{
|
||||||
"Endpoint": {
|
"endpoint": {
|
||||||
Documentation: `Defines the publicly accessible address for a remote peer. This should be left out for peers behind a NAT or peers that don't have a stable publicly accessible IP:PORT pair. Typically, this only needs to be defined on the main bounce server, but it can also be defined on other public nodes with stable IPs like public-server2 in the example config below.
|
Documentation: `Defines the publicly accessible address for a remote peer. This should be left out for peers behind a NAT or peers that don't have a stable publicly accessible IP:PORT pair. Typically, this only needs to be defined on the main bounce server, but it can also be defined on other public nodes with stable IPs like public-server2 in the example config below.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -243,7 +243,7 @@ Endpoint is a hostname/FQDN
|
|||||||
`,
|
`,
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
"AllowedIPs": {
|
"allowedips": {
|
||||||
Documentation: `This defines the IP ranges for which a peer will route traffic. On simple clients, this is usually a single address (the VPN address of the simple client itself). For bounce servers this will be a range of the IPs or subnets that the relay server is capable of routing traffic for. Multiple IPs and subnets may be specified using comma-separated IPv4 or IPv6 CIDR notation (from a single /32 or /128 address, all the way up to 0.0.0.0/0 and ::/0 to indicate a default route to send all internet and VPN traffic through that peer). This option may be specified multiple times.
|
Documentation: `This defines the IP ranges for which a peer will route traffic. On simple clients, this is usually a single address (the VPN address of the simple client itself). For bounce servers this will be a range of the IPs or subnets that the relay server is capable of routing traffic for. Multiple IPs and subnets may be specified using comma-separated IPv4 or IPv6 CIDR notation (from a single /32 or /128 address, all the way up to 0.0.0.0/0 and ::/0 to indicate a default route to send all internet and VPN traffic through that peer). This option may be specified multiple times.
|
||||||
|
|
||||||
When deciding how to route a packet, the system chooses the most specific route first, and falls back to broader routes. So for a packet destined to 192.0.2.3, the system would first look for a peer advertising 192.0.2.3/32 specifically, and would fall back to a peer advertising 192.0.2.1/24 or a larger range like 0.0.0.0/0 as a last resort.
|
When deciding how to route a packet, the system chooses the most specific route first, and falls back to broader routes. So for a packet destined to 192.0.2.3, the system would first look for a peer advertising 192.0.2.3/32 specifically, and would fall back to a peer advertising 192.0.2.1/24 or a larger range like 0.0.0.0/0 as a last resort.
|
||||||
@ -280,7 +280,7 @@ Peer is a relay server that routes to itself and all nodes on its local LAN
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"PublicKey": {
|
"publickey": {
|
||||||
Documentation: `This is the public key for the remote node, shareable with all peers. All nodes must have a public key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN.
|
Documentation: `This is the public key for the remote node, shareable with all peers. All nodes must have a public key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN.
|
||||||
|
|
||||||
This key can be generated with wg pubkey < example.key > example.key.pub. (see above for how to generate the private key example.key)
|
This key can be generated with wg pubkey < example.key > example.key.pub. (see above for how to generate the private key example.key)
|
||||||
@ -291,7 +291,7 @@ This key can be generated with wg pubkey < example.key > example.key.pub. (see a
|
|||||||
`,
|
`,
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
"PersistentKeepalive": {
|
"persistentkeepalive": {
|
||||||
Documentation: `If the connection is going from a NAT-ed peer to a public peer, the node behind the NAT must regularly send an outgoing ping in order to keep the bidirectional connection alive in the NAT router's connection table.
|
Documentation: `If the connection is going from a NAT-ed peer to a public peer, the node behind the NAT must regularly send an outgoing ping in order to keep the bidirectional connection alive in the NAT router's connection table.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -310,17 +310,17 @@ Oocal NAT-ed node to remote public node
|
|||||||
`,
|
`,
|
||||||
Value: docvalues.PositiveNumberValue(),
|
Value: docvalues.PositiveNumberValue(),
|
||||||
},
|
},
|
||||||
"PresharedKey": {
|
"presharedkey": {
|
||||||
Documentation: "Optionally defines a pre-shared key for the peer, used to authenticate the connection. This is not necessary, but strongly recommended for security.",
|
Documentation: "Optionally defines a pre-shared key for the peer, used to authenticate the connection. This is not necessary, but strongly recommended for security.",
|
||||||
Value: docvalues.StringValue{},
|
Value: docvalues.StringValue{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var PeerAllowedDuplicateFields = map[string]struct{}{
|
var PeerAllowedDuplicateFields = map[NormalizedName]struct{}{
|
||||||
"AllowedIPs": {},
|
"allowedips": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
var OptionsHeaderMap = map[string](map[string]docvalues.DocumentationValue){
|
var OptionsHeaderMap = map[NormalizedName](map[NormalizedName]docvalues.DocumentationValue){
|
||||||
"Interface": InterfaceOptions,
|
"interface": InterfaceOptions,
|
||||||
"Peer": PeerOptions,
|
"peer": PeerOptions,
|
||||||
}
|
}
|
23
server/handlers/wireguard/fields/fields_formatted.go
Normal file
23
server/handlers/wireguard/fields/fields_formatted.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package fields
|
||||||
|
|
||||||
|
var AllOptionsFormatted = map[NormalizedName]string{
|
||||||
|
// Interface
|
||||||
|
"address": "Address",
|
||||||
|
"listenport": "ListenPort",
|
||||||
|
"privatekey": "PrivateKey",
|
||||||
|
"dns": "DNS",
|
||||||
|
"table": "Table",
|
||||||
|
"mtu": "MTU",
|
||||||
|
"preup": "PreUp",
|
||||||
|
"postup": "PostUp",
|
||||||
|
"predown": "Predown",
|
||||||
|
"postdown": "PostDown",
|
||||||
|
"fwmark": "FwMark",
|
||||||
|
|
||||||
|
// Peer Options
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"allowedips": "AllowedIPs",
|
||||||
|
"publickey": "PublicKey",
|
||||||
|
"persistentkeepalive": "PersistentKeepalive",
|
||||||
|
"presharedkey": "PresharedKey",
|
||||||
|
}
|
@ -1,452 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
docvalues "config-lsp/doc-values"
|
|
||||||
"config-lsp/handlers/wireguard/fields"
|
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
"config-lsp/utils"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/netip"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Analyze(
|
|
||||||
p parser.WireguardParser,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
sectionsErrors := analyzeSections(p.Sections)
|
|
||||||
sectionsErrors = append(sectionsErrors, analyzeOnlyOneInterfaceSectionSpecified(p)...)
|
|
||||||
|
|
||||||
if len(sectionsErrors) > 0 {
|
|
||||||
return sectionsErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
validCheckErrors := checkIfValuesAreValid(p.Sections)
|
|
||||||
|
|
||||||
if len(validCheckErrors) > 0 {
|
|
||||||
return validCheckErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
diagnostics := make([]protocol.Diagnostic, 0)
|
|
||||||
diagnostics = append(diagnostics, analyzeParserForDuplicateProperties(p)...)
|
|
||||||
diagnostics = append(diagnostics, analyzeDNSContainsFallback(p)...)
|
|
||||||
diagnostics = append(diagnostics, analyzeKeepAliveIsSet(p)...)
|
|
||||||
diagnostics = append(diagnostics, analyzeSymmetricPropertiesExist(p)...)
|
|
||||||
diagnostics = append(diagnostics, analyzeDuplicateAllowedIPs(p)...)
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeSections(
|
|
||||||
sections []*parser.WireguardSection,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
var diagnostics []protocol.Diagnostic
|
|
||||||
|
|
||||||
for _, section := range sections {
|
|
||||||
sectionDiagnostics := analyzeSection(*section)
|
|
||||||
|
|
||||||
if len(sectionDiagnostics) > 0 {
|
|
||||||
diagnostics = append(diagnostics, sectionDiagnostics...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(diagnostics) > 0 {
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeOnlyOneInterfaceSectionSpecified(
|
|
||||||
p parser.WireguardParser,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
var diagnostics []protocol.Diagnostic
|
|
||||||
alreadyFound := false
|
|
||||||
|
|
||||||
for _, section := range p.GetSectionsByName("Interface") {
|
|
||||||
if alreadyFound {
|
|
||||||
severity := protocol.DiagnosticSeverityError
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: "Only one [Interface] section is allowed",
|
|
||||||
Severity: &severity,
|
|
||||||
Range: section.GetHeaderLineRange(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
alreadyFound = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeDNSContainsFallback(
|
|
||||||
p parser.WireguardParser,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
lineNumber, property := p.FindFirstPropertyByName("DNS")
|
|
||||||
|
|
||||||
if property == nil {
|
|
||||||
return []protocol.Diagnostic{}
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsAmount := len(strings.Split(property.Value.Value, ","))
|
|
||||||
|
|
||||||
if dnsAmount == 1 {
|
|
||||||
severity := protocol.DiagnosticSeverityHint
|
|
||||||
|
|
||||||
return []protocol.Diagnostic{
|
|
||||||
{
|
|
||||||
Message: "There is only one DNS server specified. It is recommended to set up fallback DNS servers",
|
|
||||||
Severity: &severity,
|
|
||||||
Range: protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: *lineNumber,
|
|
||||||
Character: property.Value.Location.Start,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: *lineNumber,
|
|
||||||
Character: property.Value.Location.End,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return []protocol.Diagnostic{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeKeepAliveIsSet(
|
|
||||||
p parser.WireguardParser,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
var diagnostics []protocol.Diagnostic
|
|
||||||
|
|
||||||
for _, section := range p.GetSectionsByName("Peer") {
|
|
||||||
// If an endpoint is set, then we should only check for the keepalive property
|
|
||||||
if section.ExistsProperty("Endpoint") && !section.ExistsProperty("PersistentKeepalive") {
|
|
||||||
severity := protocol.DiagnosticSeverityHint
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: "PersistentKeepalive is not set. It is recommended to set this property, as it helps to maintain the connection when users are behind NAT",
|
|
||||||
Severity: &severity,
|
|
||||||
Range: section.GetRange(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the values are valid.
|
|
||||||
// Assumes that sections have been analyzed already.
|
|
||||||
func checkIfValuesAreValid(
|
|
||||||
sections []*parser.WireguardSection,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
var diagnostics []protocol.Diagnostic
|
|
||||||
|
|
||||||
for _, section := range sections {
|
|
||||||
for lineNumber, property := range section.Properties {
|
|
||||||
diagnostics = append(
|
|
||||||
diagnostics,
|
|
||||||
analyzeProperty(property, section, lineNumber)...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeSection(
|
|
||||||
s parser.WireguardSection,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
var diagnostics []protocol.Diagnostic
|
|
||||||
|
|
||||||
if s.Name == nil {
|
|
||||||
// No section name
|
|
||||||
severity := protocol.DiagnosticSeverityError
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: "This section is missing a name",
|
|
||||||
Severity: &severity,
|
|
||||||
Range: s.GetRange(),
|
|
||||||
})
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, found := fields.OptionsHeaderMap[*s.Name]; !found {
|
|
||||||
// Unknown section
|
|
||||||
severity := protocol.DiagnosticSeverityError
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: fmt.Sprintf("Unknown section '%s'. It must be one of: [Interface], [Peer]", *s.Name),
|
|
||||||
Severity: &severity,
|
|
||||||
Range: s.GetHeaderLineRange(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the property is valid.
|
|
||||||
// Returns a list of diagnostics.
|
|
||||||
// `belongingSection` is the section to which the property belongs. This value is
|
|
||||||
// expected to be non-nil and expected to be a valid Wireguard section.
|
|
||||||
func analyzeProperty(
|
|
||||||
p parser.WireguardProperty,
|
|
||||||
belongingSection *parser.WireguardSection,
|
|
||||||
propertyLine uint32,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
sectionOptions := fields.OptionsHeaderMap[*belongingSection.Name]
|
|
||||||
option, found := sectionOptions[p.Key.Name]
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
// Unknown property
|
|
||||||
severity := protocol.DiagnosticSeverityError
|
|
||||||
return []protocol.Diagnostic{
|
|
||||||
{
|
|
||||||
Message: fmt.Sprintf("Unknown property '%s'", p.Key.Name),
|
|
||||||
Severity: &severity,
|
|
||||||
Range: protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: propertyLine,
|
|
||||||
Character: p.Key.Location.Start,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: propertyLine,
|
|
||||||
Character: p.Key.Location.End,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.Value == nil {
|
|
||||||
// No value to check
|
|
||||||
severity := protocol.DiagnosticSeverityWarning
|
|
||||||
return []protocol.Diagnostic{
|
|
||||||
{
|
|
||||||
Message: "Property is missing a value",
|
|
||||||
Severity: &severity,
|
|
||||||
Range: p.GetLineRange(propertyLine),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errors := option.DeprecatedCheckIsValid(p.Value.Value)
|
|
||||||
|
|
||||||
return utils.Map(errors, func(err *docvalues.InvalidValue) protocol.Diagnostic {
|
|
||||||
severity := protocol.DiagnosticSeverityError
|
|
||||||
return protocol.Diagnostic{
|
|
||||||
Message: err.GetMessage(),
|
|
||||||
Severity: &severity,
|
|
||||||
Range: protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: propertyLine,
|
|
||||||
Character: p.Value.Location.Start + err.Start,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: propertyLine,
|
|
||||||
Character: p.Value.Location.Start + err.End,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeParserForDuplicateProperties(
|
|
||||||
p parser.WireguardParser,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
diagnostics := make([]protocol.Diagnostic, 0)
|
|
||||||
|
|
||||||
for _, section := range p.Sections {
|
|
||||||
diagnostics = append(diagnostics, analyzeDuplicateProperties(*section)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeDuplicateProperties(
|
|
||||||
s parser.WireguardSection,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
var diagnostics []protocol.Diagnostic
|
|
||||||
|
|
||||||
existingProperties := make(map[string]uint32)
|
|
||||||
|
|
||||||
lines := utils.KeysOfMap(s.Properties)
|
|
||||||
slices.Sort(lines)
|
|
||||||
|
|
||||||
for _, currentLineNumber := range lines {
|
|
||||||
property := s.Properties[currentLineNumber]
|
|
||||||
var skipCheck = false
|
|
||||||
|
|
||||||
if s.Name != nil {
|
|
||||||
switch *s.Name {
|
|
||||||
case "Interface":
|
|
||||||
if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found {
|
|
||||||
skipCheck = true
|
|
||||||
}
|
|
||||||
case "Peer":
|
|
||||||
if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found {
|
|
||||||
skipCheck = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if skipCheck {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingLineNumber, found := existingProperties[property.Key.Name]; found {
|
|
||||||
severity := protocol.DiagnosticSeverityError
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: fmt.Sprintf("Property '%s' is already defined on line %d", property.Key.Name, existingLineNumber+1),
|
|
||||||
Severity: &severity,
|
|
||||||
Range: protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: currentLineNumber,
|
|
||||||
Character: 0,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: currentLineNumber,
|
|
||||||
Character: 99999,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
existingProperties[property.Key.Name] = currentLineNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
type propertyWithLine struct {
|
|
||||||
Line uint32
|
|
||||||
Property parser.WireguardProperty
|
|
||||||
IpPrefix netip.Prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapAllowedIPsToMasks(p parser.WireguardParser) map[uint8][]propertyWithLine {
|
|
||||||
ips := make(map[uint8][]propertyWithLine)
|
|
||||||
|
|
||||||
for _, section := range p.GetSectionsByName("Peer") {
|
|
||||||
for lineNumber, property := range section.Properties {
|
|
||||||
if property.Key.Name == "AllowedIPs" {
|
|
||||||
ipAddress, err := netip.ParsePrefix(property.Value.Value)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// This should not happen...
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
hostBits := uint8(ipAddress.Bits())
|
|
||||||
|
|
||||||
if _, found := ips[uint8(hostBits)]; !found {
|
|
||||||
ips[hostBits] = make([]propertyWithLine, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
ips[hostBits] = append(ips[hostBits], propertyWithLine{
|
|
||||||
Line: uint32(lineNumber),
|
|
||||||
Property: property,
|
|
||||||
IpPrefix: ipAddress,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ips
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy
|
|
||||||
// Simply compare the host bits of the IP addresses.
|
|
||||||
// Use a binary tree to store the host bits.
|
|
||||||
func analyzeDuplicateAllowedIPs(p parser.WireguardParser) []protocol.Diagnostic {
|
|
||||||
diagnostics := make([]protocol.Diagnostic, 0)
|
|
||||||
|
|
||||||
maskedIPs := mapAllowedIPsToMasks(p)
|
|
||||||
hostBits := utils.KeysOfMap(maskedIPs)
|
|
||||||
slices.Sort(hostBits)
|
|
||||||
|
|
||||||
ipHostSet := utils.CreateIPv4HostSet()
|
|
||||||
|
|
||||||
for _, hostBit := range hostBits {
|
|
||||||
ips := maskedIPs[hostBit]
|
|
||||||
|
|
||||||
for _, ipInfo := range ips {
|
|
||||||
if ctx, _ := ipHostSet.ContainsIP(ipInfo.IpPrefix); ctx != nil {
|
|
||||||
severity := protocol.DiagnosticSeverityError
|
|
||||||
definedLine := (*ctx).Value("line").(uint32)
|
|
||||||
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine),
|
|
||||||
Severity: &severity,
|
|
||||||
Range: protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: ipInfo.Line,
|
|
||||||
Character: ipInfo.Property.Key.Location.Start,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: ipInfo.Line,
|
|
||||||
Character: ipInfo.Property.Value.Location.End,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
humanLineNumber := ipInfo.Line + 1
|
|
||||||
ctx := context.WithValue(context.Background(), "line", humanLineNumber)
|
|
||||||
|
|
||||||
ipHostSet.AddIP(
|
|
||||||
ipInfo.IpPrefix,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
||||||
|
|
||||||
func analyzeSymmetricPropertiesExist(
|
|
||||||
p parser.WireguardParser,
|
|
||||||
) []protocol.Diagnostic {
|
|
||||||
diagnostics := make([]protocol.Diagnostic, 0, 4)
|
|
||||||
severity := protocol.DiagnosticSeverityHint
|
|
||||||
|
|
||||||
for _, section := range p.GetSectionsByName("Interface") {
|
|
||||||
preUpLine, preUpProperty := section.FetchFirstProperty("PreUp")
|
|
||||||
preDownLine, preDownProperty := section.FetchFirstProperty("PreDown")
|
|
||||||
|
|
||||||
postUpLine, postUpProperty := section.FetchFirstProperty("PostUp")
|
|
||||||
postDownLine, postDownProperty := section.FetchFirstProperty("PostDown")
|
|
||||||
|
|
||||||
if preUpProperty != nil && preDownProperty == nil {
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: "PreUp is set, but PreDown is not. It is recommended to set both properties symmetrically",
|
|
||||||
Range: preUpProperty.GetLineRange(*preUpLine),
|
|
||||||
Severity: &severity,
|
|
||||||
})
|
|
||||||
} else if preUpProperty == nil && preDownProperty != nil {
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: "PreDown is set, but PreUp is not. It is recommended to set both properties symmetrically",
|
|
||||||
Range: preDownProperty.GetLineRange(*preDownLine),
|
|
||||||
Severity: &severity,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if postUpProperty != nil && postDownProperty == nil {
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: "PostUp is set, but PostDown is not. It is recommended to set both properties symmetrically",
|
|
||||||
Range: postUpProperty.GetLineRange(*postUpLine),
|
|
||||||
Severity: &severity,
|
|
||||||
})
|
|
||||||
} else if postUpProperty == nil && postDownProperty != nil {
|
|
||||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
|
||||||
Message: "PostDown is set, but PostUp is not. It is recommended to set both properties symmetrically",
|
|
||||||
Range: postDownProperty.GetLineRange(*postDownLine),
|
|
||||||
Severity: &severity,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diagnostics
|
|
||||||
}
|
|
@ -1,8 +1,10 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
wgcommands "config-lsp/handlers/wireguard/commands"
|
wgcommands "config-lsp/handlers/wireguard/commands"
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -11,11 +13,10 @@ type CodeActionName string
|
|||||||
const (
|
const (
|
||||||
CodeActionGeneratePrivateKey CodeActionName = "generatePrivateKey"
|
CodeActionGeneratePrivateKey CodeActionName = "generatePrivateKey"
|
||||||
CodeActionGeneratePresharedKey CodeActionName = "generatePresharedKey"
|
CodeActionGeneratePresharedKey CodeActionName = "generatePresharedKey"
|
||||||
CodeActionAddKeepalive CodeActionName = "addKeepalive"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CodeAction interface {
|
type CodeAction interface {
|
||||||
RunCommand(*parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error)
|
RunCommand(*ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeActionArgs interface{}
|
type CodeActionArgs interface{}
|
||||||
@ -32,14 +33,15 @@ func CodeActionGeneratePrivateKeyArgsFromArguments(arguments map[string]any) Cod
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (args CodeActionGeneratePrivateKeyArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) {
|
func (args CodeActionGeneratePrivateKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) {
|
||||||
privateKey, err := wgcommands.CreateNewPrivateKey()
|
privateKey, err := wgcommands.CreateNewPrivateKey()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &protocol.ApplyWorkspaceEditParams{}, err
|
return &protocol.ApplyWorkspaceEditParams{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
section, property := p.GetPropertyByLine(args.Line)
|
section := d.Config.FindSectionByLine(args.Line)
|
||||||
|
property := d.Config.FindPropertyByLine(args.Line)
|
||||||
|
|
||||||
if section == nil || property == nil {
|
if section == nil || property == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -53,7 +55,16 @@ func (args CodeActionGeneratePrivateKeyArgs) RunCommand(p *parser.WireguardParse
|
|||||||
args.URI: {
|
args.URI: {
|
||||||
{
|
{
|
||||||
NewText: " " + privateKey,
|
NewText: " " + privateKey,
|
||||||
Range: property.GetInsertRange(args.Line),
|
Range: protocol.Range{
|
||||||
|
Start: protocol.Position{
|
||||||
|
Line: property.End.Line,
|
||||||
|
Character: property.End.Character,
|
||||||
|
},
|
||||||
|
End: protocol.Position{
|
||||||
|
Line: property.End.Line,
|
||||||
|
Character: property.End.Character,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -73,14 +84,15 @@ func CodeActionGeneratePresharedKeyArgsFromArguments(arguments map[string]any) C
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (args CodeActionGeneratePresharedKeyArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) {
|
func (args CodeActionGeneratePresharedKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) {
|
||||||
presharedKey, err := wgcommands.CreatePresharedKey()
|
presharedKey, err := wgcommands.CreatePresharedKey()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &protocol.ApplyWorkspaceEditParams{}, err
|
return &protocol.ApplyWorkspaceEditParams{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
section, property := p.GetPropertyByLine(args.Line)
|
section := d.Config.FindSectionByLine(args.Line)
|
||||||
|
property := d.Config.FindPropertyByLine(args.Line)
|
||||||
|
|
||||||
if section == nil || property == nil {
|
if section == nil || property == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -94,45 +106,14 @@ func (args CodeActionGeneratePresharedKeyArgs) RunCommand(p *parser.WireguardPar
|
|||||||
args.URI: {
|
args.URI: {
|
||||||
{
|
{
|
||||||
NewText: " " + presharedKey,
|
NewText: " " + presharedKey,
|
||||||
Range: property.GetInsertRange(args.Line),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type CodeActionAddKeepaliveArgs struct {
|
|
||||||
URI protocol.DocumentUri
|
|
||||||
SectionIndex uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func CodeActionAddKeepaliveArgsFromArguments(arguments map[string]any) CodeActionAddKeepaliveArgs {
|
|
||||||
return CodeActionAddKeepaliveArgs{
|
|
||||||
URI: arguments["URI"].(protocol.DocumentUri),
|
|
||||||
SectionIndex: uint32(arguments["SectionIndex"].(float64)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (args CodeActionAddKeepaliveArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) {
|
|
||||||
section := p.Sections[args.SectionIndex]
|
|
||||||
|
|
||||||
label := "Add PersistentKeepalive"
|
|
||||||
return &protocol.ApplyWorkspaceEditParams{
|
|
||||||
Label: &label,
|
|
||||||
Edit: protocol.WorkspaceEdit{
|
|
||||||
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
|
|
||||||
args.URI: {
|
|
||||||
{
|
|
||||||
NewText: "PersistentKeepalive = 25\n",
|
|
||||||
Range: protocol.Range{
|
Range: protocol.Range{
|
||||||
Start: protocol.Position{
|
Start: protocol.Position{
|
||||||
Line: section.EndLine + 1,
|
Line: property.End.Line,
|
||||||
Character: 0,
|
Character: property.End.Character,
|
||||||
},
|
},
|
||||||
End: protocol.Position{
|
End: protocol.Position{
|
||||||
Line: section.EndLine + 1,
|
Line: property.End.Line,
|
||||||
Character: 0,
|
Character: property.End.Character,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,160 +1,34 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
docvalues "config-lsp/doc-values"
|
"config-lsp/handlers/wireguard"
|
||||||
"config-lsp/handlers/wireguard/fields"
|
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
"config-lsp/utils"
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
"maps"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getHeaderCompletion(name string, documentation string) protocol.CompletionItem {
|
func SuggestCompletions(
|
||||||
textFormat := protocol.InsertTextFormatPlainText
|
d *wireguard.WGDocument,
|
||||||
kind := protocol.CompletionItemKindEnum
|
params *protocol.CompletionParams,
|
||||||
|
|
||||||
insertText := "[" + name + "]\n"
|
|
||||||
|
|
||||||
return protocol.CompletionItem{
|
|
||||||
Label: "[" + name + "]",
|
|
||||||
InsertTextFormat: &textFormat,
|
|
||||||
InsertText: &insertText,
|
|
||||||
Kind: &kind,
|
|
||||||
Documentation: &documentation,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRootCompletionsForEmptyLine(
|
|
||||||
p parser.WireguardParser,
|
|
||||||
) ([]protocol.CompletionItem, error) {
|
) ([]protocol.CompletionItem, error) {
|
||||||
completions := make([]protocol.CompletionItem, 0)
|
lineNumber := params.Position.Line
|
||||||
|
|
||||||
if _, found := p.GetInterfaceSection(); !found {
|
if _, found := d.Config.CommentLines[lineNumber]; found {
|
||||||
completions = append(completions, getHeaderCompletion("Interface", fields.HeaderInterfaceEnum.Documentation))
|
|
||||||
}
|
|
||||||
|
|
||||||
completions = append(completions, getHeaderCompletion("Peer", fields.HeaderPeerEnum.Documentation))
|
|
||||||
|
|
||||||
return completions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCompletionsForSectionEmptyLine(
|
|
||||||
s parser.WireguardSection,
|
|
||||||
) ([]protocol.CompletionItem, error) {
|
|
||||||
if s.Name == nil {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
options := make(map[string]docvalues.DocumentationValue)
|
section := d.Config.FindSectionByLine(lineNumber)
|
||||||
|
property := d.Config.FindPropertyByLine(lineNumber)
|
||||||
|
|
||||||
switch *s.Name {
|
if section == nil {
|
||||||
case "Interface":
|
// First, the user needs to define a section header
|
||||||
maps.Copy(options, fields.InterfaceOptions)
|
if property == nil {
|
||||||
|
return GetSectionHeaderCompletions(d)
|
||||||
// Remove existing options
|
|
||||||
for _, property := range s.Properties {
|
|
||||||
if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(options, property.Key.Name)
|
|
||||||
}
|
|
||||||
case "Peer":
|
|
||||||
maps.Copy(options, fields.PeerOptions)
|
|
||||||
|
|
||||||
// Remove existing options
|
|
||||||
for _, property := range s.Properties {
|
|
||||||
if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(options, property.Key.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kind := protocol.CompletionItemKindProperty
|
|
||||||
|
|
||||||
return utils.MapMapToSlice(
|
|
||||||
options,
|
|
||||||
func(optionName string, value docvalues.DocumentationValue) protocol.CompletionItem {
|
|
||||||
insertText := optionName + " = "
|
|
||||||
|
|
||||||
return protocol.CompletionItem{
|
|
||||||
Kind: &kind,
|
|
||||||
Documentation: value.Documentation,
|
|
||||||
Label: optionName,
|
|
||||||
InsertText: &insertText,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSeparatorCompletion(property parser.WireguardProperty, character uint32) ([]protocol.CompletionItem, error) {
|
|
||||||
var insertText string
|
|
||||||
|
|
||||||
if character == property.Key.Location.End {
|
|
||||||
insertText = property.Key.Name + " = "
|
|
||||||
} else {
|
|
||||||
insertText = "= "
|
|
||||||
}
|
|
||||||
|
|
||||||
kind := protocol.CompletionItemKindValue
|
|
||||||
|
|
||||||
return []protocol.CompletionItem{
|
|
||||||
{
|
|
||||||
Label: insertText,
|
|
||||||
InsertText: &insertText,
|
|
||||||
Kind: &kind,
|
|
||||||
},
|
|
||||||
}, parser.PropertyNotFullyTypedError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCompletionsForSectionPropertyLine(
|
|
||||||
s parser.WireguardSection,
|
|
||||||
lineNumber uint32,
|
|
||||||
character uint32,
|
|
||||||
) ([]protocol.CompletionItem, error) {
|
|
||||||
property, err := s.GetPropertyByLine(lineNumber)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Name == nil {
|
|
||||||
return nil, parser.PropertyNotFoundError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
options, found := fields.OptionsHeaderMap[*s.Name]
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return nil, parser.PropertyNotFoundError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if property.Separator == nil {
|
|
||||||
if _, found := options[property.Key.Name]; found {
|
|
||||||
return GetSeparatorCompletion(*property, character)
|
|
||||||
}
|
|
||||||
// Get empty line completions
|
|
||||||
return nil, parser.PropertyNotFullyTypedError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
option, found := options[property.Key.Name]
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
if character < property.Separator.Location.Start {
|
|
||||||
return nil, parser.PropertyNotFullyTypedError{}
|
|
||||||
} else {
|
} else {
|
||||||
return nil, parser.PropertyNotFoundError{}
|
// However, if they start typing a property - we should not
|
||||||
|
// show anything to signal them that they can't write a property yet.
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return GetSectionBodyCompletions(d, *section, property, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
if property.Value == nil {
|
|
||||||
if character >= property.Separator.Location.End {
|
|
||||||
return option.DeprecatedFetchCompletions("", 0), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relativeCursor := character - property.Value.Location.Start
|
|
||||||
|
|
||||||
return option.DeprecatedFetchCompletions(property.Value.Value, relativeCursor), nil
|
|
||||||
}
|
}
|
||||||
|
209
server/handlers/wireguard/handlers/completions_body.go
Normal file
209
server/handlers/wireguard/handlers/completions_body.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
docvalues "config-lsp/doc-values"
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
|
"config-lsp/handlers/wireguard/fields"
|
||||||
|
"config-lsp/utils"
|
||||||
|
"maps"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSectionBodyCompletions(
|
||||||
|
d *wireguard.WGDocument,
|
||||||
|
section ast.WGSection,
|
||||||
|
property *ast.WGProperty,
|
||||||
|
params *protocol.CompletionParams,
|
||||||
|
) ([]protocol.CompletionItem, error) {
|
||||||
|
// These are the possible scenarios:
|
||||||
|
// | = Cursor position
|
||||||
|
/*
|
||||||
|
[Inter|
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
[Interface]
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
[Interface]
|
||||||
|
Add|
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
[Interface]
|
||||||
|
Address = 10.0.0.1/24
|
||||||
|
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// First scenario, user is typing the section name
|
||||||
|
if params.Position.Line == section.Start.Line {
|
||||||
|
return GetSectionHeaderCompletions(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third and fourth scenarios, the user wants to add a new property
|
||||||
|
completions, err := getPropertyCompletions(d, section, property, params)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Something weird happened
|
||||||
|
return completions, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fourth scenario may arrive here, the user is typing a property name, but the previous line is empty.
|
||||||
|
// In this case, the user may want to add a property or add a new section.
|
||||||
|
// We should therefore suggest both options.
|
||||||
|
|
||||||
|
isLineEmpty := property == nil
|
||||||
|
if !isLineEmpty {
|
||||||
|
return completions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if previous line is empty
|
||||||
|
previousLineProperty := d.Config.FindPropertyByLine(params.Position.Line - 1)
|
||||||
|
|
||||||
|
if previousLineProperty == nil && params.Position.Line-1 != section.Start.Line {
|
||||||
|
sectionCompletions, err := GetSectionHeaderCompletions(d)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return sectionCompletions, err
|
||||||
|
}
|
||||||
|
|
||||||
|
completions = append(completions, sectionCompletions...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return completions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPropertyCompletions(
|
||||||
|
d *wireguard.WGDocument,
|
||||||
|
section ast.WGSection,
|
||||||
|
property *ast.WGProperty,
|
||||||
|
params *protocol.CompletionParams,
|
||||||
|
) ([]protocol.CompletionItem, error) {
|
||||||
|
// These are the possible scenarios:
|
||||||
|
/* Empty line / Key started / Separator missing:
|
||||||
|
Add|
|
||||||
|
Address |
|
||||||
|
*/
|
||||||
|
/* Value missing or started:
|
||||||
|
Address = 10.|
|
||||||
|
*/
|
||||||
|
|
||||||
|
currentLine := params.Position.Line
|
||||||
|
position := common.LSPCharacterAsCursorPosition(params.Position.Character)
|
||||||
|
|
||||||
|
// Special case, key defined but separator missing
|
||||||
|
if property != nil && property.Separator == nil && !property.Key.ContainsPosition(position) {
|
||||||
|
return getKeyCompletions(section, true, currentLine), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if property == nil || property.Separator == nil || property.Key.ContainsPosition(position) {
|
||||||
|
// First scenario
|
||||||
|
return getKeyCompletions(section, false, currentLine), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the cursor it outside the value
|
||||||
|
if property.Value != nil && property.Value.IsPositionAfterEnd(position) {
|
||||||
|
// Then we don't show anything
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, suggest value completions
|
||||||
|
return getValueCompletions(section, *property, position), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyCompletions(
|
||||||
|
section ast.WGSection,
|
||||||
|
onlySeparator bool,
|
||||||
|
currentLine uint32,
|
||||||
|
) []protocol.CompletionItem {
|
||||||
|
options := make(map[fields.NormalizedName]docvalues.DocumentationValue)
|
||||||
|
allowedDuplicatedFields := make(map[fields.NormalizedName]struct{})
|
||||||
|
|
||||||
|
switch section.Header.Name {
|
||||||
|
case "Interface":
|
||||||
|
maps.Copy(options, fields.InterfaceOptions)
|
||||||
|
allowedDuplicatedFields = fields.InterfaceAllowedDuplicateFields
|
||||||
|
case "Peer":
|
||||||
|
maps.Copy(options, fields.PeerOptions)
|
||||||
|
allowedDuplicatedFields = fields.PeerAllowedDuplicateFields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove existing, non-duplicate options
|
||||||
|
it := section.Properties.Iterator()
|
||||||
|
for it.Next() {
|
||||||
|
property := it.Value().(*ast.WGProperty)
|
||||||
|
normalizedName := fields.CreateNormalizedName(property.Key.Name)
|
||||||
|
if _, found := allowedDuplicatedFields[normalizedName]; found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if property.Key.Start.Line == currentLine {
|
||||||
|
// The user is currently typing the key, thus we should suggest it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(options, normalizedName)
|
||||||
|
}
|
||||||
|
|
||||||
|
kind := protocol.CompletionItemKindField
|
||||||
|
|
||||||
|
return utils.MapMapToSlice(
|
||||||
|
options,
|
||||||
|
func(rawOptionName fields.NormalizedName, value docvalues.DocumentationValue) protocol.CompletionItem {
|
||||||
|
optionName := fields.AllOptionsFormatted[rawOptionName]
|
||||||
|
var label string
|
||||||
|
var insertText string
|
||||||
|
|
||||||
|
if onlySeparator {
|
||||||
|
label = optionName + " = "
|
||||||
|
insertText = "= "
|
||||||
|
} else {
|
||||||
|
label = optionName
|
||||||
|
insertText = optionName + " = "
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocol.CompletionItem{
|
||||||
|
Kind: &kind,
|
||||||
|
Documentation: value.Documentation,
|
||||||
|
Label: label,
|
||||||
|
InsertText: &insertText,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getValueCompletions(
|
||||||
|
section ast.WGSection,
|
||||||
|
property ast.WGProperty,
|
||||||
|
cursorPosition common.CursorPosition,
|
||||||
|
) []protocol.CompletionItem {
|
||||||
|
// TODO: Normalize section header name
|
||||||
|
normalizedHeaderName := fields.CreateNormalizedName(section.Header.Name)
|
||||||
|
options, found := fields.OptionsHeaderMap[normalizedHeaderName]
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
option, found := options[fields.CreateNormalizedName(property.Key.Name)]
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if property.Value == nil {
|
||||||
|
return option.DeprecatedFetchCompletions("", 0)
|
||||||
|
} else {
|
||||||
|
return option.DeprecatedFetchCompletions(
|
||||||
|
property.Value.Value,
|
||||||
|
common.DeprecatedImprovedCursorToIndex(
|
||||||
|
cursorPosition,
|
||||||
|
property.Value.Value,
|
||||||
|
property.Value.Start.Character,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
46
server/handlers/wireguard/handlers/completions_header.go
Normal file
46
server/handlers/wireguard/handlers/completions_header.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/fields"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getHeaderCompletion(name string, documentation string) protocol.CompletionItem {
|
||||||
|
textFormat := protocol.InsertTextFormatPlainText
|
||||||
|
kind := protocol.CompletionItemKindEnum
|
||||||
|
|
||||||
|
insertText := "[" + name + "]\n"
|
||||||
|
|
||||||
|
return protocol.CompletionItem{
|
||||||
|
Label: "[" + name + "]",
|
||||||
|
InsertTextFormat: &textFormat,
|
||||||
|
InsertText: &insertText,
|
||||||
|
Kind: &kind,
|
||||||
|
Documentation: &documentation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSectionHeaderCompletions(
|
||||||
|
d *wireguard.WGDocument,
|
||||||
|
) ([]protocol.CompletionItem, error) {
|
||||||
|
completions := make([]protocol.CompletionItem, 0)
|
||||||
|
|
||||||
|
containsInterfaceSection := false
|
||||||
|
|
||||||
|
for _, section := range d.Config.Sections {
|
||||||
|
if section.Header.Name == "Interface" {
|
||||||
|
containsInterfaceSection = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !containsInterfaceSection {
|
||||||
|
completions = append(completions, getHeaderCompletion("Interface", fields.HeaderInterfaceEnum.Documentation))
|
||||||
|
}
|
||||||
|
|
||||||
|
completions = append(completions, getHeaderCompletion("Peer", fields.HeaderPeerEnum.Documentation))
|
||||||
|
|
||||||
|
return completions, nil
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
"config-lsp/handlers/wireguard/fields"
|
"config-lsp/handlers/wireguard/fields"
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
"config-lsp/utils"
|
"config-lsp/utils"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSimplePropertyInInterface(
|
func TestSimplePropertyInInterface(
|
||||||
@ -14,17 +17,33 @@ func TestSimplePropertyInInterface(
|
|||||||
[Interface]
|
[Interface]
|
||||||
|
|
||||||
`)
|
`)
|
||||||
p := parser.CreateWireguardParser()
|
p := ast.NewWGConfig()
|
||||||
p.ParseFromString(sample)
|
parseErrors := p.Parse(sample)
|
||||||
|
|
||||||
completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0])
|
if len(parseErrors) > 0 {
|
||||||
|
t.Fatalf("Parser failed with error %v", parseErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &wireguard.WGDocument{
|
||||||
|
Config: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &protocol.CompletionParams{
|
||||||
|
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||||
|
Position: protocol.Position{
|
||||||
|
Line: 1,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
completions, err := SuggestCompletions(d, params)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
t.Errorf("getCompletionsForEmptyLine failed with error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(completions) != len(fields.InterfaceOptions) {
|
if len(completions) != len(fields.InterfaceOptions) {
|
||||||
t.Fatalf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions))
|
t.Errorf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,10 +55,26 @@ func TestSimpleOneExistingPropertyInInterface(
|
|||||||
PrivateKey = 1234567890
|
PrivateKey = 1234567890
|
||||||
|
|
||||||
`)
|
`)
|
||||||
p := parser.CreateWireguardParser()
|
p := ast.NewWGConfig()
|
||||||
p.ParseFromString(sample)
|
parseErrors := p.Parse(sample)
|
||||||
|
|
||||||
completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0])
|
if len(parseErrors) > 0 {
|
||||||
|
t.Fatalf("Parser failed with error %v", parseErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &wireguard.WGDocument{
|
||||||
|
Config: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &protocol.CompletionParams{
|
||||||
|
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||||
|
Position: protocol.Position{
|
||||||
|
Line: 2,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
completions, err := SuggestCompletions(d, params)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
||||||
@ -51,124 +86,151 @@ PrivateKey = 1234567890
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmptyRootCompletionsWork(
|
func TestEmptyCompletions(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
sample := utils.Dedent(`
|
sample := utils.Dedent(`
|
||||||
`)
|
|
||||||
|
|
||||||
p := parser.CreateWireguardParser()
|
`)
|
||||||
p.ParseFromString(sample)
|
p := ast.NewWGConfig()
|
||||||
|
parseErrors := p.Parse(sample)
|
||||||
|
|
||||||
completions, _ := GetRootCompletionsForEmptyLine(p)
|
if len(parseErrors) > 0 {
|
||||||
|
t.Fatalf("Parser failed with error %v", parseErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &wireguard.WGDocument{
|
||||||
|
Config: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &protocol.CompletionParams{
|
||||||
|
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||||
|
Position: protocol.Position{
|
||||||
|
Line: 0,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
completions, err := SuggestCompletions(d, params)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if len(completions) != 2 {
|
if len(completions) != 2 {
|
||||||
t.Fatalf("getRootCompletionsForEmptyLine: Expected 2 completions, but got %v", len(completions))
|
t.Fatalf("getRootCompletionsForEmptyLine: Expected 2 completions, but got %v", len(completions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInterfaceSectionRootCompletionsBeforeWork(
|
func TestIncompletePropertyCompletions(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
) {
|
) {
|
||||||
sample := utils.Dedent(`
|
sample := utils.Dedent(`
|
||||||
|
|
||||||
[Interface]
|
|
||||||
`)
|
|
||||||
p := parser.CreateWireguardParser()
|
|
||||||
p.ParseFromString(sample)
|
|
||||||
|
|
||||||
completions, _ := GetRootCompletionsForEmptyLine(p)
|
|
||||||
|
|
||||||
if len(completions) != 1 {
|
|
||||||
t.Fatalf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInterfaceAndPeerSectionRootCompletionsWork(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
|
|
||||||
[Peer]
|
[Peer]
|
||||||
|
Add
|
||||||
`)
|
`)
|
||||||
p := parser.CreateWireguardParser()
|
p := ast.NewWGConfig()
|
||||||
p.ParseFromString(sample)
|
parseErrors := p.Parse(sample)
|
||||||
|
|
||||||
completions, _ := GetRootCompletionsForEmptyLine(p)
|
if len(parseErrors) > 0 {
|
||||||
|
t.Fatalf("Parser failed with error %v", parseErrors)
|
||||||
if len(completions) != 1 {
|
|
||||||
t.Fatalf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPropertyNoSepatorShouldCompleteSeparator(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
DNS
|
|
||||||
`)
|
|
||||||
p := parser.CreateWireguardParser()
|
|
||||||
p.ParseFromString(sample)
|
|
||||||
|
|
||||||
completions, err := GetCompletionsForSectionPropertyLine(*p.Sections[0], 1, 3)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("getCompletionsForPropertyLine err is nil but should not be")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(completions) != 1 {
|
d := &wireguard.WGDocument{
|
||||||
t.Fatalf("getCompletionsForPropertyLine: Expected 1 completion, but got %v", len(completions))
|
Config: p,
|
||||||
}
|
}
|
||||||
|
|
||||||
if *completions[0].InsertText != "DNS = " {
|
params := &protocol.CompletionParams{
|
||||||
t.Fatalf("getCompletionsForPropertyLine: Expected completion to be 'DNS = ', but got '%v'", completions[0].Label)
|
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||||
|
Position: protocol.Position{
|
||||||
|
Line: 1,
|
||||||
|
Character: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
completions, err := SuggestCompletions(d, params)
|
||||||
|
|
||||||
func TestPropertyNoSeparatorWithSpaceShouldCompleteSeparator(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
DNS
|
|
||||||
`)
|
|
||||||
p := parser.CreateWireguardParser()
|
|
||||||
p.ParseFromString(sample)
|
|
||||||
|
|
||||||
completions, err := GetCompletionsForSectionPropertyLine(*p.Sections[0], 1, 4)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("getCompletionsForPropertyLine err is nil but should not be")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(completions) != 1 {
|
|
||||||
t.Fatalf("getCompletionsForPropertyLine: Expected 1 completion, but got %v", len(completions))
|
|
||||||
}
|
|
||||||
|
|
||||||
if *completions[0].InsertText != "= " {
|
|
||||||
t.Fatalf("getCompletionsForPropertyLine: Expected completion to be '= ', but got '%v'", completions[0].Label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHeaderButNoProperty(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
|
|
||||||
`)
|
|
||||||
p := parser.CreateWireguardParser()
|
|
||||||
p.ParseFromString(sample)
|
|
||||||
|
|
||||||
completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0])
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err)
|
t.Errorf("getCompletionsForEmptyLine failed with error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(completions) != len(fields.InterfaceOptions) {
|
if len(completions) != len(fields.PeerOptions) {
|
||||||
t.Fatalf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions))
|
t.Errorf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyBeforeLineIsEmpty(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
sample := utils.Dedent(`
|
||||||
|
[Interface]
|
||||||
|
DNS = 1.1.1.1
|
||||||
|
|
||||||
|
|
||||||
|
`)
|
||||||
|
p := ast.NewWGConfig()
|
||||||
|
parseErrors := p.Parse(sample)
|
||||||
|
|
||||||
|
if len(parseErrors) > 0 {
|
||||||
|
t.Fatalf("Parser failed with error %v", parseErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &wireguard.WGDocument{
|
||||||
|
Config: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &protocol.CompletionParams{
|
||||||
|
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||||
|
Position: protocol.Position{
|
||||||
|
Line: 3,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
completions, err := SuggestCompletions(d, params)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("getCompletionsForPropertyLine failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(completions) != len(fields.InterfaceOptions)+1-1 {
|
||||||
|
t.Errorf("getCompletionsForPropertyLine: Expected completions, but got %v", len(completions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropertyValueCompletions(
|
||||||
|
t *testing.T,
|
||||||
|
) {
|
||||||
|
sample := utils.Dedent(`
|
||||||
|
[Interface]
|
||||||
|
Table =
|
||||||
|
`)
|
||||||
|
p := ast.NewWGConfig()
|
||||||
|
parseErrors := p.Parse(sample)
|
||||||
|
|
||||||
|
if len(parseErrors) > 0 {
|
||||||
|
t.Fatalf("Parser failed with error %v", parseErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
d := &wireguard.WGDocument{
|
||||||
|
Config: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
params := &protocol.CompletionParams{
|
||||||
|
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
|
||||||
|
Position: protocol.Position{
|
||||||
|
Line: 1,
|
||||||
|
Character: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
completions, err := SuggestCompletions(d, params)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("SuggestComplete failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(len(completions) == 2) {
|
||||||
|
t.Errorf("Expected 2 completions, but got %v", len(completions))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,52 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
"config-lsp/handlers/wireguard/commands"
|
"config-lsp/handlers/wireguard/commands"
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetKeepaliveCodeActions(
|
func GetKeepaliveCodeActions(
|
||||||
p *parser.WireguardParser,
|
d *wireguard.WGDocument,
|
||||||
params *protocol.CodeActionParams,
|
params *protocol.CodeActionParams,
|
||||||
) []protocol.CodeAction {
|
) []protocol.CodeAction {
|
||||||
line := params.Range.Start.Line
|
line := params.Range.Start.Line
|
||||||
|
|
||||||
for index, section := range p.Sections {
|
for _, section := range d.Indexes.SectionsByName["Peer"] {
|
||||||
if section.StartLine >= line && line <= section.EndLine && section.Name != nil && *section.Name == "Peer" {
|
if section.Start.Line >= line && line <= section.End.Line {
|
||||||
if section.ExistsProperty("Endpoint") && !section.ExistsProperty("PersistentKeepalive") {
|
if section.FindPropertyByName("Endpoint") != nil && section.FindFirstPropertyByName("PersistentKeepalive") == nil {
|
||||||
commandID := "wireguard." + CodeActionAddKeepalive
|
var insertionLine uint32
|
||||||
command := protocol.Command{
|
lastProperty := section.GetLastProperty()
|
||||||
Title: "Add PersistentKeepalive",
|
|
||||||
Command: string(commandID),
|
if lastProperty == nil {
|
||||||
Arguments: []any{
|
insertionLine = section.End.Line
|
||||||
CodeActionAddKeepaliveArgs{
|
} else {
|
||||||
URI: params.TextDocument.URI,
|
insertionLine = lastProperty.End.Line + 1
|
||||||
SectionIndex: uint32(index),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return []protocol.CodeAction{
|
return []protocol.CodeAction{
|
||||||
{
|
{
|
||||||
Title: "Add PersistentKeepalive",
|
Title: "Add PersistentKeepalive",
|
||||||
Command: &command,
|
Edit: &protocol.WorkspaceEdit{
|
||||||
|
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
|
||||||
|
params.TextDocument.URI: {
|
||||||
|
{
|
||||||
|
Range: protocol.Range{
|
||||||
|
Start: protocol.Position{
|
||||||
|
Line: insertionLine,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
End: protocol.Position{
|
||||||
|
Line: insertionLine,
|
||||||
|
Character: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewText: "PersistentKeepalive = 25\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,11 +57,17 @@ func GetKeepaliveCodeActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetKeyGenerationCodeActions(
|
func GetKeyGenerationCodeActions(
|
||||||
p *parser.WireguardParser,
|
d *wireguard.WGDocument,
|
||||||
params *protocol.CodeActionParams,
|
params *protocol.CodeActionParams,
|
||||||
) []protocol.CodeAction {
|
) []protocol.CodeAction {
|
||||||
|
if !wgcommands.AreWireguardToolsAvailable() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
line := params.Range.Start.Line
|
line := params.Range.Start.Line
|
||||||
section, property := p.GetPropertyByLine(line)
|
|
||||||
|
section := d.Config.FindSectionByLine(line)
|
||||||
|
property := d.Config.FindPropertyByLine(line)
|
||||||
|
|
||||||
if section == nil || property == nil || property.Separator == nil {
|
if section == nil || property == nil || property.Separator == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -53,10 +75,6 @@ func GetKeyGenerationCodeActions(
|
|||||||
|
|
||||||
switch property.Key.Name {
|
switch property.Key.Name {
|
||||||
case "PrivateKey":
|
case "PrivateKey":
|
||||||
if !wgcommands.AreWireguardToolsAvailable() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
commandID := "wireguard." + CodeActionGeneratePrivateKey
|
commandID := "wireguard." + CodeActionGeneratePrivateKey
|
||||||
command := protocol.Command{
|
command := protocol.Command{
|
||||||
Title: "Generate Private Key",
|
Title: "Generate Private Key",
|
||||||
|
@ -1,105 +1,81 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
docvalues "config-lsp/doc-values"
|
docvalues "config-lsp/doc-values"
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
"config-lsp/handlers/wireguard/fields"
|
"config-lsp/handlers/wireguard/fields"
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPropertyInfo(
|
func GetPropertyHoverInfo(
|
||||||
p parser.WireguardProperty,
|
d *wireguard.WGDocument,
|
||||||
cursor uint32,
|
section ast.WGSection,
|
||||||
section parser.WireguardSection,
|
property ast.WGProperty,
|
||||||
) []string {
|
index common.IndexPosition,
|
||||||
if cursor <= p.Key.Location.End {
|
) (*protocol.Hover, error) {
|
||||||
options, found := fields.OptionsHeaderMap[*section.Name]
|
availableOptions, found := fields.OptionsHeaderMap[fields.CreateNormalizedName(section.Header.Name)]
|
||||||
|
|
||||||
if !found {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
option, found := options[p.Key.Name]
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Split(option.Documentation, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
options, found := fields.OptionsHeaderMap[*section.Name]
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return []string{}
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if option, found := options[p.Key.Name]; found {
|
option, found := availableOptions[fields.CreateNormalizedName(property.Key.Name)]
|
||||||
return option.GetTypeDescription()
|
|
||||||
|
if !found {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{}
|
if property.Key.ContainsPosition(index) {
|
||||||
|
return &protocol.Hover{
|
||||||
|
Contents: protocol.MarkupContent{
|
||||||
|
Kind: protocol.MarkupKindMarkdown,
|
||||||
|
Value: option.Documentation,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if property.Value != nil && property.Value.ContainsPosition(index) {
|
||||||
|
return &protocol.Hover{
|
||||||
|
Contents: protocol.MarkupContent{
|
||||||
|
Kind: protocol.MarkupKindMarkdown,
|
||||||
|
Value: strings.Join(option.GetTypeDescription(), "\n"),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSectionInfo(s parser.WireguardSection) []string {
|
func GetSectionHoverInfo(
|
||||||
if s.Name == nil {
|
d *wireguard.WGDocument,
|
||||||
return []string{}
|
section ast.WGSection,
|
||||||
}
|
) (*protocol.Hover, error) {
|
||||||
|
var docValue *docvalues.EnumString = nil
|
||||||
|
|
||||||
contents := []string{
|
switch section.Header.Name {
|
||||||
fmt.Sprintf("## [%s]", *s.Name),
|
|
||||||
"",
|
|
||||||
}
|
|
||||||
|
|
||||||
var option *docvalues.EnumString = nil
|
|
||||||
|
|
||||||
switch *s.Name {
|
|
||||||
case "Interface":
|
case "Interface":
|
||||||
option = &fields.HeaderInterfaceEnum
|
docValue = &fields.HeaderInterfaceEnum
|
||||||
case "Peer":
|
case "Peer":
|
||||||
option = &fields.HeaderPeerEnum
|
docValue = &fields.HeaderPeerEnum
|
||||||
}
|
}
|
||||||
|
|
||||||
if option == nil {
|
if docValue == nil {
|
||||||
return contents
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
contents = append(contents, strings.Split(option.Documentation, "\n")...)
|
return &protocol.Hover{
|
||||||
|
Contents: protocol.MarkupContent{
|
||||||
return contents
|
Kind: protocol.MarkupKindMarkdown,
|
||||||
}
|
Value: fmt.Sprintf(
|
||||||
|
"## [%s]\n\n%s",
|
||||||
func GetHoverContent(
|
section.Header.Name,
|
||||||
p parser.WireguardParser,
|
docValue.Documentation,
|
||||||
line uint32,
|
),
|
||||||
cursor uint32,
|
},
|
||||||
) []string {
|
}, nil
|
||||||
section := p.GetSectionByLine(line)
|
|
||||||
|
|
||||||
if section == nil {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
sectionInfo := getSectionInfo(*section)
|
|
||||||
|
|
||||||
property, _ := section.GetPropertyByLine(line)
|
|
||||||
|
|
||||||
if property == nil {
|
|
||||||
return sectionInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
propertyInfo := getPropertyInfo(*property, cursor, *section)
|
|
||||||
|
|
||||||
if len(propertyInfo) == 0 {
|
|
||||||
return sectionInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
contents := append(sectionInfo, []string{
|
|
||||||
"",
|
|
||||||
fmt.Sprintf("### %s", property.Key.Name),
|
|
||||||
}...)
|
|
||||||
contents = append(contents, propertyInfo...)
|
|
||||||
|
|
||||||
return contents
|
|
||||||
}
|
}
|
||||||
|
8
server/handlers/wireguard/indexes/indexes.go
Normal file
8
server/handlers/wireguard/indexes/indexes.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package indexes
|
||||||
|
|
||||||
|
import "config-lsp/handlers/wireguard/ast"
|
||||||
|
|
||||||
|
type WGIndexes struct {
|
||||||
|
// map of: section name -> WGSection
|
||||||
|
SectionsByName map[string][]*ast.WGSection
|
||||||
|
}
|
22
server/handlers/wireguard/indexes/indexes_handlers.go
Normal file
22
server/handlers/wireguard/indexes/indexes_handlers.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package indexes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateIndexes(config *ast.WGConfig) (*WGIndexes, []common.LSPError) {
|
||||||
|
errs := make([]common.LSPError, 0)
|
||||||
|
indexes := &WGIndexes{
|
||||||
|
SectionsByName: make(map[string][]*ast.WGSection),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, section := range config.Sections {
|
||||||
|
indexes.SectionsByName[section.Header.Name] = append(
|
||||||
|
indexes.SectionsByName[section.Header.Name],
|
||||||
|
section,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexes, errs
|
||||||
|
}
|
@ -1,8 +0,0 @@
|
|||||||
package lsp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
|
||||||
)
|
|
||||||
|
|
||||||
var documentParserMap = map[protocol.DocumentUri]*parser.WireguardParser{}
|
|
@ -1,18 +1,20 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
"config-lsp/handlers/wireguard/handlers"
|
"config-lsp/handlers/wireguard/handlers"
|
||||||
|
|
||||||
"github.com/tliron/glsp"
|
"github.com/tliron/glsp"
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
|
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
|
||||||
p := documentParserMap[params.TextDocument.URI]
|
d := wireguard.DocumentParserMap[params.TextDocument.URI]
|
||||||
|
|
||||||
actions := make([]protocol.CodeAction, 0, 2)
|
actions := make([]protocol.CodeAction, 0, 2)
|
||||||
|
|
||||||
actions = append(actions, handlers.GetKeyGenerationCodeActions(p, params)...)
|
actions = append(actions, handlers.GetKeyGenerationCodeActions(d, params)...)
|
||||||
actions = append(actions, handlers.GetKeepaliveCodeActions(p, params)...)
|
actions = append(actions, handlers.GetKeepaliveCodeActions(d, params)...)
|
||||||
|
|
||||||
if len(actions) > 0 {
|
if len(actions) > 0 {
|
||||||
return actions, nil
|
return actions, nil
|
||||||
|
@ -1,60 +1,15 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
"config-lsp/handlers/wireguard/handlers"
|
"config-lsp/handlers/wireguard/handlers"
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
"github.com/tliron/glsp"
|
"github.com/tliron/glsp"
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) {
|
func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) {
|
||||||
p := documentParserMap[params.TextDocument.URI]
|
d := wireguard.DocumentParserMap[params.TextDocument.URI]
|
||||||
|
|
||||||
lineNumber := params.Position.Line
|
return handlers.SuggestCompletions(d, params)
|
||||||
|
|
||||||
section := p.GetBelongingSectionByLine(lineNumber)
|
|
||||||
lineType := p.GetTypeByLine(lineNumber)
|
|
||||||
|
|
||||||
switch lineType {
|
|
||||||
case parser.LineTypeComment:
|
|
||||||
return nil, nil
|
|
||||||
case parser.LineTypeHeader:
|
|
||||||
return handlers.GetRootCompletionsForEmptyLine(*p)
|
|
||||||
case parser.LineTypeEmpty:
|
|
||||||
if section.Name == nil {
|
|
||||||
// Root completions
|
|
||||||
return handlers.GetRootCompletionsForEmptyLine(*p)
|
|
||||||
}
|
|
||||||
|
|
||||||
completions, err := handlers.GetCompletionsForSectionEmptyLine(*section)
|
|
||||||
|
|
||||||
// === Smart rules ===
|
|
||||||
|
|
||||||
// If previous line is empty too, maybe new section?
|
|
||||||
if lineNumber >= 1 && p.GetTypeByLine(lineNumber-1) == parser.LineTypeEmpty && len(p.GetBelongingSectionByLine(lineNumber).Properties) > 0 {
|
|
||||||
rootCompletions, err := handlers.GetRootCompletionsForEmptyLine(*p)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
completions = append(completions, rootCompletions...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return completions, err
|
|
||||||
case parser.LineTypeProperty:
|
|
||||||
completions, err := handlers.GetCompletionsForSectionPropertyLine(*section, lineNumber, params.Position.Character)
|
|
||||||
|
|
||||||
if completions == nil && err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
// Ignore
|
|
||||||
case parser.PropertyNotFullyTypedError:
|
|
||||||
return handlers.GetCompletionsForSectionEmptyLine(*section)
|
|
||||||
default:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return completions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
panic("TextDocumentCompletion: unexpected line type")
|
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,10 @@ package lsp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"config-lsp/common"
|
"config-lsp/common"
|
||||||
"config-lsp/handlers/wireguard/handlers"
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/analyzer"
|
||||||
"config-lsp/utils"
|
"config-lsp/utils"
|
||||||
|
|
||||||
"github.com/tliron/glsp"
|
"github.com/tliron/glsp"
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
@ -15,22 +17,22 @@ func TextDocumentDidChange(
|
|||||||
content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text
|
content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text
|
||||||
common.ClearDiagnostics(context, params.TextDocument.URI)
|
common.ClearDiagnostics(context, params.TextDocument.URI)
|
||||||
|
|
||||||
p := documentParserMap[params.TextDocument.URI]
|
document := wireguard.DocumentParserMap[params.TextDocument.URI]
|
||||||
p.Clear()
|
document.Config.Clear()
|
||||||
|
|
||||||
diagnostics := make([]protocol.Diagnostic, 0)
|
diagnostics := make([]protocol.Diagnostic, 0)
|
||||||
errors := p.ParseFromString(content)
|
errors := document.Config.Parse(content)
|
||||||
|
|
||||||
if len(errors) > 0 {
|
if len(errors) > 0 {
|
||||||
diagnostics = append(diagnostics, utils.Map(
|
diagnostics = append(diagnostics, utils.Map(
|
||||||
errors,
|
errors,
|
||||||
func(err common.ParseError) protocol.Diagnostic {
|
func(err common.LSPError) protocol.Diagnostic {
|
||||||
return err.ToDiagnostic()
|
return err.ToDiagnostic()
|
||||||
},
|
},
|
||||||
)...)
|
)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
diagnostics = append(diagnostics, handlers.Analyze(*p)...)
|
diagnostics = append(diagnostics, analyzer.Analyze(document)...)
|
||||||
|
|
||||||
if len(diagnostics) > 0 {
|
if len(diagnostics) > 0 {
|
||||||
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
|
|
||||||
"github.com/tliron/glsp"
|
"github.com/tliron/glsp"
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TextDocumentDidClose(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
|
func TextDocumentDidClose(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
|
||||||
delete(documentParserMap, params.TextDocument.URI)
|
delete(wireguard.DocumentParserMap, params.TextDocument.URI)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,12 @@ package lsp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"config-lsp/common"
|
"config-lsp/common"
|
||||||
"config-lsp/handlers/wireguard/parser"
|
"config-lsp/handlers/wireguard"
|
||||||
|
"config-lsp/handlers/wireguard/analyzer"
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
|
"config-lsp/handlers/wireguard/indexes"
|
||||||
"config-lsp/utils"
|
"config-lsp/utils"
|
||||||
|
|
||||||
"github.com/tliron/glsp"
|
"github.com/tliron/glsp"
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
)
|
)
|
||||||
@ -14,17 +18,21 @@ func TextDocumentDidOpen(
|
|||||||
) error {
|
) error {
|
||||||
common.ClearDiagnostics(context, params.TextDocument.URI)
|
common.ClearDiagnostics(context, params.TextDocument.URI)
|
||||||
|
|
||||||
p := parser.CreateWireguardParser()
|
document := &wireguard.WGDocument{
|
||||||
documentParserMap[params.TextDocument.URI] = &p
|
Config: ast.NewWGConfig(),
|
||||||
|
Indexes: &indexes.WGIndexes{},
|
||||||
|
}
|
||||||
|
wireguard.DocumentParserMap[params.TextDocument.URI] = document
|
||||||
|
|
||||||
errors := p.ParseFromString(params.TextDocument.Text)
|
errors := document.Config.Parse(params.TextDocument.Text)
|
||||||
|
|
||||||
diagnostics := utils.Map(
|
diagnostics := utils.Map(
|
||||||
errors,
|
errors,
|
||||||
func(err common.ParseError) protocol.Diagnostic {
|
func(err common.LSPError) protocol.Diagnostic {
|
||||||
return err.ToDiagnostic()
|
return err.ToDiagnostic()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
diagnostics = append(diagnostics, analyzer.Analyze(document)...)
|
||||||
|
|
||||||
if len(diagnostics) > 0 {
|
if len(diagnostics) > 0 {
|
||||||
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/common"
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
"config-lsp/handlers/wireguard/handlers"
|
"config-lsp/handlers/wireguard/handlers"
|
||||||
"config-lsp/handlers/wireguard/parser"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tliron/glsp"
|
"github.com/tliron/glsp"
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
@ -13,29 +13,28 @@ func TextDocumentHover(
|
|||||||
context *glsp.Context,
|
context *glsp.Context,
|
||||||
params *protocol.HoverParams,
|
params *protocol.HoverParams,
|
||||||
) (*protocol.Hover, error) {
|
) (*protocol.Hover, error) {
|
||||||
p := documentParserMap[params.TextDocument.URI]
|
d := wireguard.DocumentParserMap[params.TextDocument.URI]
|
||||||
|
line := params.Position.Line
|
||||||
|
|
||||||
switch p.GetTypeByLine(params.Position.Line) {
|
section := d.Config.FindSectionByLine(line)
|
||||||
case parser.LineTypeComment:
|
property := d.Config.FindPropertyByLine(line)
|
||||||
return nil, nil
|
|
||||||
case parser.LineTypeEmpty:
|
index := common.LSPCharacterAsIndexPosition(params.Position.Character)
|
||||||
return nil, nil
|
|
||||||
case parser.LineTypeHeader:
|
if property != nil && section != nil {
|
||||||
fallthrough
|
return handlers.GetPropertyHoverInfo(
|
||||||
case parser.LineTypeProperty:
|
d,
|
||||||
documentation := handlers.GetHoverContent(
|
*section,
|
||||||
*p,
|
*property,
|
||||||
params.Position.Line,
|
index,
|
||||||
params.Position.Character,
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
hover := protocol.Hover{
|
if section != nil && section.Start.Line == line {
|
||||||
Contents: protocol.MarkupContent{
|
return handlers.GetSectionHoverInfo(
|
||||||
Kind: protocol.MarkupKindMarkdown,
|
d,
|
||||||
Value: strings.Join(documentation, "\n"),
|
*section,
|
||||||
},
|
)
|
||||||
}
|
|
||||||
return &hover, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package lsp
|
package lsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"config-lsp/handlers/wireguard"
|
||||||
"config-lsp/handlers/wireguard/handlers"
|
"config-lsp/handlers/wireguard/handlers"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -15,21 +16,15 @@ func WorkspaceExecuteCommand(context *glsp.Context, params *protocol.ExecuteComm
|
|||||||
case string(handlers.CodeActionGeneratePrivateKey):
|
case string(handlers.CodeActionGeneratePrivateKey):
|
||||||
args := handlers.CodeActionGeneratePrivateKeyArgsFromArguments(params.Arguments[0].(map[string]any))
|
args := handlers.CodeActionGeneratePrivateKeyArgsFromArguments(params.Arguments[0].(map[string]any))
|
||||||
|
|
||||||
p := documentParserMap[args.URI]
|
d := wireguard.DocumentParserMap[args.URI]
|
||||||
|
|
||||||
return args.RunCommand(p)
|
return args.RunCommand(d)
|
||||||
case string(handlers.CodeActionGeneratePresharedKey):
|
case string(handlers.CodeActionGeneratePresharedKey):
|
||||||
args := handlers.CodeActionGeneratePresharedKeyArgsFromArguments(params.Arguments[0].(map[string]any))
|
args := handlers.CodeActionGeneratePresharedKeyArgsFromArguments(params.Arguments[0].(map[string]any))
|
||||||
|
|
||||||
parser := documentParserMap[args.URI]
|
d := wireguard.DocumentParserMap[args.URI]
|
||||||
|
|
||||||
return args.RunCommand(parser)
|
return args.RunCommand(d)
|
||||||
case string(handlers.CodeActionAddKeepalive):
|
|
||||||
args := handlers.CodeActionAddKeepaliveArgsFromArguments(params.Arguments[0].(map[string]any))
|
|
||||||
|
|
||||||
p := documentParserMap[args.URI]
|
|
||||||
|
|
||||||
return args.RunCommand(p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -1,127 +0,0 @@
|
|||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"config-lsp/utils"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetLineTypeWorksCorrectly(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
# A comment at the very top
|
|
||||||
Test=Hello
|
|
||||||
|
|
||||||
[Interface]
|
|
||||||
PrivateKey = 1234567890 # Some comment
|
|
||||||
Address = 10.0.0.1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = 1234567890
|
|
||||||
|
|
||||||
; I'm a comment
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
lineType := parser.GetTypeByLine(0)
|
|
||||||
if lineType != LineTypeComment {
|
|
||||||
t.Fatalf("getTypeByLine: Expected line 0 to be a comment, but it is %v", lineType)
|
|
||||||
}
|
|
||||||
|
|
||||||
lineType = parser.GetTypeByLine(1)
|
|
||||||
if lineType != LineTypeProperty {
|
|
||||||
t.Fatalf("getTypeByLine: Expected line 1 to be a property, but it is %v", lineType)
|
|
||||||
}
|
|
||||||
|
|
||||||
lineType = parser.GetTypeByLine(2)
|
|
||||||
if lineType != LineTypeEmpty {
|
|
||||||
t.Fatalf("getTypeByLine: Expected line 2 to be empty, but it is %v", lineType)
|
|
||||||
}
|
|
||||||
|
|
||||||
lineType = parser.GetTypeByLine(3)
|
|
||||||
if lineType != LineTypeHeader {
|
|
||||||
t.Fatalf("getTypeByLine: Expected line 3 to be a header, but it is %v", lineType)
|
|
||||||
}
|
|
||||||
|
|
||||||
lineType = parser.GetTypeByLine(4)
|
|
||||||
if lineType != LineTypeProperty {
|
|
||||||
t.Fatalf("getTypeByLine: Expected line 4 to be a property, but it is %v", lineType)
|
|
||||||
}
|
|
||||||
|
|
||||||
lineType = parser.GetTypeByLine(12)
|
|
||||||
if lineType != LineTypeComment {
|
|
||||||
t.Fatalf("getTypeByLine: Expected line 12 to be a comment, but it is %v", lineType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetBelongingSectionWorksCorrectly(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
# A comment at the very top
|
|
||||||
Test=Hello
|
|
||||||
|
|
||||||
[Interface]
|
|
||||||
PrivateKey = 1234567890 # Some comment
|
|
||||||
Address = 10.0.0.1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = 1234567890
|
|
||||||
|
|
||||||
; I'm a comment
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
section := parser.GetBelongingSectionByLine(0)
|
|
||||||
|
|
||||||
// Comment
|
|
||||||
if section != nil {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 0 to be in no section, but it is in %v", section)
|
|
||||||
}
|
|
||||||
|
|
||||||
section = parser.GetBelongingSectionByLine(1)
|
|
||||||
|
|
||||||
if section != parser.Sections[1] {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 1 to be in global section, but it is in %v", section)
|
|
||||||
}
|
|
||||||
|
|
||||||
section = parser.GetBelongingSectionByLine(2)
|
|
||||||
if section != parser.Sections[1] {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 2 to be in global section, but it is in %v", section)
|
|
||||||
}
|
|
||||||
|
|
||||||
section = parser.GetBelongingSectionByLine(3)
|
|
||||||
if section != parser.Sections[2] {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 3 to be in section Interface, but it is in %v", section)
|
|
||||||
}
|
|
||||||
|
|
||||||
section = parser.GetBelongingSectionByLine(4)
|
|
||||||
if section != parser.Sections[2] {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 4 to be in section Interface, but it is in %v", section)
|
|
||||||
}
|
|
||||||
|
|
||||||
section = parser.GetBelongingSectionByLine(6)
|
|
||||||
if section != parser.Sections[2] {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 6 to be in section Interface, but it is in %v", section)
|
|
||||||
}
|
|
||||||
|
|
||||||
section = parser.GetBelongingSectionByLine(10)
|
|
||||||
if section != parser.Sections[3] {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 10 to be in section Peer, but it is in %v", section)
|
|
||||||
}
|
|
||||||
|
|
||||||
section = parser.GetBelongingSectionByLine(12)
|
|
||||||
|
|
||||||
// Comment
|
|
||||||
if section != nil {
|
|
||||||
t.Fatalf("getBelongingSectionByLine: Expected line 12 to be in no section, but it is in %v", section)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,328 +0,0 @@
|
|||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"config-lsp/common"
|
|
||||||
"regexp"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commentPattern = regexp.MustCompile(`^\s*(;|#)`)
|
|
||||||
var emptyLinePattern = regexp.MustCompile(`^\s*$`)
|
|
||||||
var headerPattern = regexp.MustCompile(`^\s*\[`)
|
|
||||||
|
|
||||||
type CharacterLocation struct {
|
|
||||||
Start uint32
|
|
||||||
End uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
type wireguardLineIndex struct {
|
|
||||||
Type LineType
|
|
||||||
BelongingSection *WireguardSection
|
|
||||||
}
|
|
||||||
|
|
||||||
type WireguardParser struct {
|
|
||||||
// <key = name>: if nil then does not belong to a section
|
|
||||||
Sections []*WireguardSection
|
|
||||||
// Used to identify where not to show diagnostics
|
|
||||||
commentLines map[uint32]struct{}
|
|
||||||
|
|
||||||
// Indexes
|
|
||||||
linesIndexes map[uint32]wireguardLineIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *WireguardParser) Clear() {
|
|
||||||
p.Sections = []*WireguardSection{}
|
|
||||||
p.commentLines = map[uint32]struct{}{}
|
|
||||||
p.linesIndexes = map[uint32]wireguardLineIndex{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *WireguardParser) ParseFromString(input string) []common.ParseError {
|
|
||||||
var errors []common.ParseError
|
|
||||||
lines := strings.Split(
|
|
||||||
input,
|
|
||||||
"\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
slices.Reverse(lines)
|
|
||||||
|
|
||||||
collectedProperties := WireguardProperties{}
|
|
||||||
var lastPropertyLine *uint32
|
|
||||||
|
|
||||||
for index, line := range lines {
|
|
||||||
currentLineNumber := uint32(len(lines) - index - 1)
|
|
||||||
lineType := getLineType(line)
|
|
||||||
|
|
||||||
switch lineType {
|
|
||||||
case LineTypeComment:
|
|
||||||
p.commentLines[currentLineNumber] = struct{}{}
|
|
||||||
p.linesIndexes[currentLineNumber] = wireguardLineIndex{
|
|
||||||
Type: LineTypeComment,
|
|
||||||
BelongingSection: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
case LineTypeEmpty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
case LineTypeProperty:
|
|
||||||
err := collectedProperties.AddLine(currentLineNumber, line)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
errors = append(errors, common.ParseError{
|
|
||||||
Line: currentLineNumber,
|
|
||||||
Err: err,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastPropertyLine == nil {
|
|
||||||
lastPropertyLine = ¤tLineNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
case LineTypeHeader:
|
|
||||||
var lastLine uint32
|
|
||||||
|
|
||||||
if lastPropertyLine == nil {
|
|
||||||
// Current line
|
|
||||||
lastLine = currentLineNumber
|
|
||||||
} else {
|
|
||||||
lastLine = *lastPropertyLine
|
|
||||||
}
|
|
||||||
|
|
||||||
section := CreateWireguardSection(
|
|
||||||
currentLineNumber,
|
|
||||||
lastLine,
|
|
||||||
line,
|
|
||||||
collectedProperties,
|
|
||||||
)
|
|
||||||
|
|
||||||
p.Sections = append(p.Sections, §ion)
|
|
||||||
|
|
||||||
// Add indexes
|
|
||||||
for lineNumber := range collectedProperties {
|
|
||||||
p.linesIndexes[lineNumber] = wireguardLineIndex{
|
|
||||||
Type: LineTypeProperty,
|
|
||||||
BelongingSection: §ion,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.linesIndexes[currentLineNumber] = wireguardLineIndex{
|
|
||||||
Type: LineTypeHeader,
|
|
||||||
BelongingSection: §ion,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset
|
|
||||||
collectedProperties = WireguardProperties{}
|
|
||||||
lastPropertyLine = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var emptySection *WireguardSection
|
|
||||||
|
|
||||||
if len(collectedProperties) > 0 {
|
|
||||||
var endLine uint32
|
|
||||||
|
|
||||||
if len(p.Sections) == 0 {
|
|
||||||
endLine = uint32(len(lines))
|
|
||||||
} else {
|
|
||||||
endLine = p.Sections[len(p.Sections)-1].StartLine
|
|
||||||
}
|
|
||||||
|
|
||||||
emptySection = &WireguardSection{
|
|
||||||
StartLine: 0,
|
|
||||||
EndLine: endLine,
|
|
||||||
Properties: collectedProperties,
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Sections = append(p.Sections, emptySection)
|
|
||||||
|
|
||||||
for lineNumber := range collectedProperties {
|
|
||||||
p.linesIndexes[lineNumber] = wireguardLineIndex{
|
|
||||||
Type: LineTypeProperty,
|
|
||||||
BelongingSection: emptySection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.Sections = append(p.Sections, emptySection)
|
|
||||||
} else {
|
|
||||||
// Add empty section
|
|
||||||
var endLine = uint32(len(lines))
|
|
||||||
|
|
||||||
if len(p.Sections) > 0 {
|
|
||||||
endLine = p.Sections[len(p.Sections)-1].StartLine
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add empty section
|
|
||||||
if endLine != 0 {
|
|
||||||
emptySection = &WireguardSection{
|
|
||||||
StartLine: 0,
|
|
||||||
EndLine: endLine,
|
|
||||||
Properties: collectedProperties,
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Sections = append(p.Sections, emptySection)
|
|
||||||
|
|
||||||
for newLine := uint32(0); newLine < endLine; newLine++ {
|
|
||||||
if _, found := p.linesIndexes[newLine]; found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
p.linesIndexes[newLine] = wireguardLineIndex{
|
|
||||||
Type: LineTypeEmpty,
|
|
||||||
BelongingSection: emptySection,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we parse the content from bottom to top, we need to reverse the sections
|
|
||||||
// so its in correct order
|
|
||||||
slices.Reverse(p.Sections)
|
|
||||||
|
|
||||||
// Fill empty lines between sections
|
|
||||||
for lineNumber, section := range p.Sections {
|
|
||||||
var endLine uint32
|
|
||||||
|
|
||||||
if len(p.Sections) > lineNumber+1 {
|
|
||||||
nextSection := p.Sections[lineNumber+1]
|
|
||||||
endLine = nextSection.StartLine
|
|
||||||
} else {
|
|
||||||
endLine = uint32(len(lines))
|
|
||||||
}
|
|
||||||
|
|
||||||
for newLine := section.StartLine; newLine < endLine; newLine++ {
|
|
||||||
if _, found := p.linesIndexes[newLine]; found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
p.linesIndexes[newLine] = wireguardLineIndex{
|
|
||||||
Type: LineTypeEmpty,
|
|
||||||
BelongingSection: section,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *WireguardParser) GetSectionByLine(line uint32) *WireguardSection {
|
|
||||||
for _, section := range p.Sections {
|
|
||||||
if section.StartLine <= line && section.EndLine >= line {
|
|
||||||
return section
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for a property by name
|
|
||||||
// Returns (line number, property)
|
|
||||||
func (p *WireguardParser) FindFirstPropertyByName(name string) (*uint32, *WireguardProperty) {
|
|
||||||
for _, section := range p.Sections {
|
|
||||||
for lineNumber, property := range section.Properties {
|
|
||||||
if property.Key.Name == name {
|
|
||||||
return &lineNumber, &property
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p WireguardParser) GetInterfaceSection() (*WireguardSection, bool) {
|
|
||||||
for _, section := range p.Sections {
|
|
||||||
if section.Name != nil && *section.Name == "Interface" {
|
|
||||||
return section, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p WireguardParser) GetTypeByLine(line uint32) LineType {
|
|
||||||
// Check if line is a comment
|
|
||||||
if _, found := p.commentLines[line]; found {
|
|
||||||
return LineTypeComment
|
|
||||||
}
|
|
||||||
|
|
||||||
if info, found := p.linesIndexes[line]; found {
|
|
||||||
return info.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
return LineTypeEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the section that the line belongs to
|
|
||||||
// Example:
|
|
||||||
// [Interface]
|
|
||||||
// Address = 10.0.0.1
|
|
||||||
//
|
|
||||||
// <line here>
|
|
||||||
// [Peer]
|
|
||||||
//
|
|
||||||
// This would return the section [Interface]
|
|
||||||
func (p *WireguardParser) GetBelongingSectionByLine(line uint32) *WireguardSection {
|
|
||||||
if info, found := p.linesIndexes[line]; found {
|
|
||||||
return info.BelongingSection
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *WireguardParser) GetPropertyByLine(line uint32) (*WireguardSection, *WireguardProperty) {
|
|
||||||
section := p.GetSectionByLine(line)
|
|
||||||
|
|
||||||
if section == nil || section.Name == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
property, _ := section.GetPropertyByLine(line)
|
|
||||||
|
|
||||||
if property == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return section, property
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *WireguardParser) GetSectionsByName(name string) []*WireguardSection {
|
|
||||||
var sections []*WireguardSection
|
|
||||||
|
|
||||||
for _, section := range p.Sections {
|
|
||||||
if section.Name != nil && *section.Name == name {
|
|
||||||
sections = append(sections, section)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateWireguardParser() WireguardParser {
|
|
||||||
parser := WireguardParser{}
|
|
||||||
parser.Clear()
|
|
||||||
|
|
||||||
return parser
|
|
||||||
}
|
|
||||||
|
|
||||||
type LineType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
LineTypeComment LineType = "comment"
|
|
||||||
LineTypeEmpty LineType = "empty"
|
|
||||||
LineTypeHeader LineType = "header"
|
|
||||||
LineTypeProperty LineType = "property"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getLineType(line string) LineType {
|
|
||||||
if commentPattern.MatchString(line) {
|
|
||||||
return LineTypeComment
|
|
||||||
}
|
|
||||||
|
|
||||||
if emptyLinePattern.MatchString(line) {
|
|
||||||
return LineTypeEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
if headerPattern.MatchString(line) {
|
|
||||||
return LineTypeHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
return LineTypeProperty
|
|
||||||
}
|
|
@ -1,357 +0,0 @@
|
|||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"config-lsp/utils"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/k0kubun/pp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidWildTestWorksFine(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
PrivateKey = 1234567890
|
|
||||||
Address = 192.168.1.0/24
|
|
||||||
|
|
||||||
# I'm a comment
|
|
||||||
[Peer]
|
|
||||||
PublicKey = 1234567890
|
|
||||||
Endpoint = 1.2.3.4 ; I'm just a comment
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = 5555
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.commentLines) == 1 && utils.KeyExists(parser.commentLines, 4)) {
|
|
||||||
t.Fatalf("parseFromString failed to collect comment lines %v", parser.commentLines)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((len(parser.Sections) == 3) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer") && (*parser.Sections[2].Name == "Peer")) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].StartLine == 0 && parser.Sections[0].EndLine == 2 && parser.Sections[1].StartLine == 5 && parser.Sections[1].EndLine == 7 && parser.Sections[2].StartLine == 9 && parser.Sections[2].EndLine == 10) {
|
|
||||||
t.Fatalf("parseFromString: Invalid start and end lines %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((len(parser.Sections[0].Properties) == 2) && (len(parser.Sections[1].Properties) == 2) && (len(parser.Sections[2].Properties) == 1)) {
|
|
||||||
t.Fatalf("parseFromString: Invalid amount of properties %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((parser.Sections[0].Properties[1].Key.Name == "PrivateKey") && (parser.Sections[0].Properties[2].Key.Name == "Address")) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0 %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((parser.Sections[1].Properties[6].Key.Name == "PublicKey") && (parser.Sections[1].Properties[7].Key.Name == "Endpoint")) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 1 %v", parser.Sections[1].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[2].Properties[10].Key.Name == "PublicKey") {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 2 %v", parser.Sections[2].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if line indexes are correct
|
|
||||||
if !(parser.linesIndexes[0].Type == LineTypeHeader &&
|
|
||||||
parser.linesIndexes[1].Type == LineTypeProperty &&
|
|
||||||
parser.linesIndexes[2].Type == LineTypeProperty &&
|
|
||||||
parser.linesIndexes[3].Type == LineTypeEmpty &&
|
|
||||||
parser.linesIndexes[4].Type == LineTypeComment &&
|
|
||||||
parser.linesIndexes[5].Type == LineTypeHeader &&
|
|
||||||
parser.linesIndexes[6].Type == LineTypeProperty &&
|
|
||||||
parser.linesIndexes[7].Type == LineTypeProperty &&
|
|
||||||
parser.linesIndexes[8].Type == LineTypeEmpty &&
|
|
||||||
parser.linesIndexes[9].Type == LineTypeHeader &&
|
|
||||||
parser.linesIndexes[10].Type == LineTypeProperty) {
|
|
||||||
pp.Println(parser.linesIndexes)
|
|
||||||
t.Fatal("parseFromString: Invalid line indexes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptySectionAtStartWorksFine(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = 1234567890
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer")) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties %v", parser.Sections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptySectionAtEndWorksFine(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Inteface]
|
|
||||||
PrivateKey = 1234567890
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
# Just sneaking in here, hehe
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inteface") && (*parser.Sections[1].Name == "Peer")) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.commentLines) == 1 && utils.KeyExists(parser.commentLines, 4)) {
|
|
||||||
t.Fatalf("parseFromString failed to collect comment lines %v", parser.commentLines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmptyFileWorksFine(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections) == 1) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections %v", parser.Sections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartialSectionWithNoPropertiesWorksFine(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Inte
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = 1234567890
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.commentLines) == 0) {
|
|
||||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[1].Properties[3].Key.Name == "PublicKey") {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 1: %v", parser.Sections[1].Properties)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartialSectionWithPropertiesWorksFine(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Inte
|
|
||||||
PrivateKey = 1234567890
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].Properties[1].Key.Name == "PrivateKey") {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFileWithOnlyComments(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
# This is a comment
|
|
||||||
# Another comment
|
|
||||||
`)
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections) == 1) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.commentLines) == 2) {
|
|
||||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(utils.KeyExists(parser.commentLines, 0) && utils.KeyExists(parser.commentLines, 1)) {
|
|
||||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultipleSectionsNoProperties(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
[Peer]
|
|
||||||
[Peer]
|
|
||||||
`)
|
|
||||||
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections) == 3) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, section := range parser.Sections {
|
|
||||||
if len(section.Properties) != 0 {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties: %v", section.Properties)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWildTest1WorksCorrectly(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
DNS=1.1.1.1
|
|
||||||
|
|
||||||
|
|
||||||
`)
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections) == 1) {
|
|
||||||
t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.Sections[0].Properties) == 1) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].Properties[1].Key.Name == "DNS") {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].Properties[1].Value.Value == "1.1.1.1") {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(len(parser.commentLines) == 0) {
|
|
||||||
t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].StartLine == 0 && parser.Sections[0].EndLine == 1) {
|
|
||||||
t.Fatalf("parseFromString: Invalid start and end lines %v", parser.Sections)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartialKeyWorksCorrectly(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
DNS
|
|
||||||
`)
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].Properties[1].Key.Name == "DNS") {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].Properties[1].Separator == nil) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPartialValueWithSeparatorWorksCorrectly(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
sample := utils.Dedent(`
|
|
||||||
[Interface]
|
|
||||||
DNS=
|
|
||||||
`)
|
|
||||||
parser := CreateWireguardParser()
|
|
||||||
errors := parser.ParseFromString(sample)
|
|
||||||
|
|
||||||
if len(errors) > 0 {
|
|
||||||
t.Fatalf("parseFromString failed with error: %v", errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].Properties[1].Value == nil) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(parser.Sections[0].Properties[1].Separator != nil) {
|
|
||||||
t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
docvalues "config-lsp/doc-values"
|
|
||||||
"config-lsp/utils"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
|
||||||
)
|
|
||||||
|
|
||||||
var linePattern = regexp.MustCompile(`^\s*(?P<key>.+?)\s*(?P<separator>=)\s*(?P<value>\S.*?)?\s*(?:(?:;|#).*)?\s*$`)
|
|
||||||
|
|
||||||
type WireguardPropertyKey struct {
|
|
||||||
Location CharacterLocation
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type WireguardPropertyValue struct {
|
|
||||||
Location CharacterLocation
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
type WireguardPropertySeparator struct {
|
|
||||||
Location CharacterLocation
|
|
||||||
}
|
|
||||||
|
|
||||||
type WireguardProperty struct {
|
|
||||||
Key WireguardPropertyKey
|
|
||||||
Separator *WireguardPropertySeparator
|
|
||||||
Value *WireguardPropertyValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p WireguardProperty) String() string {
|
|
||||||
if p.Value == nil {
|
|
||||||
return p.Key.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.Key.Name + "=" + p.Value.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p WireguardProperty) GetLineRange(line uint32) protocol.Range {
|
|
||||||
return protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: line,
|
|
||||||
Character: p.Key.Location.Start,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: line,
|
|
||||||
Character: p.Key.Location.End,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p WireguardProperty) GetInsertRange(line uint32) protocol.Range {
|
|
||||||
var insertPosition uint32 = p.Separator.Location.End
|
|
||||||
var length uint32 = 0
|
|
||||||
|
|
||||||
if p.Value != nil {
|
|
||||||
insertPosition = p.Value.Location.Start - 1
|
|
||||||
// Length of the value; +1 because of the starting space
|
|
||||||
length = (p.Value.Location.End - p.Value.Location.Start) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: line,
|
|
||||||
Character: insertPosition,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: line,
|
|
||||||
Character: insertPosition + length,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WireguardProperties [<line number>]: <property>
|
|
||||||
type WireguardProperties map[uint32]WireguardProperty
|
|
||||||
|
|
||||||
func (p *WireguardProperties) AddLine(lineNumber uint32, line string) error {
|
|
||||||
property, err := CreateWireguardProperty(line)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
(*p)[lineNumber] = *property
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateWireguardProperty(line string) (*WireguardProperty, error) {
|
|
||||||
if !strings.Contains(line, "=") {
|
|
||||||
indexes := utils.GetTrimIndex(line)
|
|
||||||
|
|
||||||
if indexes == nil {
|
|
||||||
// weird, should not happen
|
|
||||||
return nil, &docvalues.MalformedLineError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &WireguardProperty{
|
|
||||||
Key: WireguardPropertyKey{
|
|
||||||
Name: line[indexes[0]:indexes[1]],
|
|
||||||
Location: CharacterLocation{
|
|
||||||
Start: uint32(indexes[0]),
|
|
||||||
End: uint32(indexes[1]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
indexes := linePattern.FindStringSubmatchIndex(line)
|
|
||||||
|
|
||||||
if indexes == nil || len(indexes) == 0 {
|
|
||||||
return nil, &docvalues.MalformedLineError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStart := uint32(indexes[2])
|
|
||||||
keyEnd := uint32(indexes[3])
|
|
||||||
key := WireguardPropertyKey{
|
|
||||||
Location: CharacterLocation{
|
|
||||||
Start: keyStart,
|
|
||||||
End: keyEnd,
|
|
||||||
},
|
|
||||||
Name: line[keyStart:keyEnd],
|
|
||||||
}
|
|
||||||
|
|
||||||
separatorStart := uint32(indexes[4])
|
|
||||||
separatorEnd := uint32(indexes[5])
|
|
||||||
separator := WireguardPropertySeparator{
|
|
||||||
Location: CharacterLocation{
|
|
||||||
Start: separatorStart,
|
|
||||||
End: separatorEnd,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var value *WireguardPropertyValue
|
|
||||||
|
|
||||||
if indexes[6] != -1 && indexes[7] != -1 {
|
|
||||||
// value exists
|
|
||||||
valueStart := uint32(indexes[6])
|
|
||||||
valueEnd := uint32(indexes[7])
|
|
||||||
|
|
||||||
value = &WireguardPropertyValue{
|
|
||||||
Location: CharacterLocation{
|
|
||||||
Start: valueStart,
|
|
||||||
End: valueEnd,
|
|
||||||
},
|
|
||||||
Value: line[valueStart:valueEnd],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &WireguardProperty{
|
|
||||||
Key: key,
|
|
||||||
Separator: &separator,
|
|
||||||
Value: value,
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
package parser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PropertyNotFoundError struct{}
|
|
||||||
|
|
||||||
func (e PropertyNotFoundError) Error() string {
|
|
||||||
return "Property not found"
|
|
||||||
}
|
|
||||||
|
|
||||||
type PropertyNotFullyTypedError struct{}
|
|
||||||
|
|
||||||
func (e PropertyNotFullyTypedError) Error() string {
|
|
||||||
return "Property not fully typed"
|
|
||||||
}
|
|
||||||
|
|
||||||
type WireguardSection struct {
|
|
||||||
Name *string
|
|
||||||
StartLine uint32
|
|
||||||
EndLine uint32
|
|
||||||
Properties WireguardProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s WireguardSection) String() string {
|
|
||||||
var name string
|
|
||||||
|
|
||||||
if s.Name == nil {
|
|
||||||
name = "<nil>"
|
|
||||||
} else {
|
|
||||||
name = *s.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("[%s]; %d-%d: %v", name, s.StartLine, s.EndLine, s.Properties)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s WireguardSection) GetHeaderLineRange() protocol.Range {
|
|
||||||
return protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: s.StartLine,
|
|
||||||
Character: 0,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: s.StartLine,
|
|
||||||
Character: 99999999,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s WireguardSection) GetRange() protocol.Range {
|
|
||||||
return protocol.Range{
|
|
||||||
Start: protocol.Position{
|
|
||||||
Line: s.StartLine,
|
|
||||||
Character: 0,
|
|
||||||
},
|
|
||||||
End: protocol.Position{
|
|
||||||
Line: s.EndLine,
|
|
||||||
Character: 99999999,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s WireguardSection) FetchFirstProperty(name string) (*uint32, *WireguardProperty) {
|
|
||||||
for line, property := range s.Properties {
|
|
||||||
if property.Key.Name == name {
|
|
||||||
return &line, &property
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s WireguardSection) ExistsProperty(name string) bool {
|
|
||||||
_, property := s.FetchFirstProperty(name)
|
|
||||||
|
|
||||||
return property != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s WireguardSection) GetPropertyByLine(lineNumber uint32) (*WireguardProperty, error) {
|
|
||||||
property, found := s.Properties[lineNumber]
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return nil, PropertyNotFoundError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &property, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var validHeaderPattern = regexp.MustCompile(`^\s*\[(?P<header>.+?)\]\s*$`)
|
|
||||||
|
|
||||||
// Create a new create section
|
|
||||||
// Return (<name>, <new section>)
|
|
||||||
func CreateWireguardSection(
|
|
||||||
startLine uint32,
|
|
||||||
endLine uint32,
|
|
||||||
headerLine string,
|
|
||||||
props WireguardProperties,
|
|
||||||
) WireguardSection {
|
|
||||||
match := validHeaderPattern.FindStringSubmatch(headerLine)
|
|
||||||
|
|
||||||
var header string
|
|
||||||
|
|
||||||
if match == nil {
|
|
||||||
// Still typing it
|
|
||||||
header = headerLine[1:]
|
|
||||||
} else {
|
|
||||||
header = match[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return WireguardSection{
|
|
||||||
Name: &header,
|
|
||||||
StartLine: startLine,
|
|
||||||
EndLine: endLine,
|
|
||||||
Properties: props,
|
|
||||||
}
|
|
||||||
}
|
|
14
server/handlers/wireguard/shared.go
Normal file
14
server/handlers/wireguard/shared.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package wireguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"config-lsp/handlers/wireguard/ast"
|
||||||
|
"config-lsp/handlers/wireguard/indexes"
|
||||||
|
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WGDocument struct {
|
||||||
|
Config *ast.WGConfig
|
||||||
|
Indexes *indexes.WGIndexes
|
||||||
|
}
|
||||||
|
|
||||||
|
var DocumentParserMap = map[protocol.DocumentUri]*WGDocument{}
|
@ -2,4 +2,4 @@ package roothandler
|
|||||||
|
|
||||||
// The comment below at the end of the line is required for the CI:CD to work.
|
// The comment below at the end of the line is required for the CI:CD to work.
|
||||||
// Do not remove it
|
// Do not remove it
|
||||||
var Version = "0.1.4" // CI:CD-VERSION
|
var Version = "0.2.0" // CI:CD-VERSION
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
aliases "config-lsp/handlers/aliases/lsp"
|
aliases "config-lsp/handlers/aliases/lsp"
|
||||||
hosts "config-lsp/handlers/hosts/lsp"
|
hosts "config-lsp/handlers/hosts/lsp"
|
||||||
sshconfig "config-lsp/handlers/ssh_config/lsp"
|
sshconfig "config-lsp/handlers/ssh_config/lsp"
|
||||||
|
sshdconfig "config-lsp/handlers/sshd_config/lsp"
|
||||||
wireguard "config-lsp/handlers/wireguard/lsp"
|
wireguard "config-lsp/handlers/wireguard/lsp"
|
||||||
"config-lsp/root-handler/shared"
|
"config-lsp/root-handler/shared"
|
||||||
utils "config-lsp/root-handler/utils"
|
utils "config-lsp/root-handler/utils"
|
||||||
@ -32,7 +33,7 @@ func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionPa
|
|||||||
case shared.LanguageHosts:
|
case shared.LanguageHosts:
|
||||||
return hosts.TextDocumentCodeAction(context, params)
|
return hosts.TextDocumentCodeAction(context, params)
|
||||||
case shared.LanguageSSHDConfig:
|
case shared.LanguageSSHDConfig:
|
||||||
return nil, nil
|
return sshdconfig.TextDocumentCodeAction(context, params)
|
||||||
case shared.LanguageSSHConfig:
|
case shared.LanguageSSHConfig:
|
||||||
return sshconfig.TextDocumentCodeAction(context, params)
|
return sshconfig.TextDocumentCodeAction(context, params)
|
||||||
case shared.LanguageWireguard:
|
case shared.LanguageWireguard:
|
||||||
|
@ -18,46 +18,39 @@ func TextDocumentDidChange(context *glsp.Context, params *protocol.DidChangeText
|
|||||||
language := shared.Handler.GetLanguageForDocument(params.TextDocument.URI)
|
language := shared.Handler.GetLanguageForDocument(params.TextDocument.URI)
|
||||||
|
|
||||||
content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text
|
content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text
|
||||||
newLanguage, err := initFile(
|
|
||||||
context,
|
|
||||||
content,
|
|
||||||
params.TextDocument.URI,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if _, found := shared.OpenedFiles[params.TextDocument.URI]; !found {
|
||||||
if common.ServerOptions.NoUndetectableErrors {
|
// The file couldn't be initialized when opening it,
|
||||||
return nil
|
// so we will try it again here
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newLanguage != language {
|
newLanguage, err := initFile(
|
||||||
language = newLanguage
|
context,
|
||||||
|
content,
|
||||||
|
params.TextDocument.URI,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
params := &protocol.DidOpenTextDocumentParams{
|
if err != nil {
|
||||||
TextDocument: protocol.TextDocumentItem{
|
if common.ServerOptions.NoUndetectableErrors {
|
||||||
URI: params.TextDocument.URI,
|
return nil
|
||||||
Text: content,
|
} else {
|
||||||
Version: params.TextDocument.Version,
|
return err
|
||||||
LanguageID: string(*language),
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch *language {
|
if *newLanguage != *language {
|
||||||
case shared.LanguageFstab:
|
language = newLanguage
|
||||||
return fstab.TextDocumentDidOpen(context, params)
|
|
||||||
case shared.LanguageSSHDConfig:
|
params := &protocol.DidOpenTextDocumentParams{
|
||||||
return sshdconfig.TextDocumentDidOpen(context, params)
|
TextDocument: protocol.TextDocumentItem{
|
||||||
case shared.LanguageSSHConfig:
|
URI: params.TextDocument.URI,
|
||||||
return sshconfig.TextDocumentDidOpen(context, params)
|
Text: content,
|
||||||
case shared.LanguageWireguard:
|
Version: params.TextDocument.Version,
|
||||||
return wireguard.TextDocumentDidOpen(context, params)
|
LanguageID: string(*language),
|
||||||
case shared.LanguageHosts:
|
},
|
||||||
return hosts.TextDocumentDidOpen(context, params)
|
}
|
||||||
case shared.LanguageAliases:
|
|
||||||
return aliases.TextDocumentDidOpen(context, params)
|
return TextDocumentDidOpen(context, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,9 +61,6 @@ func initFile(
|
|||||||
uri protocol.DocumentUri,
|
uri protocol.DocumentUri,
|
||||||
advertisedLanguage string,
|
advertisedLanguage string,
|
||||||
) (*shared.SupportedLanguage, error) {
|
) (*shared.SupportedLanguage, error) {
|
||||||
println("Initializing the file")
|
|
||||||
println(advertisedLanguage)
|
|
||||||
println(uri)
|
|
||||||
language, err := utils.DetectLanguage(content, advertisedLanguage, uri)
|
language, err := utils.DetectLanguage(content, advertisedLanguage, uri)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -150,3 +150,15 @@ func Without[T comparable](a []T, b []T) []T {
|
|||||||
return !found
|
return !found
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindBiggestKey[T any](values map[uint32]T) uint32 {
|
||||||
|
var biggestKey uint32
|
||||||
|
|
||||||
|
for k := range values {
|
||||||
|
if k > biggestKey {
|
||||||
|
biggestKey = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return biggestKey
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "config-lsp",
|
"name": "config-lsp",
|
||||||
"description": "Language Features (completions, diagnostics, etc.) for your config files: gitconfig, fstab, aliases, hosts, wireguard, ssh_config, sshd_config, and more to come!",
|
"description": "Language Features (completions, diagnostics, etc.) for your config files: gitconfig, fstab, aliases, hosts, wireguard, ssh_config, sshd_config, and more to come!",
|
||||||
"author": "Myzel394",
|
"author": "Myzel394",
|
||||||
"version": "0.1.4",
|
"version": "0.2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Myzel394/config-lsp"
|
"url": "https://github.com/Myzel394/config-lsp"
|
||||||
|
1
vs-code-extension/tsconfig.tsbuildinfo
Normal file
1
vs-code-extension/tsconfig.tsbuildinfo
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"root":["./src/extension.ts","./src/events/on-language-detected.ts","./src/events/on-language-undetectable.ts","./src/events/shared.ts","./src/events/types.ts"],"version":"5.7.3"}
|
Loading…
x
Reference in New Issue
Block a user