Merge pull request #14 from Myzel394/add-aliases

Add aliases
This commit is contained in:
Myzel394 2024-09-08 17:14:27 +02:00 committed by GitHub
commit 0c520addf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
125 changed files with 7076 additions and 765 deletions

View File

@ -0,0 +1,5 @@
package commondocumentation
import "regexp"
var EmailRegex = regexp.MustCompile(`([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|"([]!#-[^-~ \t]|(\\[\t -~]))+")@([!#-'*+/-9=?A-Z^-~-]+(\.[!#-'*+/-9=?A-Z^-~-]+)*|\[[\t -Z^-~]*])`)

View File

@ -24,20 +24,3 @@ func SendDiagnostics(context *glsp.Context, uri protocol.DocumentUri, diagnostic
},
)
}
func DiagnoseOption(
context *glsp.Context,
uri protocol.DocumentUri,
parser *SimpleConfigParser,
optionName string,
checkerFunc func(string, SimpleConfigPosition) []protocol.Diagnostic,
) []protocol.Diagnostic {
option, err := parser.GetOption(optionName)
if err != nil {
// Nothing to diagnose
return nil
}
return checkerFunc(option.Value, option.Position)
}

View File

@ -16,6 +16,13 @@ func (l LSPError) ToDiagnostic() protocol.Diagnostic {
}
}
func (l LSPError) ShiftCharacter(offset uint32) LSPError {
return LSPError{
Range: l.Range.ShiftHorizontal(offset),
Err: l.Err,
}
}
type SyntaxError struct {
Message string
}

94
common/fetchers.go Normal file
View File

@ -0,0 +1,94 @@
package common
import (
"os"
"strings"
)
type PasswdInfo struct {
Name string
UID string
GID string
HomePath string
Line uint32
}
var _cachedPasswdInfo []PasswdInfo
func FetchPasswdInfo() ([]PasswdInfo, error) {
if len(_cachedPasswdInfo) > 0 {
return _cachedPasswdInfo, nil
}
readBytes, err := os.ReadFile("/etc/passwd")
if err != nil {
return []PasswdInfo{}, err
}
lines := strings.Split(string(readBytes), "\n")
infos := make([]PasswdInfo, 0)
for lineNumber, line := range lines {
splitted := strings.Split(line, ":")
if len(splitted) < 6 {
continue
}
info := PasswdInfo{
Name: splitted[0],
UID: splitted[2],
GID: splitted[3],
HomePath: splitted[5],
Line: uint32(lineNumber),
}
infos = append(infos, info)
}
_cachedPasswdInfo = infos
return infos, nil
}
type GroupInfo struct {
Name string
GID string
}
var _cachedGroupInfo []GroupInfo
func FetchGroupInfo() ([]GroupInfo, error) {
if len(_cachedGroupInfo) > 0 {
return _cachedGroupInfo, nil
}
readBytes, err := os.ReadFile("/etc/group")
if err != nil {
return []GroupInfo{}, err
}
lines := strings.Split(string(readBytes), "\n")
infos := make([]GroupInfo, 0)
for _, line := range lines {
splitted := strings.Split(line, ":")
if len(splitted) < 3 {
continue
}
info := GroupInfo{
Name: splitted[0],
GID: splitted[2],
}
infos = append(infos, info)
}
_cachedGroupInfo = infos
return infos, nil
}

View File

@ -1 +0,0 @@
package common

View File

