diff --git a/doc-values/base-value.go b/doc-values/base-value.go index 1935d62..b2c2ba4 100644 --- a/doc-values/base-value.go +++ b/doc-values/base-value.go @@ -10,6 +10,7 @@ type Value interface { GetTypeDescription() []string CheckIsValid(value string) []*InvalidValue FetchCompletions(line string, cursor uint32) []protocol.CompletionItem + FetchHoverInfo(line string, cursor uint32) []string } type InvalidValue struct { diff --git a/doc-values/value-array.go b/doc-values/value-array.go index 94a59ff..6f1b15b 100644 --- a/doc-values/value-array.go +++ b/doc-values/value-array.go @@ -114,17 +114,55 @@ func (v ArrayValue) CheckIsValid(value string) []*InvalidValue { return errors } -func (v ArrayValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { - if cursor == 0 { - return v.SubValue.FetchCompletions(line, cursor) +func (v ArrayValue) getCurrentValue(line string, cursor uint32) (string, uint32) { + if line == "" { + return line, cursor } - relativePosition, found := utils.FindPreviousCharacter(line, v.Separator, int(cursor-1)) + if cursor == uint32(len(line)) { + cursor-- + } + + var start uint32 + var end uint32 + + relativePosition, found := utils.FindPreviousCharacter( + line, + v.Separator, + int(cursor), + ) if found { - line = line[uint32(relativePosition+1):] - cursor -= uint32(relativePosition + 1) + // +1 to skip the separator + start = uint32(relativePosition + 1) + } else { + start = 0 } - return v.SubValue.FetchCompletions(line, cursor) + relativePosition, found = utils.FindNextCharacter( + line, + v.Separator, + int(cursor), + ) + + if found { + // -1 to skip the separator + end = uint32(relativePosition - 1) + } else { + end = uint32(len(line)) + } + + return line[start:end], cursor - start +} + +func (v ArrayValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { + value, cursor := v.getCurrentValue(line, cursor) + + return v.SubValue.FetchCompletions(value, cursor) +} + +func (v ArrayValue) FetchHoverInfo(line string, cursor uint32) []string { + value, cursor := v.getCurrentValue(line, cursor) + + return v.SubValue.FetchHoverInfo(value, cursor) } diff --git a/doc-values/value-custom.go b/doc-values/value-custom.go index 3f255db..22f0e8d 100644 --- a/doc-values/value-custom.go +++ b/doc-values/value-custom.go @@ -21,7 +21,7 @@ type CustomValue struct { } func (v CustomValue) GetTypeDescription() []string { - return []string{"Custom"} + return v.FetchValue(EmptyValueContextInstance).GetTypeDescription() } func (v CustomValue) CheckIsValid(value string) []*InvalidValue { @@ -31,3 +31,7 @@ func (v CustomValue) CheckIsValid(value string) []*InvalidValue { func (v CustomValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { return v.FetchValue(EmptyValueContextInstance).FetchCompletions(line, cursor) } + +func (v CustomValue) FetchHoverInfo(line string, cursor uint32) []string { + return v.FetchValue(EmptyValueContextInstance).FetchHoverInfo(line, cursor) +} diff --git a/doc-values/value-enum.go b/doc-values/value-enum.go index 8e5cd98..9457008 100644 --- a/doc-values/value-enum.go +++ b/doc-values/value-enum.go @@ -118,3 +118,14 @@ func (v EnumValue) FetchCompletions(line string, cursor uint32) []protocol.Compl return completions } +func (v EnumValue) FetchHoverInfo(line string, cursor uint32) []string { + for _, value := range v.Values { + if value.InsertText == line { + return []string{ + value.Documentation, + } + } + } + + return []string{} +} diff --git a/doc-values/value-gid.go b/doc-values/value-gid.go index 5fca78c..183767b 100644 --- a/doc-values/value-gid.go +++ b/doc-values/value-gid.go @@ -2,6 +2,7 @@ package docvalues import ( "config-lsp/utils" + "fmt" "strconv" protocol "github.com/tliron/glsp/protocol_3_16" @@ -126,3 +127,26 @@ func (v GIDValue) FetchCompletions(line string, cursor uint32) []protocol.Comple return completions } +func (v GIDValue) FetchHoverInfo(line string, cursor uint32) []string { + uid, err := strconv.Atoi(line) + + if err != nil { + return []string{} + } + + infos, err := fetchGroupInfo() + + if err != nil { + return []string{} + } + + for _, info := range infos { + if info.GID == strconv.Itoa(uid) { + return []string{ + fmt.Sprintf("Group %s; GID: %s", info.Name, info.GID), + } + } + } + + return []string{} +} diff --git a/doc-values/value-ip-address.go b/doc-values/value-ip-address.go index 939c3be..927c9bf 100644 --- a/doc-values/value-ip-address.go +++ b/doc-values/value-ip-address.go @@ -2,6 +2,7 @@ package docvalues import ( "config-lsp/utils" + "fmt" net "net/netip" protocol "github.com/tliron/glsp/protocol_3_16" @@ -162,3 +163,47 @@ func (v IPAddressValue) FetchCompletions(line string, cursor uint32) []protocol. return []protocol.CompletionItem{} } + +func (v IPAddressValue) FetchHoverInfo(line string, cursor uint32) []string { + if v.AllowRange { + ip, err := net.ParsePrefix(line) + + if err != nil { + return []string{} + } + + if ip.Addr().IsPrivate() { + return []string{ + fmt.Sprintf("%s (Private IP Address)", ip.String()), + } + } else if ip.Addr().IsLoopback() { + return []string{ + fmt.Sprintf("%s (Loopback IP Address)", ip.String()), + } + } else { + return []string{ + fmt.Sprintf("%s (Public IP Address)", ip.String()), + } + } + } else { + ip, err := net.ParseAddr(line) + + if err != nil { + return []string{} + } + + if ip.IsPrivate() { + return []string{ + fmt.Sprintf("%s (Private IP Address)", ip.String()), + } + } else if ip.IsLoopback() { + return []string{ + fmt.Sprintf("%s (Loopback IP Address)", ip.String()), + } + } else { + return []string{ + fmt.Sprintf("%s (Public IP Address)", ip.String()), + } + } + } +} diff --git a/doc-values/value-key-enum-assignment.go b/doc-values/value-key-enum-assignment.go index c6e297c..ad5fe98 100644 --- a/doc-values/value-key-enum-assignment.go +++ b/doc-values/value-key-enum-assignment.go @@ -136,6 +136,35 @@ func (v KeyEnumAssignmentValue) FetchEnumCompletions() []protocol.CompletionItem return completions } +type selectedValue string + +const ( + keySelected selectedValue = "key" + valueSelected selectedValue = "value" +) + +func (v KeyEnumAssignmentValue) getValueAtCursor(line string, cursor uint32) (string, *selectedValue, uint32) { + relativePosition, found := utils.FindPreviousCharacter(line, v.Separator, int(cursor)) + + if found { + // Value found + selected := valueSelected + return line[uint32(relativePosition+1):], &selected, cursor - uint32(relativePosition) + } + + selected := keySelected + + // Key, let's check for the separator + relativePosition, found = utils.FindNextCharacter(line, v.Separator, int(cursor)) + + if found { + return line[:uint32(relativePosition)], &selected, cursor + } + + // No separator, so we can just return the whole line + return line, &selected, cursor +} + func (v KeyEnumAssignmentValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { if cursor == 0 { return v.FetchEnumCompletions() @@ -160,3 +189,50 @@ func (v KeyEnumAssignmentValue) FetchCompletions(line string, cursor uint32) []p return v.FetchEnumCompletions() } } + +func (v KeyEnumAssignmentValue) FetchHoverInfo(line string, cursor uint32) []string { + if len(v.CheckIsValid(line)) != 0 { + return []string{} + } + + value, selected, cursor := v.getValueAtCursor(line, cursor) + + if selected == nil { + return []string{} + } + + if *selected == keySelected { + // Search for enum documentation + enums := utils.KeysOfMap(v.Values) + key := value + + for _, enum := range enums { + if enum.InsertText == value { + return []string{ + fmt.Sprintf("## `%s%s`", key, v.Separator), + enum.Documentation, + } + } + } + } else if *selected == valueSelected { + // Search for value documentation + // - 1 to remove the separator + key := strings.SplitN(line, v.Separator, 2)[0] + checkValue, found := v.getValue(key) + + if !found { + return []string{} + } + + info := (*checkValue).FetchHoverInfo(value, cursor) + + return append( + []string{ + fmt.Sprintf("## `%s%s%s`", key, v.Separator, value), + }, + info..., + ) + } + + return []string{} +} diff --git a/doc-values/value-key-value-assignment.go b/doc-values/value-key-value-assignment.go index deb31e2..f57f126 100644 --- a/doc-values/value-key-value-assignment.go +++ b/doc-values/value-key-value-assignment.go @@ -120,3 +120,49 @@ func (v KeyValueAssignmentValue) FetchCompletions(line string, cursor uint32) [] return v.Key.FetchCompletions(line, cursor) } } + +func (v KeyValueAssignmentValue) getValueAtCursor(line string, cursor uint32) (string, *selectedValue, uint32) { + relativePosition, found := utils.FindPreviousCharacter(line, v.Separator, int(cursor)) + + if found { + // Value found + selected := valueSelected + return line[:uint32(relativePosition)], &selected, cursor - uint32(relativePosition) + } + + selected := keySelected + + // Key, let's check for the separator + relativePosition, found = utils.FindNextCharacter(line, v.Separator, int(cursor)) + + if found { + return line[:uint32(relativePosition)], &selected, cursor + } + + // No separator, so we can just return the whole line + return line, &selected, cursor +} + +func (v KeyValueAssignmentValue) FetchHoverInfo(line string, cursor uint32) []string { + if len(v.CheckIsValid(line)) != 0 { + return []string{} + } + + value, selected, cursor := v.getValueAtCursor(line, cursor) + + if selected == nil { + return []string{} + } + + if *selected == keySelected { + // Get key documentation + return v.Key.FetchHoverInfo(value, cursor) + } else if *selected == valueSelected { + // Get for value documentation + key := strings.SplitN(line, v.Separator, 2)[0] + + return v.getValue(key).FetchHoverInfo(value, cursor) + } + + return []string{} +} diff --git a/doc-values/value-mask-mode.go b/doc-values/value-mask-mode.go index 945ffc8..85de36a 100644 --- a/doc-values/value-mask-mode.go +++ b/doc-values/value-mask-mode.go @@ -1,8 +1,10 @@ package docvalues import ( - protocol "github.com/tliron/glsp/protocol_3_16" + "fmt" "regexp" + + protocol "github.com/tliron/glsp/protocol_3_16" ) type MaskModeInvalidError struct{} @@ -121,3 +123,44 @@ func (v MaskModeValue) FetchCompletions(line string, cursor uint32) []protocol.C }, } } + +func getMaskRepresentation(digit uint8) string { + switch digit { + case 0: + return "---" + case 1: + return "--x" + case 2: + return "-w-" + case 3: + return "-wx" + case 4: + return "r--" + case 5: + return "r-x" + case 6: + return "rw-" + case 7: + return "rwx" + } + + return "" +} + +func (v MaskModeValue) FetchHoverInfo(line string, cursor uint32) []string { + if !maskModePattern.MatchString(line) { + return []string{} + } + + mask := line + + firstDigit := uint8(mask[1] - 48) + secondDigit := uint8(mask[2] - 48) + thridDigit := uint8(mask[3] - 48) + + representation := getMaskRepresentation(firstDigit) + getMaskRepresentation(secondDigit) + getMaskRepresentation(thridDigit) + + return []string{ + fmt.Sprintf("%s (%s)", mask, representation), + } +} diff --git a/doc-values/value-number.go b/doc-values/value-number.go index b2f05b4..9133db5 100644 --- a/doc-values/value-number.go +++ b/doc-values/value-number.go @@ -76,3 +76,7 @@ func (v NumberValue) CheckIsValid(value string) []*InvalidValue { func (v NumberValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { return []protocol.CompletionItem{} } + +func (v NumberValue) FetchHoverInfo(line string, cursor uint32) []string { + return []string{} +} diff --git a/doc-values/value-or.go b/doc-values/value-or.go index ef4d6a6..6e628ad 100644 --- a/doc-values/value-or.go +++ b/doc-values/value-or.go @@ -77,3 +77,16 @@ func (v OrValue) FetchCompletions(line string, cursor uint32) []protocol.Complet return completions } + +func (v OrValue) FetchHoverInfo(line string, cursor uint32) []string { + for _, subValue := range v.Values { + valueErrors := subValue.CheckIsValid(line) + + if len(valueErrors) == 0 { + // Found + return subValue.FetchHoverInfo(line, cursor) + } + } + + return []string{} +} diff --git a/doc-values/value-path.go b/doc-values/value-path.go index ef22d26..32a689d 100644 --- a/doc-values/value-path.go +++ b/doc-values/value-path.go @@ -81,3 +81,7 @@ func (v PathValue) CheckIsValid(value string) []*InvalidValue { func (v PathValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { return []protocol.CompletionItem{} } + +func (v PathValue) FetchHoverInfo(line string, cursor uint32) []string { + return []string{} +} diff --git a/doc-values/value-power-of-two.go b/doc-values/value-power-of-two.go index 75bba30..110e5a5 100644 --- a/doc-values/value-power-of-two.go +++ b/doc-values/value-power-of-two.go @@ -75,3 +75,7 @@ func (v PowerOfTwoValue) FetchCompletions(line string, cursor uint32) []protocol }, ) } + +func (v PowerOfTwoValue) FetchHoverInfo(line string, cursor uint32) []string { + return []string{} +} diff --git a/doc-values/value-prefix.go b/doc-values/value-prefix.go index 7854f7e..0b5609f 100644 --- a/doc-values/value-prefix.go +++ b/doc-values/value-prefix.go @@ -3,6 +3,8 @@ package docvalues import ( "config-lsp/utils" "fmt" + "strings" + protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -49,3 +51,17 @@ func (v PrefixWithMeaningValue) FetchCompletions(line string, cursor uint32) []p return append(prefixCompletions, v.SubValue.FetchCompletions(line, cursor)...) } + +func (v PrefixWithMeaningValue) FetchHoverInfo(line string, cursor uint32) []string { + for _, prefix := range v.Prefixes { + if strings.HasPrefix(line, prefix.Prefix) { + return append([]string{ + fmt.Sprintf("Prefix: _%s_ -> %s", prefix.Prefix, prefix.Meaning), + }, + v.SubValue.FetchHoverInfo(line[1:], cursor)..., + ) + } + } + + return v.SubValue.FetchHoverInfo(line[1:], cursor) +} diff --git a/doc-values/value-regex.go b/doc-values/value-regex.go index 2be9481..1209953 100644 --- a/doc-values/value-regex.go +++ b/doc-values/value-regex.go @@ -40,3 +40,9 @@ func (v RegexValue) CheckIsValid(value string) []*InvalidValue { func (v RegexValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { return []protocol.CompletionItem{} } + +func (v RegexValue) FetchHoverInfo(line string, cursor uint32) []string { + return []string{ + fmt.Sprintf("Pattern: `%s`", v.Regex.String()), + } +} diff --git a/doc-values/value-string.go b/doc-values/value-string.go index 67b163e..ccfbca8 100644 --- a/doc-values/value-string.go +++ b/doc-values/value-string.go @@ -48,3 +48,7 @@ func (v StringValue) CheckIsValid(value string) []*InvalidValue { func (v StringValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { return []protocol.CompletionItem{} } + +func (v StringValue) FetchHoverInfo(line string, cursor uint32) []string { + return []string{} +} diff --git a/doc-values/value-uid.go b/doc-values/value-uid.go index bfc436e..6de101b 100644 --- a/doc-values/value-uid.go +++ b/doc-values/value-uid.go @@ -127,3 +127,27 @@ func (v UIDValue) FetchCompletions(line string, cursor uint32) []protocol.Comple return completions } + +func (v UIDValue) FetchHoverInfo(line string, cursor uint32) []string { + uid, err := strconv.Atoi(line) + + if err != nil { + return []string{} + } + + infos, err := fetchPasswdInfo() + + if err != nil { + return []string{} + } + + for _, info := range infos { + if info.UID == strconv.Itoa(uid) { + return []string{ + fmt.Sprintf("User %s; ID: %s:%s; ~:%s", info.Name, info.UID, info.GID, info.HomePath), + } + } + } + + return []string{} +} diff --git a/doc-values/value-umask.go b/doc-values/value-umask.go index ed4b54a..fc44e2c 100644 --- a/doc-values/value-umask.go +++ b/doc-values/value-umask.go @@ -110,3 +110,7 @@ func (v UmaskValue) FetchCompletions(line string, cursor uint32) []protocol.Comp }, } } + +func (v UmaskValue) FetchHoverInfo(line string, cursor uint32) []string { + return []string{} +} diff --git a/handlers/fstab/hover-fields.go b/handlers/fstab/hover-fields.go new file mode 100644 index 0000000..38a5aec --- /dev/null +++ b/handlers/fstab/hover-fields.go @@ -0,0 +1,92 @@ +package fstab + +import protocol "github.com/tliron/glsp/protocol_3_16" + +var SpecHoverField = protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: `## The first field (fs_spec). +This field describes the block special device, remote filesystem or filesystem image for loop device to be mounted or swap file or swap device to be enabled. + +For ordinary mounts, it will hold (a link to) a block special device node (as created by mknod(2)) for the device to be mounted, like /dev/cdrom or /dev/sdb7. For NFS mounts, this field is :, +e.g., knuth.aeb.nl:/. For filesystems with no storage, any string can be used, and will show up in df(1) output, for example. Typical usage is proc for procfs; mem, none, or tmpfs for tmpfs. Other special filesystems, like udev and sysfs, are typically not listed in fstab. + +LABEL=