diff --git a/flake.nix b/flake.nix index 08e129f..337a65a 100644 --- a/flake.nix +++ b/flake.nix @@ -42,7 +42,10 @@ }; }; devShells.default = pkgs.mkShell { - buildInputs = inputs; + buildInputs = inputs ++ (with pkgs; [ + mailutils + swaks + ]); }; } ); diff --git a/handlers/aliases/ast/values.go b/handlers/aliases/ast/values.go index ac94fc0..d468b62 100644 --- a/handlers/aliases/ast/values.go +++ b/handlers/aliases/ast/values.go @@ -6,6 +6,7 @@ import ( "config-lsp/handlers/aliases/fields" "config-lsp/utils" "fmt" + "strconv" ) type AliasValueInterface interface { @@ -102,6 +103,16 @@ type AliasValueErrorCode struct { AliasValue } +func (a AliasValueErrorCode) ErrorCodeAsInt() uint16 { + code, err := strconv.Atoi(a.Value) + + if err != nil { + return 0 + } + + return uint16(code) +} + type AliasValueErrorMessage struct { AliasValue } diff --git a/handlers/aliases/handlers/completions.go b/handlers/aliases/handlers/completions.go index 8df8d3f..615c765 100644 --- a/handlers/aliases/handlers/completions.go +++ b/handlers/aliases/handlers/completions.go @@ -48,7 +48,7 @@ func GetCompletionsForEntry( return completions, nil } - value := getValueAtCursor(cursor, entry) + value := GetValueAtCursor(cursor, entry) relativeCursor := cursor - entry.Key.Location.Start.Character if value == nil { diff --git a/handlers/aliases/handlers/get-value.go b/handlers/aliases/handlers/get-value.go index 0a3b9bf..eab77ba 100644 --- a/handlers/aliases/handlers/get-value.go +++ b/handlers/aliases/handlers/get-value.go @@ -5,7 +5,7 @@ import ( "slices" ) -func getValueAtCursor( +func GetValueAtCursor( cursor uint32, entry *ast.AliasEntry, ) *ast.AliasValueInterface { diff --git a/handlers/aliases/handlers/hover.go b/handlers/aliases/handlers/hover.go new file mode 100644 index 0000000..52ced93 --- /dev/null +++ b/handlers/aliases/handlers/hover.go @@ -0,0 +1,158 @@ +package handlers + +import ( + "config-lsp/handlers/aliases/ast" + "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 { + switch value.(type) { + case ast.AliasValueUser: + return []string{ + "### User", + "`user`", + "", + "A user on the host machine. The user must have a valid entry in the passwd(5) database file.", + } + case ast.AliasValueEmail: + return []string{ + "### Email", + "`user-part@domain-part`", + "", + "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.", + } + case ast.AliasValueInclude: + return []string{ + "### Include", + "`include:/path/to/file`", + "", + "Include any definitions in file as alias entries. The format of the file is identical to this one.", + } + case ast.AliasValueFile: + return []string{ + "### File", + "`/path/to/file`", + "", + "Append messages to file, specified by its absolute pathname.", + } + case ast.AliasValueCommand: + return []string{ + "### Command", + "`|command`", + "", + "Pipe the message to command on its standard input. The command is run under the privileges of the daemon's unprivileged account.", + } + case ast.AliasValueError: + return []string{ + "### Error", + "`error:code message`", + "", + "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.", + } + } + + 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" +} diff --git a/handlers/aliases/lsp/text-document-hover.go b/handlers/aliases/lsp/text-document-hover.go index 0845504..51acd2c 100644 --- a/handlers/aliases/lsp/text-document-hover.go +++ b/handlers/aliases/lsp/text-document-hover.go @@ -1,7 +1,10 @@ package lsp import ( - "config-lsp/handlers/hosts" + "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" @@ -11,76 +14,51 @@ func TextDocumentHover( context *glsp.Context, params *protocol.HoverParams, ) (*protocol.Hover, error) { - document := hosts.DocumentParserMap[params.TextDocument.URI] + document := aliases.DocumentParserMap[params.TextDocument.URI] line := params.Position.Line - // character := params.Position.Character + character := params.Position.Character if _, found := document.Parser.CommentLines[line]; found { // Comment return nil, nil } - // entry, found := document.Parser.Tree.Entries[line] - // - // if !found { - // // Empty line - // return nil, nil - // } - // - // target := handlers.GetHoverTargetInEntry(*entry, character) - // - // var hostname *ast.HostsHostname - // - // switch *target { - // case handlers.HoverTargetIPAddress: - // relativeCursor := character - entry.IPAddress.Location.Start.Character - // hover := fields.IPAddressField.FetchHoverInfo(entry.IPAddress.Value.String(), relativeCursor) - // - // return &protocol.Hover{ - // Contents: hover, - // }, nil - // case handlers.HoverTargetHostname: - // hostname = entry.Hostname - // case handlers.HoverTargetAlias: - // for _, alias := range entry.Aliases { - // if alias.Location.Start.Character <= character && character <= alias.Location.End.Character { - // hostname = alias - // break - // } - // } - // } - // - // if hostname != nil { - // contents := []string{ - // "## Hostname", - // } - // contents = append( - // contents, - // fields.HostnameField.GetTypeDescription()..., - // ) - // contents = append( - // contents, - // []string{ - // "", - // }..., - // ) - // contents = append( - // contents, - // fields.HostnameField.Documentation, - // ) - // contents = append( - // contents, - // handlers.GetHoverInfoForHostname(*document, *hostname, character)..., - // ) - // - // return &protocol.Hover{ - // Contents: &protocol.MarkupContent{ - // Kind: protocol.MarkupKindMarkdown, - // Value: strings.Join(contents, "\n"), - // }, - // }, 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 }