@ -1,6 +1,11 @@
package common
import protocol "github.com/tliron/glsp/protocol_3_16"
import (
"fmt"
"github.com/antlr4-go/antlr/v4"
protocol "github.com/tliron/glsp/protocol_3_16"
)
type Location struct {
Line uint32
@ -12,6 +17,38 @@ type LocationRange struct {
End Location
}
func (l LocationRange) ShiftHorizontal(offset uint32) LocationRange {
return LocationRange{
Start: Location{
Line: l.Start.Line,
Character: l.Start.Character + offset,
},
End: Location{
Line: l.End.Line,
Character: l.End.Character + offset,
},
}
}
func (l LocationRange) String() string {
if l.Start.Line == l.End.Line {
return fmt.Sprintf("%d:%d-%d", l.Start.Line, l.Start.Character, l.End.Character)
}
return fmt.Sprintf("%d:%d-%d:%d", l.Start.Line, l.Start.Character, l.End.Line, l.End.Character)
}
var GlobalLocationRange = LocationRange{
Start: Location{
Line: 0,
Character: 0,
},
End: Location{
Line: 0,
Character: 0,
},
}
func (l LocationRange) ToLSPRange() protocol.Range {
return protocol.Range{
Start: protocol.Position{
@ -63,3 +100,22 @@ func CreateSingleCharRange(line uint32, character uint32) LocationRange {
},
}
}
func CharacterRangeFromCtx(
ctx antlr.BaseParserRuleContext,
) LocationRange {
line := uint32(ctx.GetStart().GetLine())
start := uint32(ctx.GetStart().GetStart())
end := uint32(ctx.GetStop().GetStop())
return LocationRange{
Start: Location{
Line: line,
Character: start,
},
End: Location{
Line: line,
Character: end + 1,
},
}
}

View File

@ -1,17 +1,9 @@
package common
import (
docvalues "config-lsp/doc-values"
protocol "github.com/tliron/glsp/protocol_3_16"
"regexp"
"strings"
)
type Parser interface {
ParseFromContent(content string) []ParseError
AnalyzeValues() []protocol.Diagnostic
}
type ParseError struct {
Line uint32
Err error
@ -37,142 +29,3 @@ func (e ParseError) ToDiagnostic() protocol.Diagnostic {
},
}
}
type SimpleConfigPosition struct {
Line uint32
}
type SimpleConfigLine struct {
Value string
Separator string
Position SimpleConfigPosition
}
// Get the character positions of [Option End, Separator End, Value End]
func (l SimpleConfigLine) GetCharacterPositions(optionName string) [3]int {
return [3]int{len(optionName), len(optionName + l.Separator), len(optionName + l.Separator + l.Value)}
}
type SimpleConfigOptions struct {
Separator regexp.Regexp
IgnorePattern regexp.Regexp
// This is the separator that will be used when adding a new line
IdealSeparator string
AvailableOptions *map[string]Option
}
type SimpleConfigParser struct {
Lines map[string]SimpleConfigLine
Options SimpleConfigOptions
}
func (p *SimpleConfigParser) AddLine(line string, lineNumber uint32) (string, error) {
var option string
var separator string
var value string
re := p.Options.Separator
matches := re.FindStringSubmatch(line)
if len(matches) == 0 {
return "", docvalues.MalformedLineError{}
}
optionIndex := re.SubexpIndex("OptionName")
if optionIndex == -1 {
return "", docvalues.MalformedLineError{}
}
option = matches[optionIndex]
if _, exists := (*p.Options.AvailableOptions)[option]; !exists {
return option, docvalues.OptionUnknownError{}
}
separatorIndex := re.SubexpIndex("Separator")
if separatorIndex == -1 {
return option, docvalues.MalformedLineError{}
}
separator = matches[separatorIndex]
valueIndex := re.SubexpIndex("Value")
if valueIndex == -1 {
return option, docvalues.MalformedLineError{}
}
value = matches[valueIndex]
if _, exists := p.Lines[option]; exists {
return option, docvalues.OptionAlreadyExistsError{
AlreadyLine: p.Lines[option].Position.Line,
}
}
p.Lines[option] = SimpleConfigLine{
Value: value,
Separator: separator,
Position: SimpleConfigPosition{
Line: lineNumber,
},
}
return option, nil
}
func (p *SimpleConfigParser) ReplaceOption(option string, value string) {
p.Lines[option] = SimpleConfigLine{
Value: value,
Position: SimpleConfigPosition{
Line: p.Lines[option].Position.Line,
},
}
}
func (p *SimpleConfigParser) RemoveOption(option string) {
delete(p.Lines, option)
}
func (p *SimpleConfigParser) GetOption(option string) (SimpleConfigLine, error) {
if _, exists := p.Lines[option]; exists {
return p.Lines[option], nil
}
return SimpleConfigLine{}, docvalues.OptionUnknownError{}
}
func (p *SimpleConfigParser) ParseFromFile(content string) []docvalues.OptionError {
lines := strings.Split(content, "\n")
errors := make([]docvalues.OptionError, 0)
for index, line := range lines {
if p.Options.IgnorePattern.MatchString(line) {
continue
}
option, err := p.AddLine(line, uint32(index))
if err != nil {
errors = append(errors, docvalues.OptionError{
Line: uint32(index),
ProvidedOption: option,
DocError: err,
})
}
}
return errors
}
func (p *SimpleConfigParser) Clear() {
clear(p.Lines)
}
// TODO: Use better approach: Store an extra array of lines in order; with references to the SimpleConfigLine
func (p *SimpleConfigParser) FindByLineNumber(lineNumber uint32) (string, SimpleConfigLine, error) {
for option, line := range p.Lines {
if line.Position.Line == lineNumber {
return option, line, nil
}
}
return "", SimpleConfigLine{Value: "", Position: SimpleConfigPosition{Line: 0}}, docvalues.LineNotFoundError{}
}

View File

@ -2,7 +2,6 @@ package docvalues
import (
"config-lsp/utils"
protocol "github.com/tliron/glsp/protocol_3_16"
)

View File

@ -1,6 +1,7 @@
package docvalues
import (
"config-lsp/common"
"fmt"
"unicode/utf8"
@ -95,3 +96,22 @@ type LineNotFoundError struct{}
func (e LineNotFoundError) Error() string {
return "Line not found"
}
func LSPErrorFromInvalidValue(
line uint32,
invaludValue InvalidValue,
) common.LSPError {
return common.LSPError{
Range: common.LocationRange{
Start: common.Location{
Line: line,
Character: invaludValue.Start,
},
End: common.Location{
Line: line,
Character: invaludValue.End,
},
},
Err: invaludValue.Err,
}
}

View File

@ -1,63 +1,17 @@
package docvalues
import (
"config-lsp/common"
"config-lsp/utils"
"os"
"regexp"
"strings"
)
type passwdInfo struct {
Name string
UID string
GID string
HomePath string
}
var _cachedPasswdInfo []passwdInfo
func fetchPasswdInfo() ([]passwdInfo, error) {
if len(_cachedPasswdInfo) > 0 {
return _cachedPasswdInfo, nil
}
readBytes, err := os.ReadFile("/etc/passwd")
if err != nil {
return []passwdInfo{}, err
}
lines := strings.Split(string(readBytes), "\n")
infos := make([]passwdInfo, 0)
for _, line := range lines {
splitted := strings.Split(line, ":")
if len(splitted) < 6 {
continue
}
info := passwdInfo{
Name: splitted[0],
UID: splitted[2],
GID: splitted[3],
HomePath: splitted[5],
}
infos = append(infos, info)
}
_cachedPasswdInfo = infos
return infos, nil
}
// UserValue returns a Value that fetches user names from /etc/passwd
// if `separatorForMultiple` is not empty, it will return an ArrayValue
func UserValue(separatorForMultiple string, enforceValues bool) Value {
return CustomValue{
FetchValue: func(context CustomValueContext) Value {
infos, err := fetchPasswdInfo()
infos, err := common.FetchPasswdInfo()
if err != nil {
return StringValue{}
@ -65,7 +19,7 @@ func UserValue(separatorForMultiple string, enforceValues bool) Value {
enumValues := EnumValue{
EnforceValues: enforceValues,
Values: utils.Map(infos, func(info passwdInfo) EnumString {
Values: utils.Map(infos, func(info common.PasswdInfo) EnumString {
return CreateEnumString(info.Name)
}),
}
@ -83,51 +37,10 @@ func UserValue(separatorForMultiple string, enforceValues bool) Value {
}
}
type groupInfo struct {
Name string
GID string
}
var _cachedGroupInfo []groupInfo
func fetchGroupInfo() ([]groupInfo, error) {
if len(_cachedGroupInfo) > 0 {
return _cachedGroupInfo, nil
}
readBytes, err := os.ReadFile("/etc/group")
if err != nil {
return []groupInfo{}, err
}
lines := strings.Split(string(readBytes), "\n")
infos := make([]groupInfo, 0)
for _, line := range lines {
splitted := strings.Split(line, ":")
if len(splitted) < 3 {
continue
}
info := groupInfo{
Name: splitted[0],
GID: splitted[2],
}
infos = append(infos, info)
}
_cachedGroupInfo = infos
return infos, nil
}
func GroupValue(separatorForMultiple string, enforceValues bool) Value {
return CustomValue{
FetchValue: func(context CustomValueContext) Value {
infos, err := fetchGroupInfo()
infos, err := common.FetchGroupInfo()
if err != nil {
return StringValue{}
@ -135,7 +48,7 @@ func GroupValue(separatorForMultiple string, enforceValues bool) Value {
enumValues := EnumValue{
EnforceValues: enforceValues,
Values: utils.Map(infos, func(info groupInfo) EnumString {
Values: utils.Map(infos, func(info common.GroupInfo) EnumString {
return CreateEnumString(info.Name)
}),
}

View File

@ -2,7 +2,6 @@ package docvalues
import (
"config-lsp/utils"
protocol "github.com/tliron/glsp/protocol_3_16"
"golang.org/x/exp/slices"
)

View File

@ -1,6 +1,7 @@
package docvalues
import (
"config-lsp/common"
"config-lsp/utils"
"fmt"
"strconv"
@ -54,7 +55,7 @@ func (v GIDValue) CheckIsValid(value string) []*InvalidValue {
}
if v.EnforceUsingExisting {
infos, err := fetchPasswdInfo()
infos, err := common.FetchPasswdInfo()
if err != nil {
return []*InvalidValue{}
@ -90,7 +91,7 @@ var defaultGIDsExplanation = []EnumString{
}
func (v GIDValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem {
infos, err := fetchGroupInfo()
infos, err := common.FetchGroupInfo()
if err != nil {
return utils.Map(defaultUIDsExplanation, func(enum EnumString) protocol.CompletionItem {
@ -134,7 +135,7 @@ func (v GIDValue) FetchHoverInfo(line string, cursor uint32) []string {
return []string{}
}
infos, err := fetchGroupInfo()
infos, err := common.FetchGroupInfo()
if err != nil {
return []string{}

View File

@ -1,6 +1,7 @@
package docvalues
import (
"config-lsp/common"
"config-lsp/utils"
"fmt"
"strconv"
@ -54,7 +55,7 @@ func (v UIDValue) CheckIsValid(value string) []*InvalidValue {
}
if v.EnforceUsingExisting {
infos, err := fetchPasswdInfo()
infos, err := common.FetchPasswdInfo()
if err != nil {
return []*InvalidValue{}
@ -90,7 +91,7 @@ var defaultUIDsExplanation = []EnumString{
}
func (v UIDValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem {
infos, err := fetchPasswdInfo()
infos, err := common.FetchPasswdInfo()
if err != nil {
return utils.Map(defaultUIDsExplanation, func(enum EnumString) protocol.CompletionItem {
@ -135,7 +136,7 @@ func (v UIDValue) FetchHoverInfo(line string, cursor uint32) []string {
return []string{}
}
infos, err := fetchPasswdInfo()
infos, err := common.FetchPasswdInfo()
if err != nil {
return []string{}

View File

@ -42,7 +42,10 @@
};
};
devShells.default = pkgs.mkShell {
buildInputs = inputs;
buildInputs = inputs ++ (with pkgs; [
mailutils
swaks
]);
};
}
);

1
go.mod
View File

@ -10,6 +10,7 @@ require (
require (
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/k0kubun/pp v3.0.1+incompatible // indirect

2
go.sum
View File

@ -2,6 +2,8 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

106
handlers/aliases/Aliases.g4 Normal file
View File

@ -0,0 +1,106 @@
grammar Aliases;
lineStatement
: entry EOF
;
entry
: SEPARATOR? key SEPARATOR? separator SEPARATOR? values
;
separator
: COLON
;
key
: STRING
;
// // Values // //
values
: (value COMMA SEPARATOR)* value
;
value
: (user | file | command | include | email | error)
;
user
: STRING
;
file
: SLASH (STRING SLASH)* STRING?
;
command
: VERTLINE STRING?
;
include
: COLON INCLUDE COLON file?
;
comment
: NUMBER_SIGN (SEPARATOR? STRING)+ SEPARATOR?
;
email
: STRING AT STRING
;
error
: ERROR COLON errorCode? SEPARATOR? errorMessage?
;
errorCode
: DIGITS
;
errorMessage
: (STRING SEPARATOR)* STRING
;
DIGITS
: [0-9]+
;
ERROR
: 'e' 'r' 'r' 'o' 'r'
;
SEPARATOR
: [ \t]+
;
AT
: '@'
;
INCLUDE
: 'i' 'n' 'c' 'l' 'u' 'd' 'e'
;
VERTLINE
: '|'
;
COLON
: ':'
;
COMMA
: ','
;
NUMBER_SIGN
: '#'
;
SLASH
: '/'
;
STRING
: ~(' ' | '\t' | '\n' | '\r' | ':' | ',' | '#' | '@' | '|' | '/')+
;

View File

@ -0,0 +1,37 @@
package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/utils"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func Analyze(
d *aliases.AliasesDocument,
) []protocol.Diagnostic {
// Double keys must be checked first so
// that the index is populated for the
// other checks
errors := analyzeDoubleKeys(d)
errors = append(errors, analyzeValuesAreValid(d)...)
if len(errors) > 0 {
return utils.Map(
errors,
func(err common.LSPError) protocol.Diagnostic {
return err.ToDiagnostic()
},
)
}
errors = append(errors, analyzeContainsRequiredKeys(*d)...)
errors = append(errors, analyzeContainsNoDoubleValues(*d.Parser)...)
return utils.Map(
errors,
func(err common.LSPError) protocol.Diagnostic {
return err.ToDiagnostic()
},
)
}

View File

@ -0,0 +1,21 @@
package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/indexes"
)
func analyzeDoubleKeys(
d *aliases.AliasesDocument,
) []common.LSPError {
indexes, errors := indexes.CreateIndexes(*d.Parser)
d.Indexes = &indexes
if len(errors) > 0 {
return errors
}
return nil
}

View File

@ -0,0 +1,73 @@
package analyzer
import (
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/utils"
"testing"
)
func TestWorks(
t *testing.T,
) {
input := utils.Dedent(`
support: michael
marketing: john
support: jane
`)
p := ast.NewAliasesParser()
errors := p.Parse(input)
d := aliases.AliasesDocument{
Parser: &p,
}
if len(errors) != 0 {
t.Errorf("Expected no errors, got %v", errors)
}
errors = analyzeDoubleKeys(&d)
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, got %v", errors)
}
if d.Indexes == nil {
t.Errorf("Expected indexes to be set")
}
}
func TestValidWorks(
t *testing.T,
) {
input := utils.Dedent(`
support: michael
marketing: john
supportgroup: jane
suppor: jane
`)
p := ast.NewAliasesParser()
errors := p.Parse(input)
d := aliases.AliasesDocument{
Parser: &p,
}
if len(errors) != 0 {
t.Errorf("Expected no errors, got %v", errors)
}
errors = analyzeDoubleKeys(&d)
if !(len(errors) == 0) {
t.Errorf("Expected 0 errors, got %v", errors)
}
if d.Indexes == nil {
t.Errorf("Expected indexes to be set")
}
if d.Indexes.Keys["support"] == nil {
t.Errorf("Expected support to be in indexes")
}
}

View File

@ -0,0 +1,193 @@
package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/indexes"
"config-lsp/utils"
"errors"
"fmt"
)
var valueHandlerMap = map[string]func(
rawValue []ast.AliasValueInterface,
) []common.LSPError{
"AliasValueUser": analyzeValueUser,
"AliasValueEmail": analyzeValueEmail,
"AliasValueCommand": analyzeValueCommand,
"AliasValueFile": analyzeValueFile,
"AliasValueInclude": analyzeValueInclude,
"AliasValueError": analyzeValueError,
}
func analyzeContainsNoDoubleValues(
p ast.AliasesParser,
) []common.LSPError {
errors := make([]common.LSPError, 0)
it := p.Aliases.Iterator()
for it.Next() {
entry := it.Value().(*ast.AliasEntry)
valuesPerType := utils.Group(
entry.Values.Values,
func(entry ast.AliasValueInterface) string {
return entry.GetStructName()
},
)
for valueType, values := range valuesPerType {
handler := valueHandlerMap[valueType]
newErrors := handler(values)
errors = append(errors, newErrors...)
}
}
return errors
}
func analyzeValueUser(
rawValues []ast.AliasValueInterface,
) []common.LSPError {
users := make(map[string]struct{})
errs := make([]common.LSPError, 0)
// Simple double value check
for _, rawValue := range rawValues {
value := rawValue.(ast.AliasValueUser)
key := indexes.NormalizeKey(value.Value)
if _, found := users[key]; found {
errs = append(errs, common.LSPError{
Range: value.Location,
Err: errors.New(fmt.Sprintf("User '%s' is defined multiple times", key)),
})
} else {
users[key] = struct{}{}
}
}
return errs
}
func analyzeValueEmail(
rawValues []ast.AliasValueInterface,
) []common.LSPError {
emails := make(map[string]struct{})
errs := make([]common.LSPError, 0)
for _, rawValue := range rawValues {
value := rawValue.(ast.AliasValueEmail)
// Simple double value check
if _, found := emails[value.Value]; found {
errs = append(errs, common.LSPError{
Range: value.Location,
Err: errors.New(fmt.Sprintf("Email '%s' is defined multiple times", value.Value)),
})
} else {
emails[value.Value] = struct{}{}
}
}
return errs
}
func analyzeValueCommand(
rawValues []ast.AliasValueInterface,
) []common.LSPError {
commands := make(map[string]struct{})
errs := make([]common.LSPError, 0)
for _, rawValue := range rawValues {
value := rawValue.(ast.AliasValueCommand)
command := value.Command
// Simple double value check
if _, found := commands[command]; found {
errs = append(errs, common.LSPError{
Range: value.Location,
Err: errors.New(fmt.Sprintf("Command '%s' is defined multiple times", command)),
})
} else {
commands[command] = struct{}{}
}
}
return errs
}
func analyzeValueFile(
rawValues []ast.AliasValueInterface,
) []common.LSPError {
files := make(map[string]struct{})
errs := make([]common.LSPError, 0)
for _, rawValue := range rawValues {
value := rawValue.(ast.AliasValueFile)
path := string(value.Path)
// Simple double value check
if _, found := files[path]; found {
errs = append(errs, common.LSPError{
Range: value.Location,
Err: errors.New(fmt.Sprintf("File '%s' is defined multiple times", path)),
})
} else {
files[path] = struct{}{}
}
}
return errs
}
func analyzeValueInclude(
rawValues []ast.AliasValueInterface,
) []common.LSPError {
files := make(map[string]struct{})
errs := make([]common.LSPError, 0)
for _, rawValue := range rawValues {
value := rawValue.(ast.AliasValueInclude)
path := string(value.Path.Path)
// Simple double value check
if _, found := files[path]; found {
errs = append(errs, common.LSPError{
Range: value.Location,
Err: errors.New(fmt.Sprintf("Inclusion '%s' is included multiple times", path)),
})
} else {
files[path] = struct{}{}
}
}
return errs
}
func analyzeValueError(
rawValues []ast.AliasValueInterface,
) []common.LSPError {
codes := make(map[uint16]struct{})
errs := make([]common.LSPError, 0)
for _, rawValue := range rawValues {
value := rawValue.(ast.AliasValueError)
code := value.Code.ErrorCodeAsInt()
// Simple double value check
if _, found := codes[code]; found {
errs = append(errs, common.LSPError{
Range: value.Location,
Err: errors.New(fmt.Sprintf("Error code '%d' is defined multiple times", code)),
})
} else {
codes[code] = struct{}{}
}
}
return errs
}

View File

@ -0,0 +1,119 @@
package analyzer
import (
"config-lsp/handlers/aliases/ast"
"config-lsp/utils"
"testing"
)
func TestContainsDoubleUsers(
t *testing.T,
) {
input := utils.Dedent(`
alice: root
master: alice, alice
`)
p := ast.NewAliasesParser()
errors := p.Parse(input)
// d := aliases.AliasesDocument{
// Parser: &p,
// }
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
errors = analyzeContainsNoDoubleValues(p)
if !(len(errors) == 1) {
t.Errorf("Expected errors, got none")
}
}
func TestContainsDoubleEmails(
t *testing.T,
) {
input := utils.Dedent(`
alice: root@localhost, some, noise, here, root@localhost
`)
p := ast.NewAliasesParser()
errors := p.Parse(input)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
errors = analyzeContainsNoDoubleValues(p)
if !(len(errors) == 1) {
t.Errorf("Expected errors, got none")
}
}
func TestContainsDoubleCommands(
t *testing.T,
) {
input := utils.Dedent(`
alice: |echo, |test, |echo
`)
p := ast.NewAliasesParser()
errors := p.Parse(input)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
errors = analyzeContainsNoDoubleValues(p)
if !(len(errors) == 1) {
t.Errorf("Expected errors, got none")
}
}
func TestContainsDoubleErrors(
t *testing.T,
) {
input := utils.Dedent(`
alice: error:450 Nonono, error:450 Some other message
root: error:450 Nonon, error:451 This is not okay
`)
p := ast.NewAliasesParser()
errors := p.Parse(input)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
errors = analyzeContainsNoDoubleValues(p)
if !(len(errors) == 1) {
t.Errorf("Expected no errors, got %v", errors)
}
if !(errors[0].Range.Start.Line == 0 && errors[0].Range.End.Line == 0) {
t.Errorf("Expected error to be on line 0, got %v-%v", errors[0].Range.Start.Line, errors[0].Range.End.Line)
}
}
func TestComplexExampleContainsNoDoubleValues(
t *testing.T,
) {
input := utils.Dedent(`
alice: root@localhost, user@localhost
master: alice, root
noreply: error:450 Nonono
`)
p := ast.NewAliasesParser()
errors := p.Parse(input)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
errors = analyzeContainsNoDoubleValues(p)
if !(len(errors) == 0) {
t.Errorf("Expected no errors, got %v", errors)
}
}

View File

@ -0,0 +1,33 @@
package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/indexes"
"fmt"
ers "errors"
)
var RequiredAliases = []string{
indexes.NormalizeKey("mailer-daemon"),
indexes.NormalizeKey("hostmaster"),
indexes.NormalizeKey("postmaster"),
}
func analyzeContainsRequiredKeys(
d aliases.AliasesDocument,
) []common.LSPError {
errors := make([]common.LSPError, 0)
for _, requiredField := range RequiredAliases {
if _, found := d.Indexes.Keys[requiredField]; !found {
errors = append(errors, common.LSPError{
Range: common.GlobalLocationRange,
Err: ers.New(fmt.Sprintf("Please add the alias '%s'. It is required by the aliases file.", requiredField)),
})
}
}
return errors
}

View File

@ -0,0 +1,146 @@
package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/fetchers"
"config-lsp/handlers/aliases/indexes"
"fmt"
"net/mail"
"path"
"strconv"
ers "errors"
)
func analyzeValuesAreValid(
d *aliases.AliasesDocument,
) []common.LSPError {
errors := make([]common.LSPError, 0)
it := d.Parser.Aliases.Iterator()
for it.Next() {
entry := it.Value().(*ast.AliasEntry)
if entry.Key == nil {
errors = append(errors, common.LSPError{
Range: entry.Location,
Err: ers.New("An alias is required"),
})
continue
}
if entry.Separator == nil {
errors = append(errors, common.LSPError{
Range: entry.Location,
Err: ers.New("A ':' is required as a separator"),
})
continue
}
if entry.Values == nil || len(entry.Values.Values) == 0 {
errors = append(errors, common.LSPError{
Range: entry.Location,
Err: ers.New("A value is required"),
})
continue
}
for _, value := range entry.Values.Values {
newErrors := checkValue(d.Indexes, value)
errors = append(errors, newErrors...)
}
}
return errors
}
func checkValue(
i *indexes.AliasesIndexes,
value ast.AliasValueInterface,
) []common.LSPError {
switch value.(type) {
case ast.AliasValueUser:
aliasValue := value.(ast.AliasValueUser)
users := fetchers.GetAvailableUserValues(i)
if _, found := users[aliasValue.Value]; !found {
return []common.LSPError{{
Range: aliasValue.Location,
Err: ers.New(fmt.Sprintf("User '%s' not found", aliasValue.Value)),
}}
}
case ast.AliasValueEmail:
emailValue := value.(ast.AliasValueEmail)
if _, error := mail.ParseAddress(emailValue.Value); error != nil {
return []common.LSPError{{
Range: emailValue.Location,
Err: ers.New(fmt.Sprintf("This does not seem to be a valid email: %s", error.Error())),
}}
}
case ast.AliasValueFile:
fileValue := value.(ast.AliasValueFile)
// I'm not sure if the path really needs to be absolute
// The docs say:
// "Append messages to file, specified by its absolute pathname."
//
if !path.IsAbs(fileValue.Value) {
return []common.LSPError{{
Range: fileValue.Location,
Err: ers.New("This path must be absolute"),
}}
}
case ast.AliasValueError:
errorValue := value.(ast.AliasValueError)
if errorValue.Code == nil {
return []common.LSPError{{
Range: errorValue.Location,
Err: ers.New("An error code in the form of 4XX or 5XX is required"),
}}
}
errorCode, err := strconv.Atoi(errorValue.Code.Value)
if err != nil || (errorCode < 400 || errorCode > 599) {
return []common.LSPError{{
Range: errorValue.Code.Location,
Err: ers.New("This error code is invalid. It must be in the form of 4XX or 5XX"),
}}
}
if errorValue.Message == nil || errorValue.Message.Value == "" {
return []common.LSPError{{
Range: errorValue.Location,
Err: ers.New("An error message is required"),
}}
}
case ast.AliasValueInclude:
incldueValue := value.(ast.AliasValueInclude)
if incldueValue.Path == nil {
return []common.LSPError{{
Range: incldueValue.Location,
Err: ers.New("A path is required"),
}}
}
// Again, I'm not sure if the path really needs to be absolute
if !path.IsAbs(string(incldueValue.Path.Path)) {
return []common.LSPError{{
Range: incldueValue.Path.Location,
Err: ers.New("This path must be absolute"),
}}
}
}
return nil
}

View File

@ -0,0 +1,41 @@
package ast
import (
"config-lsp/common"
"github.com/emirpasic/gods/maps/treemap"
)
// Procedure
// Save options in fields
// Each type, such as Include, User etc is own type
// Each type inherits interface
// This interface has methods such as:
// - CheckIsUser()
// For user, this checks if the user is listed in passwd
// For include, this includes the file and parses it and validates it
//
// Parse content manually as the /etc/aliases file is so simple
type AliasKey struct {
Location common.LocationRange
Value string
}
type AliasValues struct {
Location common.LocationRange
Values []AliasValueInterface
}
type AliasEntry struct {
Location common.LocationRange
Key *AliasKey
Separator *common.LocationRange
Values *AliasValues
}
type AliasesParser struct {
// uint32 -> *AliasEntry
Aliases *treemap.Map
CommentLines map[uint32]struct{}
}

View File

@ -0,0 +1,298 @@
package ast
import (
"config-lsp/common"
"config-lsp/handlers/aliases/parser"
"github.com/antlr4-go/antlr/v4"
)
type aliasesListenerContext struct {
line uint32
currentIncludeIndex *uint32
currentErrorValueIndex *uint32
}
type aliasesParserListener struct {
*parser.BaseAliasesListener
Parser *AliasesParser
Errors []common.LSPError
aliasContext aliasesListenerContext
}
func (s *aliasesParserListener) EnterEntry(ctx *parser.EntryContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
s.Parser.Aliases.Put(
location.Start.Line,
&AliasEntry{
Location: location,
},
)
}
func (s *aliasesParserListener) EnterSeparator(ctx *parser.SeparatorContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Separator = &location
}
func (s *aliasesParserListener) EnterKey(ctx *parser.KeyContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Key = &AliasKey{
Location: location,
Value: ctx.GetText(),
}
}
func (s *aliasesParserListener) EnterValues(ctx *parser.ValuesContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Values = &AliasValues{
Location: location,
Values: make([]AliasValueInterface, 0, 5),
}
}
// === Value === //
func (s *aliasesParserListener) EnterUser(ctx *parser.UserContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
user := AliasValueUser{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
}
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Values.Values = append(entry.Values.Values, user)
}
func (s *aliasesParserListener) EnterFile(ctx *parser.FileContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
if s.aliasContext.currentIncludeIndex != nil {
// This `file` is inside an `include`, so we need to set the path on the include
values := entry.Values
rawValue := values.Values[*s.aliasContext.currentIncludeIndex]
// Set the path
include := rawValue.(AliasValueInclude)
include.Path = &AliasValueIncludePath{
Location: location,
Path: path(ctx.GetText()),
}
values.Values[*s.aliasContext.currentIncludeIndex] = include
// Clean up
s.aliasContext.currentIncludeIndex = nil
return
}
// Raw file, process it
file := AliasValueFile{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
Path: path(ctx.GetText()),
}
entry.Values.Values = append(entry.Values.Values, file)
}
func (s *aliasesParserListener) EnterCommand(ctx *parser.CommandContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
command := AliasValueCommand{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
Command: ctx.GetText()[1:],
}
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Values.Values = append(entry.Values.Values, command)
}
func (s *aliasesParserListener) EnterInclude(ctx *parser.IncludeContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
include := AliasValueInclude{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
}
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Values.Values = append(entry.Values.Values, include)
index := uint32(len(entry.Values.Values) - 1)
s.aliasContext.currentIncludeIndex = &index
}
func (s *aliasesParserListener) EnterEmail(ctx *parser.EmailContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
email := AliasValueEmail{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
}
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Values.Values = append(entry.Values.Values, email)
}
func (s *aliasesParserListener) EnterError(ctx *parser.ErrorContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
errorValue := AliasValueError{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
}
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
entry.Values.Values = append(entry.Values.Values, errorValue)
index := uint32(len(entry.Values.Values) - 1)
s.aliasContext.currentErrorValueIndex = &index
}
func (s *aliasesParserListener) ExitError(ctx *parser.ErrorContext) {
s.aliasContext.currentErrorValueIndex = nil
}
// EnterErrorCode is called when production errorCode is entered.
func (s *aliasesParserListener) EnterErrorCode(ctx *parser.ErrorCodeContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
values := entry.Values.Values
rawValue := values[*s.aliasContext.currentErrorValueIndex]
value := rawValue.(AliasValueError)
value.Code = &AliasValueErrorCode{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
}
values[*s.aliasContext.currentErrorValueIndex] = value
}
// EnterErrorMessage is called when production errorMessage is entered.
func (s *aliasesParserListener) EnterErrorMessage(ctx *parser.ErrorMessageContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.aliasContext.line)
rawEntry, _ := s.Parser.Aliases.Get(location.Start.Line)
entry := rawEntry.(*AliasEntry)
values := entry.Values.Values
rawValue := values[*s.aliasContext.currentErrorValueIndex]
value := rawValue.(AliasValueError)
value.Message = &AliasValueErrorMessage{
AliasValue: AliasValue{
Location: location,
Value: ctx.GetText(),
},
}
values[*s.aliasContext.currentErrorValueIndex] = value
}
func createListener(
parser *AliasesParser,
line uint32,
) aliasesParserListener {
return aliasesParserListener{
Parser: parser,
aliasContext: aliasesListenerContext{
line: line,
},
Errors: make([]common.LSPError, 0),
}
}
type errorListener struct {
*antlr.DefaultErrorListener
Errors []common.LSPError
context aliasesListenerContext
}
func (d *errorListener) SyntaxError(
recognizer antlr.Recognizer,
offendingSymbol interface{},
_ int,
character int,
message string,
error antlr.RecognitionException,
) {
line := d.context.line
d.Errors = append(d.Errors, common.LSPError{
Range: common.CreateSingleCharRange(uint32(line), uint32(character)),
Err: common.SyntaxError{
Message: message,
},
})
}
func createErrorListener(
line uint32,
) errorListener {
return errorListener{
Errors: make([]common.LSPError, 0),
context: aliasesListenerContext{
line: line,
},
}
}

View File

@ -0,0 +1,82 @@
package ast
import (
"config-lsp/common"
"config-lsp/handlers/aliases/parser"
"config-lsp/utils"
"regexp"
"github.com/antlr4-go/antlr/v4"
"github.com/emirpasic/gods/maps/treemap"
gods "github.com/emirpasic/gods/utils"
)
func NewAliasesParser() AliasesParser {
p := AliasesParser{}
p.Clear()
return p
}
func (p *AliasesParser) Clear() {
p.CommentLines = make(map[uint32]struct{})
p.Aliases = treemap.NewWith(gods.UInt32Comparator)
}
var commentPattern = regexp.MustCompile(`^\s*#.*$`)
var emptyPattern = regexp.MustCompile(`^\s*$`)
func (p *AliasesParser) Parse(input string) []common.LSPError {
errors := make([]common.LSPError, 0)
lines := utils.SplitIntoLines(input)
for rawLineNumber, line := range lines {
lineNumber := uint32(rawLineNumber)
if commentPattern.MatchString(line) {
p.CommentLines[lineNumber] = struct{}{}
continue
}
if emptyPattern.MatchString(line) {
continue
}
errors = append(
errors,
p.parseStatement(lineNumber, line)...,
)
}
return errors
}
func (p *AliasesParser) parseStatement(
line uint32,
input string,
) []common.LSPError {
stream := antlr.NewInputStream(input)
lexerErrorListener := createErrorListener(line)
lexer := parser.NewAliasesLexer(stream)
lexer.RemoveErrorListeners()
lexer.AddErrorListener(&lexerErrorListener)
tokenStream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)
parserErrorListener := createErrorListener(line)
antlrParser := parser.NewAliasesParser(tokenStream)
antlrParser.RemoveErrorListeners()
antlrParser.AddErrorListener(&parserErrorListener)
listener := createListener(p, line)
antlr.ParseTreeWalkerDefault.Walk(
&listener,
antlrParser.LineStatement(),
)
errors := lexerErrorListener.Errors
errors = append(errors, parserErrorListener.Errors...)
return errors
}

View File

@ -0,0 +1,151 @@
package ast
import (
"config-lsp/utils"
"testing"
)
func TestSimpleExampleWorks(
t *testing.T,
) {
input := utils.Dedent(`
postmaster: root
`)
parser := NewAliasesParser()
errors := parser.Parse(input)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
if !(parser.Aliases.Size() == 1) {
t.Fatalf("Expected 1 alias, got %v", parser.Aliases.Size())
}
rawEntry, _ := parser.Aliases.Get(uint32(0))
entry := rawEntry.(*AliasEntry)
if !(entry.Key.Value == "postmaster") {
t.Fatalf("Expected key to be 'postmaster', got %v", entry.Key.Value)
}
userValue := entry.Values.Values[0].(AliasValueUser)
if !(userValue.Value == "root") {
t.Fatalf("Expected value to be 'root', got %v", userValue.Value)
}
if !(userValue.Location.Start.Line == 0) {
t.Fatalf("Expected start line to be 0, got %v", userValue.Location.Start.Line)
}
}
func TestMultipleValuesWorks(
t *testing.T,
) {
input := utils.Dedent(`
heinz: root, goli
michel: raiks@example.com
`)
parser := NewAliasesParser()
errors := parser.Parse(input)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
if !(parser.Aliases.Size() == 2) {
t.Fatalf("Expected 2 aliases, got %v", parser.Aliases.Size())
}
rawEntry, _ := parser.Aliases.Get(uint32(0))
entry := rawEntry.(*AliasEntry)
if !(entry.Key.Value == "heinz") {
t.Fatalf("Expected key to be 'heinz', got %v", entry.Key.Value)
}
rawEntry, _ = parser.Aliases.Get(uint32(1))
entry = rawEntry.(*AliasEntry)
if !(entry.Key.Value == "michel") {
t.Fatalf("Expected key to be 'michel', got %v", entry.Key.Value)
}
emailValue := entry.Values.Values[0].(AliasValueEmail)
if !(emailValue.Value == "raiks@example.com") {
t.Fatalf("Expected value to be 'raiks@example.com', got %v", emailValue.Value)
}
}
func TestIncludeWorks(
t *testing.T,
) {
input := utils.Dedent(`
luke: :include:/etc/other_aliases
`)
parser := NewAliasesParser()
errors := parser.Parse(input)
if len(errors) != 0 {
t.Fatalf("Expected no errors, got %v", errors)
}
if !(parser.Aliases.Size() == 1) {
t.Fatalf("Expected 1 alias, got %v", parser.Aliases.Size())
}
rawEntry, _ := parser.Aliases.Get(uint32(0))
entry := rawEntry.(*AliasEntry)
if !(entry.Key.Value == "luke") {
t.Fatalf("Expected key to be 'luke', got %v", entry.Key.Value)
}
includeValue := entry.Values.Values[0].(AliasValueInclude)
if !(includeValue.Path.Path == "/etc/other_aliases") {
t.Fatalf("Expected path to be '/etc/other_aliases', got %v", includeValue.Path.Path)
}
if !(includeValue.Location.Start.Character == 6 && includeValue.Location.End.Character == 33) {
t.Fatalf("Expected location to be 6-33, got %v-%v", includeValue.Location.Start.Character, includeValue.Location.End.Character)
}
if !(includeValue.Path.Location.Start.Character == 15 && includeValue.Path.Location.End.Character == 33) {
t.Fatalf("Expected path location to be 15-33, got %v-%v", includeValue.Path.Location.Start.Character, includeValue.Path.Location.End.Character)
}
}
func TestInvalidWithEmptyValuesWorks(
t *testing.T,
) {
input := utils.Dedent(`
luke:
`)
parser := NewAliasesParser()
errors := parser.Parse(input)
if len(errors) == 0 {
t.Fatalf("Expected 1 error, got %v", errors)
}
if !(errors[0].Range.Start.Character == 5 && errors[0].Range.End.Character == 5) {
t.Fatalf("Expected error to be at 6, got %v", errors[0].Range.Start.Character)
}
}
func TestInvalidWithEmptyKeyWorks(
t *testing.T,
) {
input := utils.Dedent(`
: root
`)
parser := NewAliasesParser()
errors := parser.Parse(input)
if len(errors) == 0 {
t.Fatalf("Expected 1 error, got %v", errors)
}
if !(errors[0].Range.Start.Character == 0 && errors[0].Range.End.Character == 0) {
t.Fatalf("Expected error to be at 0, got %v", errors[0].Range.Start.Character)
}
}

View File

@ -0,0 +1,129 @@
package ast
import (
"config-lsp/common"
docvalues "config-lsp/doc-values"
"config-lsp/handlers/aliases/fields"
"config-lsp/utils"
"fmt"
"strconv"
)
type AliasValueInterface interface {
GetAliasValue() AliasValue
GetStructName() string
}
func (a AliasValue) String() string {
return fmt.Sprintf("%s %s", a.Location, a.Value)
}
func (a AliasValue) GetAliasValue() AliasValue {
return a
}
func (a AliasValue) GetStructName() string {
return "AliasValue"
}
type AliasValue struct {
Location common.LocationRange
Value string
}
type AliasValueUser struct {
AliasValue
}
func (a AliasValueUser) GetStructName() string {
return "AliasValueUser"
}
type path string
type AliasValueFile struct {
AliasValue
Path path
}
func (a AliasValueFile) GetStructName() string {
return "AliasValueFile"
}
type AliasValueCommand struct {
AliasValue
Command string
}
func (a AliasValueCommand) GetStructName() string {
return "AliasValueCommand"
}
type AliasValueIncludePath struct {
Location common.LocationRange
Path path
}
type AliasValueInclude struct {
AliasValue
Path *AliasValueIncludePath
}
func (a AliasValueInclude) CheckIsValid() []common.LSPError {
return utils.Map(
fields.PathField.CheckIsValid(string(a.Path.Path)),
func(invalidValue *docvalues.InvalidValue) common.LSPError {
return docvalues.LSPErrorFromInvalidValue(a.Location.Start.Line, *invalidValue)
},
)
}
func (a AliasValueInclude) GetStructName() string {
return "AliasValueInclude"
}
type AliasValueEmail struct {
AliasValue
}
func (a AliasValueEmail) CheckIsValid() []common.LSPError {
return utils.Map(
fields.PathField.CheckIsValid(a.Value),
func(invalidValue *docvalues.InvalidValue) common.LSPError {
return docvalues.LSPErrorFromInvalidValue(a.Location.Start.Line, *invalidValue)
},
)
}
func (a AliasValueEmail) GetStructName() string {
return "AliasValueEmail"
}
type AliasValueError struct {
AliasValue
Code *AliasValueErrorCode
Message *AliasValueErrorMessage
}
type AliasValueErrorCode struct {
AliasValue
}
func (a AliasValueError) GetStructName() string {
return "AliasValueError"
}
func (a AliasValueErrorCode) ErrorCodeAsInt() uint16 {
code, err := strconv.Atoi(a.Value)
if err != nil {
return 0
}
return uint16(code)
}
type AliasValueErrorMessage struct {
AliasValue
}

View File

@ -0,0 +1,68 @@
package fetchers
import (
"config-lsp/common"
"config-lsp/handlers/aliases/indexes"
"fmt"
"strings"
)
type aliasesUser struct {
DefinedOnLine uint32
}
type User struct {
PasswdInfo *common.PasswdInfo
AliasInfo *aliasesUser
}
func (u User) Documentation() string {
if u.PasswdInfo != nil {
return strings.Join(
[]string{
fmt.Sprintf("%s (%s:%s)", u.PasswdInfo.Name, u.PasswdInfo.UID, u.PasswdInfo.GID),
fmt.Sprintf("Home: `%s`", u.PasswdInfo.HomePath),
},
"\n",
)
}
if u.AliasInfo != nil {
return fmt.Sprintf("Defined on line %d", u.AliasInfo.DefinedOnLine+1)
}
return ""
}
// Returns a map of [username]user
// The username is normalized
func GetAvailableUserValues(
i *indexes.AliasesIndexes,
) map[string]User {
users := make(map[string]User)
passwdUsers, err := common.FetchPasswdInfo()
if err == nil {
for _, info := range passwdUsers {
key := indexes.NormalizeKey(info.Name)
users[key] = User{
PasswdInfo: &info,
}
}
}
if i != nil && i.Keys != nil {
for name, key := range i.Keys {
// Indexes keys are already normalized
users[name] = User{
AliasInfo: &aliasesUser{
DefinedOnLine: key.Location.Start.Line,
},
}
}
}
return users
}

View File

@ -0,0 +1,54 @@
package fields
import (
commondocumentation "config-lsp/common-documentation"
docvalues "config-lsp/doc-values"
)
var UserField = docvalues.DocumentationValue{
Documentation: "A user on the host machine. The user must have a valid entry in the passwd(5) database file.",
Value: docvalues.UserValue("", false),
}
var UserDeclaration = "`user`"
var PathField = docvalues.DocumentationValue{
Documentation: "Append messages to file, specified by its absolute pathname",
Value: docvalues.PathValue{
RequiredType: docvalues.PathTypeFile,
},
}
var PathDeclaration = "`/path/to/file`"
var CommandField = docvalues.DocumentationValue{
Documentation: "Pipe the message to command on its standard input. The command is run under the privileges of the daemon's unprivileged account.",
Value: docvalues.StringValue{},
}
var CommandDeclaration = "`|command`"
var EmailField = docvalues.DocumentationValue{
Documentation: "An email address in RFC 5322 format. If an address extension is appended to the user-part, it is first compared for an exact match. It is then stripped so that an address such as user+ext@example.com will only use the part that precedes + as a key.",
Value: docvalues.RegexValue{
Regex: *commondocumentation.EmailRegex,
},
}
var EmailDeclaration = "`user-part@domain-part`"
var IncludeField = docvalues.DocumentationValue{
Documentation: "Include any definitions in file as alias entries. The format of the file is identical to this one.",
Value: docvalues.PathValue{
RequiredType: docvalues.PathTypeFile,
},
}
var IncludeDeclaration = "`include:/path/to/file`"
var ErrorMessageField = docvalues.DocumentationValue{
Documentation: "A status code and message to return. The code must be 3 digits, starting 4XX (TempFail) or 5XX (PermFail). The message must be present and can be freely chosen.",
Value: docvalues.StringValue{},
}
var ErrorDeclaration = "`error:code message`"

View File

@ -0,0 +1,206 @@
package handlers
import (
"config-lsp/handlers/aliases/analyzer"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/fetchers"
"config-lsp/handlers/aliases/indexes"
"fmt"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func GetAliasesCompletions(
i *indexes.AliasesIndexes,
) []protocol.CompletionItem {
completions := make([]protocol.CompletionItem, 0)
aliases := analyzer.RequiredAliases
kind := protocol.CompletionItemKindValue
for _, alias := range aliases {
if i != nil {
if _, found := i.Keys[alias]; found {
continue
}
}
text := fmt.Sprintf("%s: ", alias)
completions = append(completions, protocol.CompletionItem{
Label: alias,
Kind: &kind,
InsertText: &text,
Documentation: "This alias is required by the aliases file",
})
}
return completions
}
func GetCompletionsForEntry(
cursor uint32,
entry *ast.AliasEntry,
i *indexes.AliasesIndexes,
) ([]protocol.CompletionItem, error) {
completions := make([]protocol.CompletionItem, 0)
if entry.Key == nil {
return completions, nil
}
value := GetValueAtCursor(cursor, entry)
relativeCursor := cursor - entry.Key.Location.Start.Character
excludedUsers := getUsersFromEntry(entry)
if value == nil {
completions = append(completions, getCommandCompletion())
completions = append(completions, getIncludeCompletion())
completions = append(completions, getErrorCompletion())
completions = append(completions, getUserCompletions(
i,
excludedUsers,
"",
0,
)...)
return completions, nil
}
switch (*value).(type) {
case ast.AliasValueUser:
userValue := (*value).(ast.AliasValueUser)
return getUserCompletions(
i,
excludedUsers,
userValue.Value,
relativeCursor,
), nil
case ast.AliasValueError:
errorValue := (*value).(ast.AliasValueError)
isAtErrorCode := errorValue.Code == nil &&
relativeCursor >= errorValue.Location.Start.Character &&
(errorValue.Message == nil ||
relativeCursor <= errorValue.Message.Location.Start.Character)
if isAtErrorCode {
kind := protocol.CompletionItemKindValue
detail_4 := "4XX (TempFail)"
insertText_4 := "400"
detail_5 := "5XX (PermFail)"
insertText_5 := "500"
return []protocol.CompletionItem{
{
Label: "4XX",
InsertText: &insertText_4,
Kind: &kind,
Detail: &detail_4,
},
{
Label: "5XX",
InsertText: &insertText_5,
Kind: &kind,
Detail: &detail_5,
},
}, nil
}
}
return completions, nil
}
func getCommandCompletion() protocol.CompletionItem {
kind := protocol.CompletionItemKindKeyword
textFormat := protocol.InsertTextFormatSnippet
insertText := "|"
return protocol.CompletionItem{
Label: "|<command>",
Documentation: "Pipe the message to command on its standard input. The command is run under the privileges of the daemon's unprivileged account.",
Kind: &kind,
InsertTextFormat: &textFormat,
InsertText: &insertText,
}
}
func getIncludeCompletion() protocol.CompletionItem {
kind := protocol.CompletionItemKindKeyword
textFormat := protocol.InsertTextFormatSnippet
insertText := ":include:"
return protocol.CompletionItem{
Label: ":include:<path>",
Documentation: " Include any definitions in file as alias entries. The format of the file is identical to this one.",
Kind: &kind,
InsertTextFormat: &textFormat,
InsertText: &insertText,
}
}
func getErrorCompletion() protocol.CompletionItem {
kind := protocol.CompletionItemKindKeyword
textFormat := protocol.InsertTextFormatSnippet
insertText := "error:"
return protocol.CompletionItem{
Label: "error:<message>",
Documentation: "A status code and message to return. The code must be 3 digits, starting 4XX (TempFail) or 5XX (PermFail). The message must be present and can be freely chosen.",
Kind: &kind,
InsertTextFormat: &textFormat,
InsertText: &insertText,
}
}
func getUserCompletions(
i *indexes.AliasesIndexes,
excluded map[string]struct{},
line string,
cursor uint32,
) []protocol.CompletionItem {
users := fetchers.GetAvailableUserValues(i)
kind := protocol.CompletionItemKindValue
completions := make([]protocol.CompletionItem, 0)
for name, user := range users {
if _, found := excluded[name]; found {
continue
}
completions = append(completions, protocol.CompletionItem{
Label: name,
Kind: &kind,
Documentation: user.Documentation(),
})
}
return completions
}
func getUsersFromEntry(
entry *ast.AliasEntry,
) map[string]struct{} {
users := map[string]struct{}{
indexes.NormalizeKey(entry.Key.Value): {},
}
if entry.Values != nil {
for _, value := range entry.Values.Values {
switch (value).(type) {
case ast.AliasValueUser:
userValue := value.(ast.AliasValueUser)
users[indexes.NormalizeKey(userValue.Value)] = struct{}{}
}
}
}
return users
}

View File

@ -0,0 +1,39 @@
package handlers
import (
"config-lsp/handlers/aliases/ast"
"slices"
)
func GetValueAtCursor(
cursor uint32,
entry *ast.AliasEntry,
) *ast.AliasValueInterface {
if entry.Values == nil || len(entry.Values.Values) == 0 {
return nil
}
index, found := slices.BinarySearchFunc(
entry.Values.Values,
cursor,
func(entry ast.AliasValueInterface, pos uint32) int {
value := entry.GetAliasValue()
if pos > value.Location.End.Character {
return -1
}
if pos < value.Location.Start.Character {
return 1
}
return 0
},
)
if !found {
return nil
}
return &entry.Values.Values[index]
}

View File

@ -0,0 +1,94 @@
package handlers
import (
"config-lsp/common"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/indexes"
"config-lsp/utils"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func GetDefinitionLocationForValue(
i indexes.AliasesIndexes,
value ast.AliasValueInterface,
documentURI string,
) []protocol.Location {
switch value.(type) {
case ast.AliasValueUser:
userValue := value.(ast.AliasValueUser)
// Own defined alias
if entry, found := i.Keys[indexes.NormalizeKey(userValue.Value)]; found {
return []protocol.Location{
{
URI: documentURI,
Range: entry.Key.Location.ToLSPRange(),
},
}
}
// System user
systemUsers, _ := getSystemUserMap()
if user, found := systemUsers[userValue.Value]; found {
return []protocol.Location{
{
URI: "file:///etc/passwd",
Range: protocol.Range{
Start: protocol.Position{
Line: user.Line,
Character: 0,
},
End: protocol.Position{
Line: user.Line,
Character: uint32(len(user.Name)),
},
},
},
}
}
case ast.AliasValueFile:
fileValue := value.(ast.AliasValueFile)
path := string(fileValue.Path)
if utils.DoesPathExist(path) {
return []protocol.Location{
{
URI: "file://" + path,
},
}
}
case ast.AliasValueInclude:
includeValue := value.(ast.AliasValueInclude)
if includeValue.Path != nil {
path := string(includeValue.Path.Path)
if utils.DoesPathExist(path) {
return []protocol.Location{
{
URI: "file://" + path,
},
}
}
}
}
return nil
}
func getSystemUserMap() (map[string]common.PasswdInfo, error) {
users, err := common.FetchPasswdInfo()
if err != nil {
return nil, err
}
userMap := make(map[string]common.PasswdInfo)
for _, user := range users {
userMap[user.Name] = user
}
return userMap, nil
}

View File

@ -0,0 +1,54 @@
package handlers
import (
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/indexes"
"config-lsp/utils"
"testing"
)
func TestGoToDefinitionSimpleExample(
t *testing.T,
) {
input := utils.Dedent(`
alice: root
bob: root
steve: alice@example.com, bob
david: alice
`)
parser := ast.NewAliasesParser()
errors := parser.Parse(input)
if len(errors) > 0 {
t.Fatalf("Unexpected errors: %v", errors)
}
i, errors := indexes.CreateIndexes(parser)
if len(errors) > 0 {
t.Fatalf("Expected no errors, but got: %v", errors)
}
rawEntry, _ := parser.Aliases.Get(uint32(3))
entry := rawEntry.(*ast.AliasEntry)
rawValue := entry.Values.Values[0]
value := rawValue.(ast.AliasValueUser)
locations := GetDefinitionLocationForValue(
i,
value,
"file:///etc/aliases",
)
if !(len(locations) == 1) {
t.Errorf("Expected 1 location, but got %v", len(locations))
}
if !(locations[0].URI == "file:///etc/aliases") {
t.Errorf("Unexpected location: %v", locations[0])
}
if !(locations[0].Range.Start.Line == 0 && locations[0].Range.Start.Character == 0 && locations[0].Range.End.Line == 0 && locations[0].Range.End.Character == 5) {
t.Errorf("Unexpected location: %v", locations[0])
}
}

View File

@ -0,0 +1,160 @@
package handlers
import (
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/fields"
"config-lsp/handlers/aliases/indexes"
"config-lsp/utils"
"fmt"
"strings"
)
// Get hover information for an alias entry
// Expects `entry` to contain at least a key
func GetAliasHoverInfo(
i indexes.AliasesIndexes,
entry ast.AliasEntry,
) string {
header := []string{
fmt.Sprintf("Emails targeted for `%s` will be passed to:", entry.Key.Value),
"",
}
var forwards []string
if entry.Values == nil {
forwards = []string{
"No forwards configured",
}
} else {
if len(entry.Values.Values) == 1 {
forwards = []string{
GetAliasValueHoverInfo(
i,
entry.Values.Values[0],
),
}
} else {
forwards = utils.Map(
entry.Values.Values,
func(value ast.AliasValueInterface) string {
return fmt.Sprintf(
"* %s",
GetAliasValueHoverInfo(
i,
value,
),
)
},
)
}
}
content := append(header, forwards...)
return strings.Join(
content,
"\n",
)
}
func GetAliasValueHoverInfo(
i indexes.AliasesIndexes,
value ast.AliasValueInterface,
) string {
switch value.(type) {
case ast.AliasValueUser:
return fmt.Sprintf("User: **%s**", value.GetAliasValue().Value)
case ast.AliasValueEmail:
return fmt.Sprintf("Email: **%s**", value.GetAliasValue().Value)
case ast.AliasValueInclude:
includeValue := value.(ast.AliasValueInclude)
return fmt.Sprintf("Included file: `%s`", string(includeValue.Path.Path))
case ast.AliasValueFile:
fileValue := value.(ast.AliasValueFile)
return fmt.Sprintf("File: Email will be written to `%s`", string(fileValue.Path))
case ast.AliasValueCommand:
commandValue := value.(ast.AliasValueCommand)
return fmt.Sprintf("Command: Will be passed as stdin to `%s`", commandValue.Command)
case ast.AliasValueError:
errorValue := value.(ast.AliasValueError)
if errorValue.Code == nil || errorValue.Message == nil {
return "Error: An error will show up"
}
return fmt.Sprintf(
"Error: An error will show up; code: **%s** (%s), message: '%s'",
errorValue.Code.Value,
getErrorCodeInfo(errorValue.Code.ErrorCodeAsInt()),
errorValue.Message.Value,
)
}
panic("Unknown value type")
}
func GetAliasValueTypeInfo(
value ast.AliasValueInterface,
) []string {
println(fmt.Sprintf("value: %v, value type: %T", value, value))
switch value.(type) {
case ast.AliasValueUser:
return []string{
"### User",
fields.UserDeclaration,
"",
fields.UserField.Documentation,
}
case ast.AliasValueEmail:
return []string{
"### Email",
fields.EmailDeclaration,
"",
fields.EmailField.Documentation,
}
case ast.AliasValueInclude:
return []string{
"### Include",
fields.IncludeDeclaration,
"",
fields.IncludeField.Documentation,
}
case ast.AliasValueFile:
return []string{
"### File",
fields.PathDeclaration,
"",
fields.PathField.Documentation,
}
case ast.AliasValueCommand:
return []string{
"### Command",
fields.CommandDeclaration,
"",
fields.CommandField.Documentation,
}
case ast.AliasValueError:
return []string{
"### Error",
fields.ErrorDeclaration,
"",
fields.ErrorMessageField.Documentation,
}
}
panic("Unknown value type")
}
func getErrorCodeInfo(
code uint16,
) string {
if code >= 400 && code <= 499 {
return "4XX: TempFail"
}
if code >= 500 && code <= 599 {
return "5XX: PermFail"
}
return "Unknown code"
}

View File

@ -0,0 +1,33 @@
package handlers
import (
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/indexes"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func RenameAlias(
i indexes.AliasesIndexes,
oldEntry *ast.AliasEntry,
newName string,
) []protocol.TextEdit {
occurrences := i.UserOccurrences[indexes.NormalizeKey(oldEntry.Key.Value)]
changes := make([]protocol.TextEdit, 0, len(occurrences))
// Own rename
changes = append(changes, protocol.TextEdit{
Range: oldEntry.Key.Location.ToLSPRange(),
NewText: newName,
})
// Other AliasValueUser occurrences
for _, value := range occurrences {
changes = append(changes, protocol.TextEdit{
Range: value.Location.ToLSPRange(),
NewText: newName,
})
}
return changes
}

View File

@ -0,0 +1,48 @@
package handlers
import (
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/indexes"
"config-lsp/utils"
"testing"
)
func TestRenameSimpleExample(
t *testing.T,
) {
input := utils.Dedent(`
alice: alice
bob: root
support: alice, bob
`)
parser := ast.NewAliasesParser()
errors := parser.Parse(input)
if len(errors) > 0 {
t.Fatalf("Unexpected errors: %v", errors)
}
i, errors := indexes.CreateIndexes(parser)
if len(errors) > 0 {
t.Fatalf("Expected no errors, but got: %v", errors)
}
edits := RenameAlias(i, i.Keys["alice"], "amelie")
if !(len(edits) == 3) {
t.Errorf("Expected 2 edits, but got %v", len(edits))
}
if !(edits[0].Range.Start.Line == 0 && edits[0].Range.Start.Character == 0 && edits[0].Range.End.Line == 0 && edits[0].Range.End.Character == 5) {
t.Errorf("Unexpected edit: %v", edits[0])
}
if !(edits[1].Range.Start.Line == 0 && edits[1].Range.Start.Character == 7 && edits[1].Range.End.Line == 0 && edits[1].Range.End.Character == 12) {
t.Errorf("Unexpected edit: %v", edits[1])
}
if !(edits[2].Range.Start.Line == 2 && edits[2].Range.Start.Character == 9 && edits[2].Range.End.Line == 2 && edits[2].Range.End.Character == 14) {
t.Errorf("Unexpected edit: %v", edits[2])
}
}

View File

@ -0,0 +1,309 @@
package handlers
import (
"config-lsp/handlers/aliases/ast"
"strings"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func GetRootSignatureHelp(
activeParameter uint32,
) *protocol.SignatureHelp {
index := uint32(0)
return &protocol.SignatureHelp{
ActiveSignature: &index,
Signatures: []protocol.SignatureInformation{
{
Label: "<alias>: <value1>, <value2>, ...",
ActiveParameter: &activeParameter,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<alias>") + 1),
},
Documentation: "The alias to define",
},
{
Label: []uint32{
uint32(len("<alias>:")),
uint32(len("<alias>:") + len("<value1>")),
},
Documentation: "A value to associate with the alias",
},
},
},
},
}
}
func GetAllValuesSignatureHelp() *protocol.SignatureHelp {
index := uint32(0)
return &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{
{
Label: "<user>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<user>")),
},
},
},
},
{
Label: "<user>@<host>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<user>")),
},
},
{
Label: []uint32{
uint32(len("<user>@")),
uint32(len("<user>@<host>")),
},
},
},
},
{
Label: "<file>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<file>")),
},
},
},
},
{
Label: ":include:<file>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len(":include:")),
},
},
},
},
{
Label: "|<command>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
1,
},
},
},
},
{
Label: "error:<code> <message>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("error")),
},
},
},
},
},
}
}
func GetValueSignatureHelp(
value ast.AliasValueInterface,
cursor uint32,
) *protocol.SignatureHelp {
switch value.(type) {
case ast.AliasValueUser:
index := uint32(0)
return &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{
{
Label: "<user>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<user>")),
},
},
},
},
{
Label: "<user>@<host>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<user>")),
},
},
{
Label: []uint32{
uint32(len("<user>@")),
uint32(len("<user>@") + len("<host>")),
},
},
},
},
},
}
case ast.AliasValueEmail:
isBeforeAtSymbol := cursor <= uint32(strings.Index(value.GetAliasValue().Value, "@"))
var index uint32
if isBeforeAtSymbol {
index = 0
} else {
index = 1
}
return &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{
{
Label: "<user>@<host>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<user>")),
},
},
{
Label: []uint32{
uint32(len("<user>@")),
uint32(len("<user>@") + len("<host>")),
},
},
},
},
},
}
case ast.AliasValueFile:
index := uint32(0)
return &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{
{
Label: "<file>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("<file>")),
},
},
},
},
},
}
case ast.AliasValueInclude:
index := uint32(0)
return &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{
{
Label: "include:<file>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
uint32(len("include:")),
uint32(len("include:<file>")),
},
},
},
},
},
}
case ast.AliasValueCommand:
var index uint32
if cursor == 0 {
index = 0
} else {
index = 1
}
return &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{
{
Label: "|<command>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
1,
},
},
{
Label: []uint32{
1,
uint32(1 + len("<command>")),
},
},
},
},
},
}
case ast.AliasValueError:
errorValue := value.(ast.AliasValueError)
var index uint32
if errorValue.Code == nil || cursor <= errorValue.Code.Location.End.Character {
index = 1
} else {
index = 2
}
return &protocol.SignatureHelp{
Signatures: []protocol.SignatureInformation{
{
Label: "error:<code> <message>",
ActiveParameter: &index,
Parameters: []protocol.ParameterInformation{
{
Label: []uint32{
0,
uint32(len("error:")),
},
},
{
Label: []uint32{
uint32(len("error:")),
uint32(len("error:<code>")),
},
},
{
Label: []uint32{
uint32(len("error:<code> ")),
uint32(len("error:<code> <message>")),
},
},
},
},
},
}
}
return nil
}

View File

@ -0,0 +1,67 @@
package indexes
import (
"config-lsp/common"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/shared"
"strings"
)
type AliasesIndexes struct {
Keys map[string]*ast.AliasEntry
UserOccurrences map[string][]*ast.AliasValueUser
}
func NormalizeKey(key string) string {
return strings.ToLower(key)
}
func CreateIndexes(parser ast.AliasesParser) (AliasesIndexes, []common.LSPError) {
errors := make([]common.LSPError, 0)
indexes := &AliasesIndexes{
Keys: make(map[string]*ast.AliasEntry),
UserOccurrences: make(map[string][]*ast.AliasValueUser),
}
it := parser.Aliases.Iterator()
for it.Next() {
entry := it.Value().(*ast.AliasEntry)
if entry.Values != nil {
for _, value := range entry.Values.Values {
switch value.(type) {
case ast.AliasValueUser:
userValue := value.(ast.AliasValueUser)
indexes.UserOccurrences[userValue.Value] = append(
indexes.UserOccurrences[userValue.Value],
&userValue,
)
}
}
}
if entry.Key == nil || entry.Key.Value == "" {
continue
}
normalizedAlias := NormalizeKey(entry.Key.Value)
if existingEntry, found := indexes.Keys[normalizedAlias]; found {
errors = append(errors, common.LSPError{
Range: entry.Key.Location,
Err: shared.DuplicateKeyEntry{
AlreadyFoundAt: existingEntry.Location.Start.Line,
Key: entry.Key.Value,
},
})
continue
}
indexes.Keys[normalizedAlias] = entry
}
return *indexes, errors
}

View File

@ -0,0 +1,49 @@
package indexes
import (
"config-lsp/handlers/aliases/ast"
"config-lsp/utils"
"testing"
)
func TestComplexExample(
t *testing.T,
) {
input := utils.Dedent(`
postmaster: alice, bob
alice: root
bob: root
`)
parser := ast.NewAliasesParser()
errors := parser.Parse(input)
if len(errors) > 0 {
t.Fatalf("Unexpected errors: %v", errors)
}
indexes, errors := CreateIndexes(parser)
if len(errors) > 0 {
t.Fatalf("Expected no errors, but got: %v", errors)
}
if !(len(indexes.Keys) == 3) {
t.Errorf("Expected 3 keys, but got %v", len(indexes.Keys))
}
if !(len(indexes.UserOccurrences) == 3) {
t.Errorf("Expected 3 user occurrences, but got %v", len(indexes.UserOccurrences))
}
if !(len(indexes.UserOccurrences["root"]) == 2) {
t.Errorf("Expected 2 occurrences of root, but got %v", len(indexes.UserOccurrences["root"]))
}
if !(len(indexes.UserOccurrences["alice"]) == 1) {
t.Errorf("Expected 1 occurrence of alice, but got %v", len(indexes.UserOccurrences["alice"]))
}
if !(len(indexes.UserOccurrences["bob"]) == 1) {
t.Errorf("Expected 1 occurrence of bob, but got %v", len(indexes.UserOccurrences["bob"]))
}
}

View File

@ -0,0 +1,14 @@
package lsp
import (
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
// document := hosts.DocumentParserMap[params.TextDocument.URI]
//
// actions := make([]protocol.CodeAction, 0, 1)
return nil, nil
}

View File

@ -0,0 +1,51 @@
package lsp
import (
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) {
d := aliases.DocumentParserMap[params.TextDocument.URI]
cursor := params.Position.Character
line := params.Position.Line
if _, found := d.Parser.CommentLines[line]; found {
return nil, nil
}
rawEntry, found := d.Parser.Aliases.Get(line)
if !found {
// For the key there are no completions available
return handlers.GetAliasesCompletions(d.Indexes), nil
}
entry := rawEntry.(*ast.AliasEntry)
if entry.Key == nil {
return handlers.GetAliasesCompletions(d.Indexes), nil
}
if cursor >= entry.Key.Location.Start.Character && cursor <= entry.Key.Location.End.Character {
return handlers.GetAliasesCompletions(d.Indexes), nil
}
if entry.Separator == nil && cursor > entry.Key.Location.End.Character {
return nil, nil
}
if cursor > entry.Separator.End.Character {
return handlers.GetCompletionsForEntry(
cursor,
entry,
d.Indexes,
)
}
return nil, nil
}

View File

@ -0,0 +1,40 @@
package lsp
import (
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentDefinition(context *glsp.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) {
d := aliases.DocumentParserMap[params.TextDocument.URI]
character := params.Position.Character
line := params.Position.Line
rawEntry, found := d.Parser.Aliases.Get(line)
if !found {
return nil, nil
}
entry := rawEntry.(*ast.AliasEntry)
if entry.Values != nil && character >= entry.Values.Location.Start.Character && character <= entry.Values.Location.End.Character {
rawValue := handlers.GetValueAtCursor(character, entry)
if rawValue == nil {
return nil, nil
}
return handlers.GetDefinitionLocationForValue(
*d.Indexes,
*rawValue,
params.TextDocument.URI,
), nil
}
return nil, nil
}

View File

@ -0,0 +1,41 @@
package lsp
import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/analyzer"
"config-lsp/utils"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentDidChange(
context *glsp.Context,
params *protocol.DidChangeTextDocumentParams,
) error {
content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text
common.ClearDiagnostics(context, params.TextDocument.URI)
document := aliases.DocumentParserMap[params.TextDocument.URI]
document.Parser.Clear()
diagnostics := make([]protocol.Diagnostic, 0)
errors := document.Parser.Parse(content)
if len(errors) > 0 {
diagnostics = append(diagnostics, utils.Map(
errors,
func(err common.LSPError) protocol.Diagnostic {
return err.ToDiagnostic()
},
)...)
}
diagnostics = append(diagnostics, analyzer.Analyze(document)...)
if len(diagnostics) > 0 {
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
}
return nil
}

View File

@ -0,0 +1,13 @@
package lsp
import (
"config-lsp/handlers/hosts"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentDidClose(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
delete(hosts.DocumentParserMap, params.TextDocument.URI)
return nil
}

View File

@ -0,0 +1,44 @@
package lsp
import (
"config-lsp/common"
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/analyzer"
"config-lsp/handlers/aliases/ast"
"config-lsp/utils"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentDidOpen(
context *glsp.Context,
params *protocol.DidOpenTextDocumentParams,
) error {
common.ClearDiagnostics(context, params.TextDocument.URI)
parser := ast.NewAliasesParser()
document := aliases.AliasesDocument{
Parser: &parser,
}
aliases.DocumentParserMap[params.TextDocument.URI] = &document
errors := parser.Parse(params.TextDocument.Text)
diagnostics := utils.Map(
errors,
func(err common.LSPError) protocol.Diagnostic {
return err.ToDiagnostic()
},
)
diagnostics = append(
diagnostics,
analyzer.Analyze(&document)...,
)
if len(diagnostics) > 0 {
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
}
return nil
}

View File

@ -0,0 +1,64 @@
package lsp
import (
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
"strings"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentHover(
context *glsp.Context,
params *protocol.HoverParams,
) (*protocol.Hover, error) {
document := aliases.DocumentParserMap[params.TextDocument.URI]
line := params.Position.Line
character := params.Position.Character
if _, found := document.Parser.CommentLines[line]; found {
// Comment
return nil, nil
}
rawEntry, found := document.Parser.Aliases.Get(line)
if !found {
return nil, nil
}
entry := rawEntry.(*ast.AliasEntry)
if entry.Key != nil && character >= entry.Key.Location.Start.Character && character <= entry.Key.Location.End.Character {
text := handlers.GetAliasHoverInfo(*document.Indexes, *entry)
return &protocol.Hover{
Contents: text,
}, nil
}
if entry.Values != nil && character >= entry.Values.Location.Start.Character && character <= entry.Values.Location.End.Character {
value := handlers.GetValueAtCursor(character, entry)
if value == nil {
return nil, nil
}
contents := []string{}
contents = append(contents, handlers.GetAliasValueTypeInfo(*value)...)
contents = append(contents, "")
contents = append(contents, "#### Value")
contents = append(contents, handlers.GetAliasValueHoverInfo(*document.Indexes, *value))
text := strings.Join(contents, "\n")
return &protocol.Hover{
Contents: text,
}, nil
}
return nil, nil
}

View File

@ -0,0 +1,45 @@
package lsp
import (
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentPrepareRename(context *glsp.Context, params *protocol.PrepareRenameParams) (any, error) {
d := aliases.DocumentParserMap[params.TextDocument.URI]
character := params.Position.Character
line := params.Position.Line
rawEntry, found := d.Parser.Aliases.Get(line)
if !found {
return nil, nil
}
entry := rawEntry.(*ast.AliasEntry)
if character >= entry.Key.Location.Start.Character && character <= entry.Key.Location.End.Character {
return entry.Key.Location.ToLSPRange(), nil
}
if entry.Values != nil && character >= entry.Values.Location.Start.Character && character <= entry.Values.Location.End.Character {
rawValue := handlers.GetValueAtCursor(character, entry)
if rawValue == nil {
return nil, nil
}
switch (*rawValue).(type) {
case ast.AliasValueUser:
userValue := (*rawValue).(ast.AliasValueUser)
return userValue.Location.ToLSPRange(), nil
}
}
return nil, nil
}

View File

@ -0,0 +1,60 @@
package lsp
import (
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
"config-lsp/handlers/aliases/indexes"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentRename(context *glsp.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) {
d := aliases.DocumentParserMap[params.TextDocument.URI]
character := params.Position.Character
line := params.Position.Line
rawEntry, found := d.Parser.Aliases.Get(line)
if !found {
return nil, nil
}
entry := rawEntry.(*ast.AliasEntry)
if character >= entry.Key.Location.Start.Character && character <= entry.Key.Location.End.Character {
changes := handlers.RenameAlias(*d.Indexes, entry, params.NewName)
return &protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
params.TextDocument.URI: changes,
},
}, nil
}
if entry.Values != nil && character >= entry.Values.Location.Start.Character && character <= entry.Values.Location.End.Character {
rawValue := handlers.GetValueAtCursor(character, entry)
if rawValue == nil {
return nil, nil
}
switch (*rawValue).(type) {
case ast.AliasValueUser:
userValue := (*rawValue).(ast.AliasValueUser)
definitionEntry := d.Indexes.Keys[indexes.NormalizeKey(userValue.Value)]
changes := handlers.RenameAlias(*d.Indexes, definitionEntry, params.NewName)
return &protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
params.TextDocument.URI: changes,
},
}, nil
}
}
return nil, nil
}

View File

@ -0,0 +1,52 @@
package lsp
import (
"config-lsp/handlers/aliases"
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/handlers"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentSignatureHelp(context *glsp.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) {
document := aliases.DocumentParserMap[params.TextDocument.URI]
line := params.Position.Line
character := params.Position.Character
if _, found := document.Parser.CommentLines[line]; found {
// Comment
return nil, nil
}
rawEntry, found := document.Parser.Aliases.Get(line)
if !found {
return handlers.GetRootSignatureHelp(0), nil
}
entry := rawEntry.(*ast.AliasEntry)
if entry.Key != nil && character >= entry.Key.Location.Start.Character && character <= entry.Key.Location.End.Character {
return handlers.GetRootSignatureHelp(0), nil
}
if entry.Values != nil && character >= entry.Values.Location.Start.Character && character <= entry.Values.Location.End.Character {
value := handlers.GetValueAtCursor(character, entry)
if value == nil {
// For some reason, this does not really work,
// When we return all, and then a user value is entered
// and the `GetValueSignatureHelp` is called, still the old
// signatures with all signature are shown
// return handlers.GetAllValuesSignatureHelp(), nil
return nil, nil
}
return handlers.GetValueSignatureHelp(*value, character), nil
}
return nil, nil
}

View File

@ -0,0 +1,21 @@
package lsp
import (
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func WorkspaceExecuteCommand(context *glsp.Context, params *protocol.ExecuteCommandParams) (*protocol.ApplyWorkspaceEditParams, error) {
// _, command, _ := strings.Cut(params.Command, ".")
//
// switch command {
// case string(handlers.CodeActionInlineAliases):
// args := handlers.CodeActionInlineAliasesArgsFromArguments(params.Arguments[0].(map[string]any))
//
// document := hosts.DocumentParserMap[args.URI]
//
// return args.RunCommand(*document.Parser)
// }
return nil, nil
}

View File

@ -0,0 +1,48 @@
token literal names:
null
null
null
null
'@'
null
'|'
':'
','
'#'
'/'
null
token symbolic names:
null
DIGITS
ERROR
SEPARATOR
AT
INCLUDE
VERTLINE
COLON
COMMA
NUMBER_SIGN
SLASH
STRING
rule names:
lineStatement
entry
separator
key
values
value
user
file
command
include
comment
email
error
errorCode
errorMessage
atn:
[4, 1, 11, 131, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 1, 0, 1, 0, 1, 0, 1, 1, 3, 1, 35, 8, 1, 1, 1, 1, 1, 3, 1, 39, 8, 1, 1, 1, 1, 1, 3, 1, 43, 8, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 5, 4, 55, 8, 4, 10, 4, 12, 4, 58, 9, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 68, 8, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 5, 7, 75, 8, 7, 10, 7, 12, 7, 78, 9, 7, 1, 7, 3, 7, 81, 8, 7, 1, 8, 1, 8, 3, 8, 85, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 91, 8, 9, 1, 10, 1, 10, 3, 10, 95, 8, 10, 1, 10, 4, 10, 98, 8, 10, 11, 10, 12, 10, 99, 1, 10, 3, 10, 103, 8, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 3, 12, 112, 8, 12, 1, 12, 3, 12, 115, 8, 12, 1, 12, 3, 12, 118, 8, 12, 1, 13, 1, 13, 1, 14, 1, 14, 5, 14, 124, 8, 14, 10, 14, 12, 14, 127, 9, 14, 1, 14, 1, 14, 1, 14, 0, 0, 15, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 0, 0, 135, 0, 30, 1, 0, 0, 0, 2, 34, 1, 0, 0, 0, 4, 46, 1, 0, 0, 0, 6, 48, 1, 0, 0, 0, 8, 56, 1, 0, 0, 0, 10, 67, 1, 0, 0, 0, 12, 69, 1, 0, 0, 0, 14, 71, 1, 0, 0, 0, 16, 82, 1, 0, 0, 0, 18, 86, 1, 0, 0, 0, 20, 92, 1, 0, 0, 0, 22, 104, 1, 0, 0, 0, 24, 108, 1, 0, 0, 0, 26, 119, 1, 0, 0, 0, 28, 125, 1, 0, 0, 0, 30, 31, 3, 2, 1, 0, 31, 32, 5, 0, 0, 1, 32, 1, 1, 0, 0, 0, 33, 35, 5, 3, 0, 0, 34, 33, 1, 0, 0, 0, 34, 35, 1, 0, 0, 0, 35, 36, 1, 0, 0, 0, 36, 38, 3, 6, 3, 0, 37, 39, 5, 3, 0, 0, 38, 37, 1, 0, 0, 0, 38, 39, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 42, 3, 4, 2, 0, 41, 43, 5, 3, 0, 0, 42, 41, 1, 0, 0, 0, 42, 43, 1, 0, 0, 0, 43, 44, 1, 0, 0, 0, 44, 45, 3, 8, 4, 0, 45, 3, 1, 0, 0, 0, 46, 47, 5, 7, 0, 0, 47, 5, 1, 0, 0, 0, 48, 49, 5, 11, 0, 0, 49, 7, 1, 0, 0, 0, 50, 51, 3, 10, 5, 0, 51, 52, 5, 8, 0, 0, 52, 53, 5, 3, 0, 0, 53, 55, 1, 0, 0, 0, 54, 50, 1, 0, 0, 0, 55, 58, 1, 0, 0, 0, 56, 54, 1, 0, 0, 0, 56, 57, 1, 0, 0, 0, 57, 59, 1, 0, 0, 0, 58, 56, 1, 0, 0, 0, 59, 60, 3, 10, 5, 0, 60, 9, 1, 0, 0, 0, 61, 68, 3, 12, 6, 0, 62, 68, 3, 14, 7, 0, 63, 68, 3, 16, 8, 0, 64, 68, 3, 18, 9, 0, 65, 68, 3, 22, 11, 0, 66, 68, 3, 24, 12, 0, 67, 61, 1, 0, 0, 0, 67, 62, 1, 0, 0, 0, 67, 63, 1, 0, 0, 0, 67, 64, 1, 0, 0, 0, 67, 65, 1, 0, 0, 0, 67, 66, 1, 0, 0, 0, 68, 11, 1, 0, 0, 0, 69, 70, 5, 11, 0, 0, 70, 13, 1, 0, 0, 0, 71, 76, 5, 10, 0, 0, 72, 73, 5, 11, 0, 0, 73, 75, 5, 10, 0, 0, 74, 72, 1, 0, 0, 0, 75, 78, 1, 0, 0, 0, 76, 74, 1, 0, 0, 0, 76, 77, 1, 0, 0, 0, 77, 80, 1, 0, 0, 0, 78, 76, 1, 0, 0, 0, 79, 81, 5, 11, 0, 0, 80, 79, 1, 0, 0, 0, 80, 81, 1, 0, 0, 0, 81, 15, 1, 0, 0, 0, 82, 84, 5, 6, 0, 0, 83, 85, 5, 11, 0, 0, 84, 83, 1, 0, 0, 0, 84, 85, 1, 0, 0, 0, 85, 17, 1, 0, 0, 0, 86, 87, 5, 7, 0, 0, 87, 88, 5, 5, 0, 0, 88, 90, 5, 7, 0, 0, 89, 91, 3, 14, 7, 0, 90, 89, 1, 0, 0, 0, 90, 91, 1, 0, 0, 0, 91, 19, 1, 0, 0, 0, 92, 97, 5, 9, 0, 0, 93, 95, 5, 3, 0, 0, 94, 93, 1, 0, 0, 0, 94, 95, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 98, 5, 11, 0, 0, 97, 94, 1, 0, 0, 0, 98, 99, 1, 0, 0, 0, 99, 97, 1, 0, 0, 0, 99, 100, 1, 0, 0, 0, 100, 102, 1, 0, 0, 0, 101, 103, 5, 3, 0, 0, 102, 101, 1, 0, 0, 0, 102, 103, 1, 0, 0, 0, 103, 21, 1, 0, 0, 0, 104, 105, 5, 11, 0, 0, 105, 106, 5, 4, 0, 0, 106, 107, 5, 11, 0, 0, 107, 23, 1, 0, 0, 0, 108, 109, 5, 2, 0, 0, 109, 111, 5, 7, 0, 0, 110, 112, 3, 26, 13, 0, 111, 110, 1, 0, 0, 0, 111, 112, 1, 0, 0, 0, 112, 114, 1, 0, 0, 0, 113, 115, 5, 3, 0, 0, 114, 113, 1, 0, 0, 0, 114, 115, 1, 0, 0, 0, 115, 117, 1, 0, 0, 0, 116, 118, 3, 28, 14, 0, 117, 116, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 25, 1, 0, 0, 0, 119, 120, 5, 1, 0, 0, 120, 27, 1, 0, 0, 0, 121, 122, 5, 11, 0, 0, 122, 124, 5, 3, 0, 0, 123, 121, 1, 0, 0, 0, 124, 127, 1, 0, 0, 0, 125, 123, 1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 128, 1, 0, 0, 0, 127, 125, 1, 0, 0, 0, 128, 129, 5, 11, 0, 0, 129, 29, 1, 0, 0, 0, 16, 34, 38, 42, 56, 67, 76, 80, 84, 90, 94, 99, 102, 111, 114, 117, 125]

View File

@ -0,0 +1,17 @@
DIGITS=1
ERROR=2
SEPARATOR=3
AT=4
INCLUDE=5
VERTLINE=6
COLON=7
COMMA=8
NUMBER_SIGN=9
SLASH=10
STRING=11
'@'=4
'|'=6
':'=7
','=8
'#'=9
'/'=10

View File

@ -0,0 +1,50 @@
token literal names:
null
null
null
null
'@'
null
'|'
':'
','
'#'
'/'
null
token symbolic names:
null
DIGITS
ERROR
SEPARATOR
AT
INCLUDE
VERTLINE
COLON
COMMA
NUMBER_SIGN
SLASH
STRING
rule names:
DIGITS
ERROR
SEPARATOR
AT
INCLUDE
VERTLINE
COLON
COMMA
NUMBER_SIGN
SLASH
STRING
channel names:
DEFAULT_TOKEN_CHANNEL
HIDDEN
mode names:
DEFAULT_MODE
atn:
[4, 0, 11, 64, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 1, 0, 4, 0, 25, 8, 0, 11, 0, 12, 0, 26, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 4, 2, 36, 8, 2, 11, 2, 12, 2, 37, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 4, 10, 61, 8, 10, 11, 10, 12, 10, 62, 0, 0, 11, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 1, 0, 3, 1, 0, 48, 57, 2, 0, 9, 9, 32, 32, 9, 0, 9, 10, 13, 13, 32, 32, 35, 35, 44, 44, 47, 47, 58, 58, 64, 64, 124, 124, 66, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 1, 24, 1, 0, 0, 0, 3, 28, 1, 0, 0, 0, 5, 35, 1, 0, 0, 0, 7, 39, 1, 0, 0, 0, 9, 41, 1, 0, 0, 0, 11, 49, 1, 0, 0, 0, 13, 51, 1, 0, 0, 0, 15, 53, 1, 0, 0, 0, 17, 55, 1, 0, 0, 0, 19, 57, 1, 0, 0, 0, 21, 60, 1, 0, 0, 0, 23, 25, 7, 0, 0, 0, 24, 23, 1, 0, 0, 0, 25, 26, 1, 0, 0, 0, 26, 24, 1, 0, 0, 0, 26, 27, 1, 0, 0, 0, 27, 2, 1, 0, 0, 0, 28, 29, 5, 101, 0, 0, 29, 30, 5, 114, 0, 0, 30, 31, 5, 114, 0, 0, 31, 32, 5, 111, 0, 0, 32, 33, 5, 114, 0, 0, 33, 4, 1, 0, 0, 0, 34, 36, 7, 1, 0, 0, 35, 34, 1, 0, 0, 0, 36, 37, 1, 0, 0, 0, 37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 6, 1, 0, 0, 0, 39, 40, 5, 64, 0, 0, 40, 8, 1, 0, 0, 0, 41, 42, 5, 105, 0, 0, 42, 43, 5, 110, 0, 0, 43, 44, 5, 99, 0, 0, 44, 45, 5, 108, 0, 0, 45, 46, 5, 117, 0, 0, 46, 47, 5, 100, 0, 0, 47, 48, 5, 101, 0, 0, 48, 10, 1, 0, 0, 0, 49, 50, 5, 124, 0, 0, 50, 12, 1, 0, 0, 0, 51, 52, 5, 58, 0, 0, 52, 14, 1, 0, 0, 0, 53, 54, 5, 44, 0, 0, 54, 16, 1, 0, 0, 0, 55, 56, 5, 35, 0, 0, 56, 18, 1, 0, 0, 0, 57, 58, 5, 47, 0, 0, 58, 20, 1, 0, 0, 0, 59, 61, 8, 2, 0, 0, 60, 59, 1, 0, 0, 0, 61, 62, 1, 0, 0, 0, 62, 60, 1, 0, 0, 0, 62, 63, 1, 0, 0, 0, 63, 22, 1, 0, 0, 0, 4, 0, 26, 37, 62, 0]

View File

@ -0,0 +1,17 @@
DIGITS=1
ERROR=2
SEPARATOR=3
AT=4
INCLUDE=5
VERTLINE=6
COLON=7
COMMA=8
NUMBER_SIGN=9
SLASH=10
STRING=11
'@'=4
'|'=6
':'=7
','=8
'#'=9
'/'=10

View File

@ -0,0 +1,112 @@
// Code generated from Aliases.g4 by ANTLR 4.13.0. DO NOT EDIT.
package parser // Aliases
import "github.com/antlr4-go/antlr/v4"
// BaseAliasesListener is a complete listener for a parse tree produced by AliasesParser.
type BaseAliasesListener struct{}
var _ AliasesListener = &BaseAliasesListener{}
// VisitTerminal is called when a terminal node is visited.
func (s *BaseAliasesListener) VisitTerminal(node antlr.TerminalNode) {}
// VisitErrorNode is called when an error node is visited.
func (s *BaseAliasesListener) VisitErrorNode(node antlr.ErrorNode) {}
// EnterEveryRule is called when any rule is entered.
func (s *BaseAliasesListener) EnterEveryRule(ctx antlr.ParserRuleContext) {}
// ExitEveryRule is called when any rule is exited.
func (s *BaseAliasesListener) ExitEveryRule(ctx antlr.ParserRuleContext) {}
// EnterLineStatement is called when production lineStatement is entered.
func (s *BaseAliasesListener) EnterLineStatement(ctx *LineStatementContext) {}
// ExitLineStatement is called when production lineStatement is exited.
func (s *BaseAliasesListener) ExitLineStatement(ctx *LineStatementContext) {}
// EnterEntry is called when production entry is entered.
func (s *BaseAliasesListener) EnterEntry(ctx *EntryContext) {}
// ExitEntry is called when production entry is exited.
func (s *BaseAliasesListener) ExitEntry(ctx *EntryContext) {}
// EnterSeparator is called when production separator is entered.
func (s *BaseAliasesListener) EnterSeparator(ctx *SeparatorContext) {}
// ExitSeparator is called when production separator is exited.
func (s *BaseAliasesListener) ExitSeparator(ctx *SeparatorContext) {}
// EnterKey is called when production key is entered.
func (s *BaseAliasesListener) EnterKey(ctx *KeyContext) {}
// ExitKey is called when production key is exited.
func (s *BaseAliasesListener) ExitKey(ctx *KeyContext) {}
// EnterValues is called when production values is entered.
func (s *BaseAliasesListener) EnterValues(ctx *ValuesContext) {}
// ExitValues is called when production values is exited.
func (s *BaseAliasesListener) ExitValues(ctx *ValuesContext) {}
// EnterValue is called when production value is entered.
func (s *BaseAliasesListener) EnterValue(ctx *ValueContext) {}
// ExitValue is called when production value is exited.
func (s *BaseAliasesListener) ExitValue(ctx *ValueContext) {}
// EnterUser is called when production user is entered.
func (s *BaseAliasesListener) EnterUser(ctx *UserContext) {}
// ExitUser is called when production user is exited.
func (s *BaseAliasesListener) ExitUser(ctx *UserContext) {}
// EnterFile is called when production file is entered.
func (s *BaseAliasesListener) EnterFile(ctx *FileContext) {}
// ExitFile is called when production file is exited.
func (s *BaseAliasesListener) ExitFile(ctx *FileContext) {}
// EnterCommand is called when production command is entered.
func (s *BaseAliasesListener) EnterCommand(ctx *CommandContext) {}
// ExitCommand is called when production command is exited.
func (s *BaseAliasesListener) ExitCommand(ctx *CommandContext) {}
// EnterInclude is called when production include is entered.
func (s *BaseAliasesListener) EnterInclude(ctx *IncludeContext) {}
// ExitInclude is called when production include is exited.
func (s *BaseAliasesListener) ExitInclude(ctx *IncludeContext) {}
// EnterComment is called when production comment is entered.
func (s *BaseAliasesListener) EnterComment(ctx *CommentContext) {}
// ExitComment is called when production comment is exited.
func (s *BaseAliasesListener) ExitComment(ctx *CommentContext) {}
// EnterEmail is called when production email is entered.
func (s *BaseAliasesListener) EnterEmail(ctx *EmailContext) {}
// ExitEmail is called when production email is exited.
func (s *BaseAliasesListener) ExitEmail(ctx *EmailContext) {}
// EnterError is called when production error is entered.
func (s *BaseAliasesListener) EnterError(ctx *ErrorContext) {}
// ExitError is called when production error is exited.
func (s *BaseAliasesListener) ExitError(ctx *ErrorContext) {}
// EnterErrorCode is called when production errorCode is entered.
func (s *BaseAliasesListener) EnterErrorCode(ctx *ErrorCodeContext) {}
// ExitErrorCode is called when production errorCode is exited.
func (s *BaseAliasesListener) ExitErrorCode(ctx *ErrorCodeContext) {}
// EnterErrorMessage is called when production errorMessage is entered.
func (s *BaseAliasesListener) EnterErrorMessage(ctx *ErrorMessageContext) {}
// ExitErrorMessage is called when production errorMessage is exited.
func (s *BaseAliasesListener) ExitErrorMessage(ctx *ErrorMessageContext) {}

View File

@ -0,0 +1,137 @@
// Code generated from Aliases.g4 by ANTLR 4.13.0. DO NOT EDIT.
package parser
import (
"fmt"
"github.com/antlr4-go/antlr/v4"
"sync"
"unicode"
)
// Suppress unused import error
var _ = fmt.Printf
var _ = sync.Once{}
var _ = unicode.IsLetter
type AliasesLexer struct {
*antlr.BaseLexer
channelNames []string
modeNames []string
// TODO: EOF string
}
var AliasesLexerLexerStaticData struct {
once sync.Once
serializedATN []int32
ChannelNames []string
ModeNames []string
LiteralNames []string
SymbolicNames []string
RuleNames []string
PredictionContextCache *antlr.PredictionContextCache
atn *antlr.ATN
decisionToDFA []*antlr.DFA
}
func aliaseslexerLexerInit() {
staticData := &AliasesLexerLexerStaticData
staticData.ChannelNames = []string{
"DEFAULT_TOKEN_CHANNEL", "HIDDEN",
}
staticData.ModeNames = []string{
"DEFAULT_MODE",
}
staticData.LiteralNames = []string{
"", "", "", "", "'@'", "", "'|'", "':'", "','", "'#'", "'/'",
}
staticData.SymbolicNames = []string{
"", "DIGITS", "ERROR", "SEPARATOR", "AT", "INCLUDE", "VERTLINE", "COLON",
"COMMA", "NUMBER_SIGN", "SLASH", "STRING",
}
staticData.RuleNames = []string{
"DIGITS", "ERROR", "SEPARATOR", "AT", "INCLUDE", "VERTLINE", "COLON",
"COMMA", "NUMBER_SIGN", "SLASH", "STRING",
}
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
staticData.serializedATN = []int32{
4, 0, 11, 64, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
10, 7, 10, 1, 0, 4, 0, 25, 8, 0, 11, 0, 12, 0, 26, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 4, 2, 36, 8, 2, 11, 2, 12, 2, 37, 1, 3, 1, 3, 1, 4,
1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7,
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 10, 4, 10, 61, 8, 10, 11, 10, 12, 10,
62, 0, 0, 11, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9,
19, 10, 21, 11, 1, 0, 3, 1, 0, 48, 57, 2, 0, 9, 9, 32, 32, 9, 0, 9, 10,
13, 13, 32, 32, 35, 35, 44, 44, 47, 47, 58, 58, 64, 64, 124, 124, 66, 0,
1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0,
9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0,
0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 1, 24, 1, 0, 0,
0, 3, 28, 1, 0, 0, 0, 5, 35, 1, 0, 0, 0, 7, 39, 1, 0, 0, 0, 9, 41, 1, 0,
0, 0, 11, 49, 1, 0, 0, 0, 13, 51, 1, 0, 0, 0, 15, 53, 1, 0, 0, 0, 17, 55,
1, 0, 0, 0, 19, 57, 1, 0, 0, 0, 21, 60, 1, 0, 0, 0, 23, 25, 7, 0, 0, 0,
24, 23, 1, 0, 0, 0, 25, 26, 1, 0, 0, 0, 26, 24, 1, 0, 0, 0, 26, 27, 1,
0, 0, 0, 27, 2, 1, 0, 0, 0, 28, 29, 5, 101, 0, 0, 29, 30, 5, 114, 0, 0,
30, 31, 5, 114, 0, 0, 31, 32, 5, 111, 0, 0, 32, 33, 5, 114, 0, 0, 33, 4,
1, 0, 0, 0, 34, 36, 7, 1, 0, 0, 35, 34, 1, 0, 0, 0, 36, 37, 1, 0, 0, 0,
37, 35, 1, 0, 0, 0, 37, 38, 1, 0, 0, 0, 38, 6, 1, 0, 0, 0, 39, 40, 5, 64,
0, 0, 40, 8, 1, 0, 0, 0, 41, 42, 5, 105, 0, 0, 42, 43, 5, 110, 0, 0, 43,
44, 5, 99, 0, 0, 44, 45, 5, 108, 0, 0, 45, 46, 5, 117, 0, 0, 46, 47, 5,
100, 0, 0, 47, 48, 5, 101, 0, 0, 48, 10, 1, 0, 0, 0, 49, 50, 5, 124, 0,
0, 50, 12, 1, 0, 0, 0, 51, 52, 5, 58, 0, 0, 52, 14, 1, 0, 0, 0, 53, 54,
5, 44, 0, 0, 54, 16, 1, 0, 0, 0, 55, 56, 5, 35, 0, 0, 56, 18, 1, 0, 0,
0, 57, 58, 5, 47, 0, 0, 58, 20, 1, 0, 0, 0, 59, 61, 8, 2, 0, 0, 60, 59,
1, 0, 0, 0, 61, 62, 1, 0, 0, 0, 62, 60, 1, 0, 0, 0, 62, 63, 1, 0, 0, 0,
63, 22, 1, 0, 0, 0, 4, 0, 26, 37, 62, 0,
}
deserializer := antlr.NewATNDeserializer(nil)
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
atn := staticData.atn
staticData.decisionToDFA = make([]*antlr.DFA, len(atn.DecisionToState))
decisionToDFA := staticData.decisionToDFA
for index, state := range atn.DecisionToState {
decisionToDFA[index] = antlr.NewDFA(state, index)
}
}
// AliasesLexerInit initializes any static state used to implement AliasesLexer. By default the
// static state used to implement the lexer is lazily initialized during the first call to
// NewAliasesLexer(). You can call this function if you wish to initialize the static state ahead
// of time.
func AliasesLexerInit() {
staticData := &AliasesLexerLexerStaticData
staticData.once.Do(aliaseslexerLexerInit)
}
// NewAliasesLexer produces a new lexer instance for the optional input antlr.CharStream.
func NewAliasesLexer(input antlr.CharStream) *AliasesLexer {
AliasesLexerInit()
l := new(AliasesLexer)
l.BaseLexer = antlr.NewBaseLexer(input)
staticData := &AliasesLexerLexerStaticData
l.Interpreter = antlr.NewLexerATNSimulator(l, staticData.atn, staticData.decisionToDFA, staticData.PredictionContextCache)
l.channelNames = staticData.ChannelNames
l.modeNames = staticData.ModeNames
l.RuleNames = staticData.RuleNames
l.LiteralNames = staticData.LiteralNames
l.SymbolicNames = staticData.SymbolicNames
l.GrammarFileName = "Aliases.g4"
// TODO: l.EOF = antlr.TokenEOF
return l
}
// AliasesLexer tokens.
const (
AliasesLexerDIGITS = 1
AliasesLexerERROR = 2
AliasesLexerSEPARATOR = 3
AliasesLexerAT = 4
AliasesLexerINCLUDE = 5
AliasesLexerVERTLINE = 6
AliasesLexerCOLON = 7
AliasesLexerCOMMA = 8
AliasesLexerNUMBER_SIGN = 9
AliasesLexerSLASH = 10
AliasesLexerSTRING = 11
)

View File

@ -0,0 +1,100 @@
// Code generated from Aliases.g4 by ANTLR 4.13.0. DO NOT EDIT.
package parser // Aliases
import "github.com/antlr4-go/antlr/v4"
// AliasesListener is a complete listener for a parse tree produced by AliasesParser.
type AliasesListener interface {
antlr.ParseTreeListener
// EnterLineStatement is called when entering the lineStatement production.
EnterLineStatement(c *LineStatementContext)
// EnterEntry is called when entering the entry production.
EnterEntry(c *EntryContext)
// EnterSeparator is called when entering the separator production.
EnterSeparator(c *SeparatorContext)
// EnterKey is called when entering the key production.
EnterKey(c *KeyContext)
// EnterValues is called when entering the values production.
EnterValues(c *ValuesContext)
// EnterValue is called when entering the value production.
EnterValue(c *ValueContext)
// EnterUser is called when entering the user production.
EnterUser(c *UserContext)
// EnterFile is called when entering the file production.
EnterFile(c *FileContext)
// EnterCommand is called when entering the command production.
EnterCommand(c *CommandContext)
// EnterInclude is called when entering the include production.
EnterInclude(c *IncludeContext)
// EnterComment is called when entering the comment production.
EnterComment(c *CommentContext)
// EnterEmail is called when entering the email production.
EnterEmail(c *EmailContext)
// EnterError is called when entering the error production.
EnterError(c *ErrorContext)
// EnterErrorCode is called when entering the errorCode production.
EnterErrorCode(c *ErrorCodeContext)
// EnterErrorMessage is called when entering the errorMessage production.
EnterErrorMessage(c *ErrorMessageContext)
// ExitLineStatement is called when exiting the lineStatement production.
ExitLineStatement(c *LineStatementContext)
// ExitEntry is called when exiting the entry production.
ExitEntry(c *EntryContext)
// ExitSeparator is called when exiting the separator production.
ExitSeparator(c *SeparatorContext)
// ExitKey is called when exiting the key production.
ExitKey(c *KeyContext)
// ExitValues is called when exiting the values production.
ExitValues(c *ValuesContext)
// ExitValue is called when exiting the value production.
ExitValue(c *ValueContext)
// ExitUser is called when exiting the user production.
ExitUser(c *UserContext)
// ExitFile is called when exiting the file production.
ExitFile(c *FileContext)
// ExitCommand is called when exiting the command production.
ExitCommand(c *CommandContext)
// ExitInclude is called when exiting the include production.
ExitInclude(c *IncludeContext)
// ExitComment is called when exiting the comment production.
ExitComment(c *CommentContext)
// ExitEmail is called when exiting the email production.
ExitEmail(c *EmailContext)
// ExitError is called when exiting the error production.
ExitError(c *ErrorContext)
// ExitErrorCode is called when exiting the errorCode production.
ExitErrorCode(c *ErrorCodeContext)
// ExitErrorMessage is called when exiting the errorMessage production.
ExitErrorMessage(c *ErrorMessageContext)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
package aliases
import (
"config-lsp/handlers/aliases/ast"
"config-lsp/handlers/aliases/indexes"
protocol "github.com/tliron/glsp/protocol_3_16"
)
type AliasesDocument struct {
Parser *ast.AliasesParser
Indexes *indexes.AliasesIndexes
}
var DocumentParserMap = map[protocol.DocumentUri]*AliasesDocument{}

View File

@ -0,0 +1,12 @@
package shared
import "fmt"
type DuplicateKeyEntry struct {
AlreadyFoundAt uint32
Key string
}
func (d DuplicateKeyEntry) Error() string {
return fmt.Sprintf("Alias '%s' already defined on line %d", d.Key, d.AlreadyFoundAt+1)
}

View File

@ -13,6 +13,8 @@ var sampleInvalidOptionsExample = `
LABEL=test /mnt/test btrfs subvol=backup,fat=32 0 0
`
// TODO: Improve `entries`, sometimes the indexes seem
// to be wrong. Use a treemap instead of a map.
func TestValidBasicExample(t *testing.T) {
// Arrange
parser := FstabParser{}

View File

@ -3,7 +3,6 @@ package fstab
import (
"config-lsp/common"
"config-lsp/utils"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)

View File

@ -3,7 +3,6 @@ package fstab
import (
"config-lsp/common"
"config-lsp/utils"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)

View File

@ -2,13 +2,13 @@ package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/hosts"
"config-lsp/utils"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func Analyze(parser *HostsParser) []protocol.Diagnostic {
errors := analyzeEntriesSetCorrectly(*parser)
func Analyze(document *hosts.HostsDocument) []protocol.Diagnostic {
errors := analyzeEntriesSetCorrectly(*document.Parser)
if len(errors) > 0 {
return utils.Map(
@ -19,7 +19,7 @@ func Analyze(parser *HostsParser) []protocol.Diagnostic {
)
}
errors = analyzeEntriesAreValid(*parser)
errors = analyzeEntriesAreValid(*document.Parser)
if len(errors) > 0 {
return utils.Map(
@ -30,8 +30,8 @@ func Analyze(parser *HostsParser) []protocol.Diagnostic {
)
}
errors = append(errors, analyzeDoubleIPs(parser)...)
errors = append(errors, analyzeDoubleHostNames(parser)...)
errors = append(errors, analyzeDoubleIPs(document)...)
errors = append(errors, analyzeDoubleHostNames(document)...)
return utils.Map(
errors,

View File

@ -0,0 +1,51 @@
package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/shared"
"config-lsp/utils"
"net"
"slices"
)
func ipToString(ip net.IPAddr) string {
return ip.IP.String()
}
func analyzeDoubleIPs(d *hosts.HostsDocument) []common.LSPError {
errors := make([]common.LSPError, 0)
ips := make(map[string]uint32)
d.Indexes.DoubleIPs = make(map[uint32]shared.DuplicateIPDeclaration)
// TODO: `range` does not seem to properly
// iterate in a sorted way.
// Instead, use a treemap
lines := utils.KeysOfMap(d.Parser.Tree.Entries)
slices.Sort(lines)
for _, lineNumber := range lines {
entry := d.Parser.Tree.Entries[lineNumber]
if entry.IPAddress != nil {
key := ipToString(entry.IPAddress.Value)
if foundLine, found := ips[key]; found {
err := shared.DuplicateIPDeclaration{
AlreadyFoundAt: foundLine,
}
d.Indexes.DoubleIPs[lineNumber] = err
errors = append(errors, common.LSPError{
Range: entry.IPAddress.Location,
Err: err,
})
} else {
ips[key] = lineNumber
}
}
}
return errors
}

View File

@ -1,6 +1,10 @@
package analyzer
import (
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/ast"
"config-lsp/handlers/hosts/indexes"
"config-lsp/handlers/hosts/shared"
"config-lsp/utils"
"testing"
)
@ -15,14 +19,19 @@ func TestWorksWithNonDoubleIPs(
1.2.3.6 bar.com
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
i := indexes.NewHostsIndexes()
document := hosts.HostsDocument{
Parser: &parser,
Indexes: &i,
}
errors := parser.Parse(input)
if len(errors) != 0 {
t.Fatalf("PARER FAILED! Expected no errors, but got %v", errors)
}
errors = analyzeDoubleIPs(&parser)
errors = analyzeDoubleIPs(&document)
if len(errors) != 0 {
t.Errorf("Expected no errors, but got %v", errors)
@ -38,14 +47,19 @@ func TestWorksWithDoubleIPs(
1.2.3.4 foo.com
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
i := indexes.NewHostsIndexes()
document := hosts.HostsDocument{
Parser: &parser,
Indexes: &i,
}
errors := parser.Parse(input)
if len(errors) != 0 {
t.Fatalf("PARER FAILED! Expected no errors, but got %v", errors)
}
errors = analyzeDoubleIPs(&parser)
errors = analyzeDoubleIPs(&document)
if !(len(errors) == 1) {
t.Errorf("Expected 1 error, but got %v", len(errors))
@ -55,7 +69,7 @@ func TestWorksWithDoubleIPs(
t.Errorf("Expected error on line 2, but got %v", errors[0].Range.Start.Line)
}
if !(errors[0].Err.(DuplicateIPDeclaration).AlreadyFoundAt == 0) {
t.Errorf("Expected error on line 0, but got %v", errors[0].Err.(DuplicateIPDeclaration).AlreadyFoundAt)
if !(errors[0].Err.(shared.DuplicateIPDeclaration).AlreadyFoundAt == 0) {
t.Errorf("Expected error on line 0, but got %v", errors[0].Err.(shared.DuplicateIPDeclaration).AlreadyFoundAt)
}
}

View File

@ -1,6 +1,7 @@
package analyzer
import (
"config-lsp/handlers/hosts/ast"
"config-lsp/utils"
"net"
"testing"
@ -13,7 +14,7 @@ func TestValidSimpleExampleWorks(
1.2.3.4 hello.com
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
errors := parser.Parse(input)
if len(errors) != 0 {
@ -80,7 +81,7 @@ func TestValidComplexExampleWorks(
1.2.3.4 example.com check.com
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
errors := parser.Parse(input)
if len(errors) != 0 {
@ -115,7 +116,7 @@ func TestInvalidExampleWorks(
1.2.3.4
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
errors := parser.Parse(input)
if len(errors) == 0 {

View File

@ -2,33 +2,19 @@ package analyzer
import (
"config-lsp/common"
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/ast"
"config-lsp/handlers/hosts/indexes"
"config-lsp/handlers/hosts/shared"
"config-lsp/utils"
"net"
)
type ResolverEntry struct {
IPv4Address net.IP
IPv6Address net.IP
Line uint32
}
func (e ResolverEntry) GetInfo() string {
if e.IPv4Address != nil {
return e.IPv4Address.String()
}
return e.IPv6Address.String()
}
type Resolver struct {
Entries map[string]ResolverEntry
}
func createEntry(
line uint32,
ip net.IP,
) ResolverEntry {
entry := ResolverEntry{
) indexes.ResolverEntry {
entry := indexes.ResolverEntry{
Line: line,
}
@ -46,10 +32,10 @@ type hostnameEntry struct {
HostName string
}
func createResolverFromParser(p HostsParser) (Resolver, []common.LSPError) {
func createResolverFromParser(p ast.HostsParser) (indexes.Resolver, []common.LSPError) {
errors := make([]common.LSPError, 0)
resolver := Resolver{
Entries: make(map[string]ResolverEntry),
resolver := indexes.Resolver{
Entries: make(map[string]indexes.ResolverEntry),
}
for lineNumber, entry := range p.Tree.Entries {
@ -63,7 +49,7 @@ func createResolverFromParser(p HostsParser) (Resolver, []common.LSPError) {
},
utils.Map(
entry.Aliases,
func(alias *HostsHostname) hostnameEntry {
func(alias *ast.HostsHostname) hostnameEntry {
return hostnameEntry{
Location: alias.Location,
HostName: alias.Value,
@ -83,7 +69,7 @@ func createResolverFromParser(p HostsParser) (Resolver, []common.LSPError) {
errors,
common.LSPError{
Range: hostName.Location,
Err: DuplicateHostEntry{
Err: shared.DuplicateHostEntry{
AlreadyFoundAt: resolv.Line,
Hostname: hostName.HostName,
},
@ -99,10 +85,10 @@ func createResolverFromParser(p HostsParser) (Resolver, []common.LSPError) {
return resolver, errors
}
func analyzeDoubleHostNames(p *HostsParser) []common.LSPError {
resolver, errors := createResolverFromParser(*p)
func analyzeDoubleHostNames(d *hosts.HostsDocument) []common.LSPError {
resolver, errors := createResolverFromParser(*d.Parser)
p.Resolver = &resolver
d.Indexes.Resolver = &resolver
return errors
}

View File

@ -1,6 +1,7 @@
package analyzer
import (
"config-lsp/handlers/hosts/ast"
"config-lsp/utils"
"testing"
)
@ -13,7 +14,7 @@ func TestResolverEntriesWorksWithNonOverlapping(
5.5.5.5 world.com
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
errors := parser.Parse(input)
if len(errors) != 0 {
@ -55,7 +56,7 @@ func TestResolverEntriesWithSimpleOverlapping(
5.5.5.5 hello.com
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
errors := parser.Parse(input)
if len(errors) != 0 {
@ -85,7 +86,7 @@ func TestResolverEntriesWithComplexOverlapping(
5.5.5.5 check.com test.com
`)
parser := CreateNewHostsParser()
parser := ast.NewHostsParser()
errors := parser.Parse(input)
if len(errors) != 0 {

View File

@ -3,13 +3,14 @@ package analyzer
import (
"config-lsp/common"
docvalues "config-lsp/doc-values"
"config-lsp/handlers/hosts/ast"
"config-lsp/handlers/hosts/fields"
"config-lsp/utils"
"errors"
)
func analyzeEntriesSetCorrectly(
parser HostsParser,
parser ast.HostsParser,
) []common.LSPError {
err := make([]common.LSPError, 0)
@ -35,7 +36,7 @@ func analyzeEntriesSetCorrectly(
}
func analyzeEntriesAreValid(
parser HostsParser,
parser ast.HostsParser,
) []common.LSPError {
err := make([]common.LSPError, 0)

View File

@ -1,38 +1,14 @@
package analyzer
package ast
import (
"config-lsp/common"
"fmt"
"net"
"github.com/antlr4-go/antlr/v4"
)
func characterRangeFromCtx(
ctx antlr.BaseParserRuleContext,
) common.LocationRange {
line := uint32(ctx.GetStart().GetLine())
start := uint32(ctx.GetStart().GetStart())
end := uint32(ctx.GetStop().GetStop())
return common.LocationRange{
Start: common.Location{
Line: line,
Character: start,
},
End: common.Location{
Line: line,
Character: end + 1,
},
}
}
type HostsParser struct {
Tree HostsTree
CommentLines map[uint32]struct{}
Resolver *Resolver
// [line]error
DoubleIPs map[uint32]DuplicateIPDeclaration
}
type HostsTree struct {

View File

@ -1,9 +1,9 @@
package analyzer
package ast
import (
"config-lsp/common"
docvalues "config-lsp/doc-values"
"config-lsp/handlers/hosts/parser"
parser2 "config-lsp/handlers/hosts/ast/parser"
"net"
"github.com/antlr4-go/antlr/v4"
@ -14,19 +14,19 @@ type hostsListenerContext struct {
}
type hostsParserListener struct {
*parser.BaseHostsListener
*parser2.BaseHostsListener
Parser *HostsParser
Errors []common.LSPError
hostsContext hostsListenerContext
}
func (s *hostsParserListener) EnterComment(ctx *parser.CommentContext) {
func (s *hostsParserListener) EnterComment(ctx *parser2.CommentContext) {
line := uint32(s.hostsContext.line)
s.Parser.CommentLines[line] = struct{}{}
}
func (s *hostsParserListener) EnterEntry(ctx *parser.EntryContext) {
location := characterRangeFromCtx(ctx.BaseParserRuleContext)
func (s *hostsParserListener) EnterEntry(ctx *parser2.EntryContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.hostsContext.line)
s.Parser.Tree.Entries[location.Start.Line] = &HostsEntry{
@ -34,8 +34,8 @@ func (s *hostsParserListener) EnterEntry(ctx *parser.EntryContext) {
}
}
func (s *hostsParserListener) EnterIpAddress(ctx *parser.IpAddressContext) {
location := characterRangeFromCtx(ctx.BaseParserRuleContext)
func (s *hostsParserListener) EnterIpAddress(ctx *parser2.IpAddressContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.hostsContext.line)
ip := net.ParseIP(ctx.GetText())
@ -65,8 +65,8 @@ func (s *hostsParserListener) EnterIpAddress(ctx *parser.IpAddressContext) {
}
}
func (s *hostsParserListener) EnterHostname(ctx *parser.HostnameContext) {
location := characterRangeFromCtx(ctx.BaseParserRuleContext)
func (s *hostsParserListener) EnterHostname(ctx *parser2.HostnameContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.hostsContext.line)
entry := s.Parser.Tree.Entries[location.Start.Line]
@ -79,8 +79,8 @@ func (s *hostsParserListener) EnterHostname(ctx *parser.HostnameContext) {
s.Parser.Tree.Entries[location.Start.Line] = entry
}
func (s *hostsParserListener) EnterAliases(ctx *parser.AliasesContext) {
location := characterRangeFromCtx(ctx.BaseParserRuleContext)
func (s *hostsParserListener) EnterAliases(ctx *parser2.AliasesContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.hostsContext.line)
entry := s.Parser.Tree.Entries[location.Start.Line]
@ -90,8 +90,8 @@ func (s *hostsParserListener) EnterAliases(ctx *parser.AliasesContext) {
entry.Aliases = aliases
}
func (s *hostsParserListener) EnterAlias(ctx *parser.AliasContext) {
location := characterRangeFromCtx(ctx.BaseParserRuleContext)
func (s *hostsParserListener) EnterAlias(ctx *parser2.AliasContext) {
location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext)
location.ChangeBothLines(s.hostsContext.line)
entry := s.Parser.Tree.Entries[location.Start.Line]
@ -123,17 +123,6 @@ type errorListener struct {
hostsContext hostsListenerContext
}
func createErrorListener(
line uint32,
) errorListener {
return errorListener{
Errors: make([]common.LSPError, 0),
hostsContext: hostsListenerContext{
line: line,
},
}
}
func (d *errorListener) SyntaxError(
recognizer antlr.Recognizer,
offendingSymbol interface{},
@ -150,3 +139,14 @@ func (d *errorListener) SyntaxError(
},
})
}
func createErrorListener(
line uint32,
) errorListener {
return errorListener{
Errors: make([]common.LSPError, 0),
hostsContext: hostsListenerContext{
line: line,
},
}
}

View File

@ -1,8 +1,8 @@
package analyzer
package ast
import (
"config-lsp/common"
"config-lsp/handlers/hosts/parser"
parser2 "config-lsp/handlers/hosts/ast/parser"
"config-lsp/utils"
"regexp"
@ -26,7 +26,7 @@ func (p *HostsParser) parseStatement(
stream := antlr.NewInputStream(input)
errorListener := createErrorListener(line)
lexer := parser.NewHostsLexer(stream)
lexer := parser2.NewHostsLexer(stream)
lexer.RemoveErrorListeners()
lexer.AddErrorListener(&errorListener)
@ -34,7 +34,7 @@ func (p *HostsParser) parseStatement(
errorListener = createErrorListener(line)
tokenStream := antlr.NewCommonTokenStream(lexer, antlr.TokenDefaultChannel)
antlrParser := parser.NewHostsParser(tokenStream)
antlrParser := parser2.NewHostsParser(tokenStream)
antlrParser.RemoveErrorListeners()
antlrParser.AddErrorListener(&errorListener)
@ -74,7 +74,7 @@ func (p *HostsParser) Parse(input string) []common.LSPError {
return errors
}
func CreateNewHostsParser() HostsParser {
func NewHostsParser() HostsParser {
p := HostsParser{}
p.Clear()

View File

@ -1,39 +0,0 @@
package analyzer
import (
"config-lsp/common"
"net"
)
func ipToString(ip net.IPAddr) string {
return ip.IP.String()
}
func analyzeDoubleIPs(p *HostsParser) []common.LSPError {
errors := make([]common.LSPError, 0)
ips := make(map[string]uint32)
p.DoubleIPs = make(map[uint32]DuplicateIPDeclaration)
for lineNumber, entry := range p.Tree.Entries {
if entry.IPAddress != nil {
key := ipToString(entry.IPAddress.Value)
if foundLine, found := ips[key]; found {
err := DuplicateIPDeclaration{
AlreadyFoundAt: foundLine,
}
p.DoubleIPs[lineNumber] = err
errors = append(errors, common.LSPError{
Range: entry.IPAddress.Location,
Err: err,
})
} else {
ips[key] = lineNumber
}
}
}
return errors
}

View File

@ -1,7 +1,7 @@
package handlers
import (
"config-lsp/handlers/hosts/handlers/analyzer"
"config-lsp/handlers/hosts/ast"
"config-lsp/utils"
"fmt"
"strings"
@ -16,7 +16,7 @@ const (
)
type CodeAction interface {
RunCommand(analyzer.HostsParser) (*protocol.ApplyWorkspaceEditParams, error)
RunCommand(ast.HostsParser) (*protocol.ApplyWorkspaceEditParams, error)
}
type CodeActionArgs interface{}
@ -35,7 +35,7 @@ func CodeActionInlineAliasesArgsFromArguments(arguments map[string]any) CodeActi
}
}
func (args CodeActionInlineAliasesArgs) RunCommand(hostsParser analyzer.HostsParser) (*protocol.ApplyWorkspaceEditParams, error) {
func (args CodeActionInlineAliasesArgs) RunCommand(hostsParser ast.HostsParser) (*protocol.ApplyWorkspaceEditParams, error) {
fromEntry := hostsParser.Tree.Entries[args.FromLine]
toEntry := hostsParser.Tree.Entries[args.ToLine]
@ -58,7 +58,7 @@ func (args CodeActionInlineAliasesArgs) RunCommand(hostsParser analyzer.HostsPar
},
utils.Map(
fromEntry.Aliases,
func(alias *analyzer.HostsHostname) string {
func(alias *ast.HostsHostname) string {
return alias.Value
},
)...,

View File

@ -1,17 +1,17 @@
package handlers
import (
"config-lsp/handlers/hosts/handlers/analyzer"
"config-lsp/handlers/hosts"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func GetInlineAliasesCodeAction(
p analyzer.HostsParser,
d hosts.HostsDocument,
params *protocol.CodeActionParams,
) []protocol.CodeAction {
line := params.Range.Start.Line
if duplicateInfo, found := p.DoubleIPs[line]; found {
if duplicateInfo, found := d.Indexes.DoubleIPs[line]; found {
commandID := "hosts." + CodeActionInlineAliases
command := protocol.Command{
Title: "Inline Aliases",

View File

@ -1,7 +1,8 @@
package handlers
import (
"config-lsp/handlers/hosts/handlers/analyzer"
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/ast"
"fmt"
)
@ -14,7 +15,7 @@ const (
)
func GetHoverTargetInEntry(
e analyzer.HostsEntry,
e ast.HostsEntry,
cursor uint32,
) *HoverTarget {
if e.IPAddress != nil && e.IPAddress.Location.ContainsCursorByCharacter(cursor) {
@ -38,11 +39,11 @@ func GetHoverTargetInEntry(
}
func GetHoverInfoForHostname(
parser analyzer.HostsParser,
hostname analyzer.HostsHostname,
d hosts.HostsDocument,
hostname ast.HostsHostname,
cursor uint32,
) []string {
ipAddress := parser.Resolver.Entries[hostname.Value]
ipAddress := d.Indexes.Resolver.Entries[hostname.Value]
return []string{
fmt.Sprintf("**%s** maps to _%s_", hostname.Value, ipAddress.GetInfo()),

View File

@ -0,0 +1,17 @@
package indexes
import (
"config-lsp/handlers/hosts/shared"
)
type HostsIndexes struct {
Resolver *Resolver
// [line]error
DoubleIPs map[uint32]shared.DuplicateIPDeclaration
}
func NewHostsIndexes() HostsIndexes {
return HostsIndexes{
DoubleIPs: make(map[uint32]shared.DuplicateIPDeclaration),
}
}

View File

@ -0,0 +1,23 @@
package indexes
import (
"net"
)
type ResolverEntry struct {
IPv4Address net.IP
IPv6Address net.IP
Line uint32
}
func (e ResolverEntry) GetInfo() string {
if e.IPv4Address != nil {
return e.IPv4Address.String()
}
return e.IPv6Address.String()
}
type Resolver struct {
Entries map[string]ResolverEntry
}

View File

@ -1,9 +0,0 @@
package lsp
import (
"config-lsp/handlers/hosts/handlers/analyzer"
protocol "github.com/tliron/glsp/protocol_3_16"
)
var documentParserMap = map[protocol.DocumentUri]*analyzer.HostsParser{}

View File

@ -1,18 +1,18 @@
package lsp
import (
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/handlers"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
parser := documentParserMap[params.TextDocument.URI]
document := hosts.DocumentParserMap[params.TextDocument.URI]
actions := make([]protocol.CodeAction, 0, 1)
actions = append(actions, handlers.GetInlineAliasesCodeAction(*parser, params)...)
actions = append(actions, handlers.GetInlineAliasesCodeAction(*document, params)...)
if len(actions) > 0 {
return actions, nil

View File

@ -2,9 +2,9 @@ package lsp
import (
"config-lsp/common"
"config-lsp/handlers/hosts/handlers/analyzer"
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/analyzer"
"config-lsp/utils"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
@ -16,11 +16,11 @@ func TextDocumentDidChange(
content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text
common.ClearDiagnostics(context, params.TextDocument.URI)
parser := documentParserMap[params.TextDocument.URI]
parser.Clear()
document := hosts.DocumentParserMap[params.TextDocument.URI]
document.Parser.Clear()
diagnostics := make([]protocol.Diagnostic, 0)
errors := parser.Parse(content)
errors := document.Parser.Parse(content)
if len(errors) > 0 {
diagnostics = append(diagnostics, utils.Map(
@ -31,7 +31,7 @@ func TextDocumentDidChange(
)...)
}
diagnostics = append(diagnostics, analyzer.Analyze(parser)...)
diagnostics = append(diagnostics, analyzer.Analyze(document)...)
if len(diagnostics) > 0 {
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)

View File

@ -1,12 +1,13 @@
package lsp
import (
"config-lsp/handlers/hosts"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentDidClose(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error {
delete(documentParserMap, params.TextDocument.URI)
delete(hosts.DocumentParserMap, params.TextDocument.URI)
return nil
}

View File

@ -2,9 +2,11 @@ package lsp
import (
"config-lsp/common"
"config-lsp/handlers/hosts/handlers/analyzer"
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/analyzer"
"config-lsp/handlers/hosts/ast"
"config-lsp/handlers/hosts/indexes"
"config-lsp/utils"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
@ -15,8 +17,13 @@ func TextDocumentDidOpen(
) error {
common.ClearDiagnostics(context, params.TextDocument.URI)
parser := analyzer.CreateNewHostsParser()
documentParserMap[params.TextDocument.URI] = &parser
parser := ast.NewHostsParser()
i := indexes.NewHostsIndexes()
document := hosts.HostsDocument{
Parser: &parser,
Indexes: &i,
}
hosts.DocumentParserMap[params.TextDocument.URI] = &document
errors := parser.Parse(params.TextDocument.Text)
@ -29,7 +36,7 @@ func TextDocumentDidOpen(
diagnostics = append(
diagnostics,
analyzer.Analyze(&parser)...,
analyzer.Analyze(&document)...,
)
if len(diagnostics) > 0 {

View File

@ -1,9 +1,10 @@
package lsp
import (
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/ast"
"config-lsp/handlers/hosts/fields"
"config-lsp/handlers/hosts/handlers"
"config-lsp/handlers/hosts/handlers/analyzer"
"strings"
"github.com/tliron/glsp"
@ -14,17 +15,17 @@ func TextDocumentHover(
context *glsp.Context,
params *protocol.HoverParams,
) (*protocol.Hover, error) {
parser := documentParserMap[params.TextDocument.URI]
document := hosts.DocumentParserMap[params.TextDocument.URI]
line := params.Position.Line
character := params.Position.Character
if _, found := parser.CommentLines[line]; found {
if _, found := document.Parser.CommentLines[line]; found {
// Comment
return nil, nil
}
entry, found := parser.Tree.Entries[line]
entry, found := document.Parser.Tree.Entries[line]
if !found {
// Empty line
@ -33,7 +34,7 @@ func TextDocumentHover(
target := handlers.GetHoverTargetInEntry(*entry, character)
var hostname *analyzer.HostsHostname
var hostname *ast.HostsHostname
switch *target {
case handlers.HoverTargetIPAddress:
@ -74,7 +75,7 @@ func TextDocumentHover(
)
contents = append(
contents,
handlers.GetHoverInfoForHostname(*parser, *hostname, character)...,
handlers.GetHoverInfoForHostname(*document, *hostname, character)...,
)
return &protocol.Hover{

View File

@ -1,6 +1,7 @@
package lsp
import (
"config-lsp/handlers/hosts"
"config-lsp/handlers/hosts/handlers"
"strings"
@ -15,9 +16,9 @@ func WorkspaceExecuteCommand(context *glsp.Context, params *protocol.ExecuteComm
case string(handlers.CodeActionInlineAliases):
args := handlers.CodeActionInlineAliasesArgsFromArguments(params.Arguments[0].(map[string]any))
parser := documentParserMap[args.URI]
document := hosts.DocumentParserMap[args.URI]
return args.RunCommand(*parser)
return args.RunCommand(*document.Parser)
}
return nil, nil

14
handlers/hosts/shared.go Normal file
View File

@ -0,0 +1,14 @@
package hosts
import (
"config-lsp/handlers/hosts/ast"
"config-lsp/handlers/hosts/indexes"
protocol "github.com/tliron/glsp/protocol_3_16"
)
type HostsDocument struct {
Parser *ast.HostsParser
Indexes *indexes.HostsIndexes
}
var DocumentParserMap = map[protocol.DocumentUri]*HostsDocument{}

View File

@ -1,4 +1,4 @@
package analyzer
package shared
import "fmt"

View File

@ -1,4 +1,4 @@
package common
package openssh
import (
docvalues "config-lsp/doc-values"

View File

@ -1,8 +1,6 @@
package openssh
import (
"config-lsp/common"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
@ -10,18 +8,18 @@ import (
func DiagnoseSSHOptions(
context *glsp.Context,
documentURI protocol.DocumentUri,
parser *common.SimpleConfigParser,
parser *SimpleConfigParser,
) []protocol.Diagnostic {
diagnostics := make([]protocol.Diagnostic, 0)
diagnostics = append(
diagnostics,
common.DiagnoseOption(
DiagnoseOption(
context,
documentURI,
parser,
"Port",
func(value string, position common.SimpleConfigPosition) []protocol.Diagnostic {
func(value string, position SimpleConfigPosition) []protocol.Diagnostic {
if value == "22" {
severity := protocol.DiagnosticSeverityWarning
@ -50,3 +48,20 @@ func DiagnoseSSHOptions(
return diagnostics
}
func DiagnoseOption(
context *glsp.Context,
uri protocol.DocumentUri,
parser *SimpleConfigParser,
optionName string,
checkerFunc func(string, SimpleConfigPosition) []protocol.Diagnostic,
) []protocol.Diagnostic {
option, err := parser.GetOption(optionName)
if err != nil {
// Nothing to diagnose
return nil
}
return checkerFunc(option.Value, option.Position)
}

Some files were not shown because too many files have changed in this diff Show More