feat: Add text document hover; Improvements

This commit is contained in:
Myzel394 2024-08-10 22:33:14 +02:00
parent 5f154553f5
commit a8e896c6ef
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
26 changed files with 669 additions and 80 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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{}
}

View File

@ -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{}
}

View File

@ -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()),
}
}
}
}

View File

@ -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<value>`", 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{}
}

View File

@ -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{}
}

View File

@ -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),
}
}

View File

@ -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{}
}

View File

@ -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{}
}

View File

@ -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{}
}

View File

@ -75,3 +75,7 @@ func (v PowerOfTwoValue) FetchCompletions(line string, cursor uint32) []protocol
},
)
}
func (v PowerOfTwoValue) FetchHoverInfo(line string, cursor uint32) []string {
return []string{}
}

View File

@ -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)
}

View File

@ -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()),
}
}

View File

@ -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{}
}

View File

@ -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{}
}

View File

@ -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{}
}

View File

@ -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 <host>:<dir>,
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=<label> or UUID=<uuid> may be given instead of a device name. This is the recommended method, as device names are often a coincidence of hardware detection order, and can change when other disks are added or removed. For example, 'LABEL=Boot' or 'UUID=3e6be9de-8139-11d1-9106-a43f08d823a6'. (Use a filesystem-specific tool like e2label(8), xfs_admin(8), or fatlabel(8) to set LABELs on filesystems).
Its also possible to use PARTUUID= and PARTLABEL=. These partitions identifiers are supported for example for GUID Partition Table (GPT).
See mount(8), blkid(8) or lsblk(8) for more details about device identifiers.
Note that mount(8) uses UUIDs as strings. The string representation of the UUID should be based on lower case characters. But when specifying the volume ID of FAT or NTFS file systems upper case characters are used (e.g UUID="A40D-85E7" or UUID="61DB7756DB7779B3").`,
},
}
var MountPointHoverField = protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.MarkupKindMarkdown,
Value: `## The second field (fs_file).
This field describes the mount point (target) for the filesystem. For swap area, this field should be specified as 'none'. If the name of the mount point contains spaces or tabs these can be escaped as '\040' and '\011' respectively.`,
},
}
var FileSystemTypeField = protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.MarkupKindMarkdown,
Value: `## The third field (fs_vfstype).
This field describes the type of the filesystem. Linux supports many filesystem types: ext4, xfs, btrfs, f2fs, vfat, ntfs, hfsplus, tmpfs, sysfs, proc, iso9660, udf, squashfs, nfs, cifs, and many more. For more details, see mount(8).
An entry swap denotes a file or partition to be used for swapping, cf. swapon(8). An entry none is useful for bind or move mounts.
More than one type may be specified in a comma-separated list.
mount(8) and umount(8) support filesystem subtypes. The subtype is defined by '.subtype' suffix. For example 'fuse.sshfs'. Its recommended to use subtype notation rather than add any prefix to the first fstab field (for example 'sshfs#example.com' is deprecated).`,
},
}
var MountOptionsField = protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.MarkupKindMarkdown,
Value: `## The fourth field (fs_mntops).
This field describes the mount options associated with the filesystem.
It is formatted as a comma-separated list of options and is optional for mount(8) or swapon(8). The usual convention is to use at least "defaults" keyword there.
It usually contains the type of mount (ro or rw, the default is rw), plus any additional options appropriate to the filesystem type (including performance-tuning options). For details, see mount(8) or swapon(8).
Basic filesystem-independent options are:
defaults
use default options. The default depends on the kernel and the filesystem. mount(8) does not have any hardcoded set of default options. The kernel default is usually rw, suid, dev, exec, auto, nouser, and async.
noauto
do not mount when mount -a is given (e.g., at boot time)
user
allow a user to mount
owner
allow device owner to mount
comment
or x-<name> for use by fstab-maintaining programs
nofail
do not report errors for this device if it does not exist.`,
},
}
var FreqHoverField = protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.MarkupKindMarkdown,
Value: `## The fifth field (fs_freq).
This field is used by dump(8) to determine which filesystems need to be dumped. Defaults to zero (dont dump) if not present.`,
},
}
var PassHoverField = protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.MarkupKindMarkdown,
Value: `## The sixth field (fs_passno).
This field is used by fsck(8) to determine the order in which filesystem checks are done at boot time. The root filesystem should be specified with a fs_passno of 1. Other filesystems should have a fs_passno of 2. Filesystems within a drive will be checked sequentially, but filesystems on different drives will be checked at the same time to utilize parallelism available in the hardware. Defaults to zero (dont check the filesystem) if not present.`,
},
}

View File

@ -12,8 +12,9 @@ import (
protocol "github.com/tliron/glsp/protocol_3_16"
)
var ignoreLinePattern = regexp.MustCompile(`^\s*(#|$)`)
var whitespacePattern = regexp.MustCompile(`\s+`)
var commentPattern = regexp.MustCompile(`^\s*#`)
var ignoreLinePattern = regexp.MustCompile(`^\s*$`)
var whitespacePattern = regexp.MustCompile(`\S+`)
type MalformedLineError struct{}
@ -76,12 +77,12 @@ func (f FstabFields) String() string {
)
}
type FstabEntry struct {
type FstabLine struct {
Line uint32
Fields FstabFields
}
func (e *FstabEntry) CheckIsValid() []protocol.Diagnostic {
func (e *FstabLine) CheckIsValid() []protocol.Diagnostic {
diagnostics := make([]protocol.Diagnostic, 0)
if e.Fields.Spec != nil {
@ -165,7 +166,7 @@ func (e *FstabEntry) CheckIsValid() []protocol.Diagnostic {
return diagnostics
}
func (e FstabEntry) GetFieldAtPosition(cursor uint32) FstabField {
func (e FstabLine) GetFieldAtPosition(cursor uint32) FstabField {
if e.Fields.Spec == nil || (cursor >= e.Fields.Spec.Start && cursor <= e.Fields.Spec.End) {
return FstabFieldSpec
}
@ -182,6 +183,7 @@ func (e FstabEntry) GetFieldAtPosition(cursor uint32) FstabField {
return FstabFieldOptions
}
println(fmt.Sprintf("cursor: %v, freq: %v", cursor, e.Fields.Freq))
if e.Fields.Freq == nil || (cursor >= e.Fields.Freq.Start && cursor <= e.Fields.Freq.End) {
return FstabFieldFreq
}
@ -189,12 +191,24 @@ func (e FstabEntry) GetFieldAtPosition(cursor uint32) FstabField {
return FstabFieldPass
}
type FstabEntryType string
const (
FstabEntryTypeLine FstabEntryType = "line"
FstabEntryTypeComment FstabEntryType = "comment"
)
type FstabEntry struct {
Type FstabEntryType
Line FstabLine
}
type FstabParser struct {
entries []FstabEntry
}
func (p *FstabParser) AddLine(line string, lineNumber int) error {
fields := whitespacePattern.Split(line, -1)
fields := whitespacePattern.FindAllStringIndex(line, -1)
if len(fields) == 0 {
return MalformedLineError{}
@ -209,95 +223,84 @@ func (p *FstabParser) AddLine(line string, lineNumber int) error {
switch len(fields) {
case 6:
value := fields[5]
start := uint32(strings.Index(line, value))
if start == 0 {
start = uint32(len(line))
}
field := fields[5]
start := uint32(field[0])
end := uint32(field[1])
pass = &Field{
Value: fields[5],
Value: line[start:end],
Start: start,
End: start + uint32(len(value)),
End: end,
}
fallthrough
case 5:
value := fields[4]
start := uint32(strings.Index(line, value))
if start == 0 {
start = uint32(len(line))
}
field := fields[4]
start := uint32(field[0])
end := uint32(field[1])
freq = &Field{
Value: value,
Value: line[start:end],
Start: start,
End: start + uint32(len(value)),
End: end,
}
fallthrough
case 4:
value := fields[3]
start := uint32(strings.Index(line, value))
if start == 0 {
start = uint32(len(line))
}
field := fields[3]
start := uint32(field[0])
end := uint32(field[1])
options = &Field{
Value: value,
Value: line[start:end],
Start: start,
End: start + uint32(len(value)),
End: end,
}
fallthrough
case 3:
value := fields[2]
start := uint32(strings.Index(line, value))
if start == 0 {
start = uint32(len(line))
}
field := fields[2]
start := uint32(field[0])
end := uint32(field[1])
filesystemType = &Field{
Value: value,
Value: line[start:end],
Start: start,
End: start + uint32(len(value)),
End: end,
}
fallthrough
case 2:
value := fields[1]
start := uint32(strings.Index(line, value))
if start == 0 {
start = uint32(len(line))
}
field := fields[1]
start := uint32(field[0])
end := uint32(field[1])
mountPoint = &Field{
Value: value,
Value: line[start:end],
Start: start,
End: start + uint32(len(value)),
End: end,
}
fallthrough
case 1:
value := fields[0]
start := uint32(strings.Index(line, value))
field := fields[0]
start := uint32(field[0])
end := uint32(field[1])
spec = &Field{
Value: value,
Value: line[start:end],
Start: start,
End: start + uint32(len(value)),
End: end,
}
}
entry := FstabEntry{
Line: uint32(lineNumber),
Fields: FstabFields{
Spec: spec,
MountPoint: mountPoint,
FilesystemType: filesystemType,
Options: options,
Freq: freq,
Pass: pass,
Type: FstabEntryTypeLine,
Line: FstabLine{
Line: uint32(lineNumber),
Fields: FstabFields{
Spec: spec,
MountPoint: mountPoint,
FilesystemType: filesystemType,
Options: options,
Freq: freq,
Pass: pass,
},
},
}
p.entries = append(p.entries, entry)
@ -305,6 +308,16 @@ func (p *FstabParser) AddLine(line string, lineNumber int) error {
return nil
}
func (p *FstabParser) AddCommentLine(line string, lineNumber int) {
entry := FstabLine{
Line: uint32(lineNumber),
}
p.entries = append(p.entries, FstabEntry{
Type: FstabEntryTypeComment,
Line: entry,
})
}
func (p *FstabParser) ParseFromContent(content string) []common.ParseError {
errors := []common.ParseError{}
lines := strings.Split(content, "\n")
@ -314,6 +327,11 @@ func (p *FstabParser) ParseFromContent(content string) []common.ParseError {
continue
}
if commentPattern.MatchString(line) {
p.AddCommentLine(line, index)
continue
}
err := p.AddLine(line, index)
if err != nil {
@ -329,11 +347,11 @@ func (p *FstabParser) ParseFromContent(content string) []common.ParseError {
func (p *FstabParser) GetEntry(line uint32) (*FstabEntry, bool) {
index, found := slices.BinarySearchFunc(p.entries, line, func(entry FstabEntry, line uint32) int {
if entry.Line < line {
if entry.Line.Line < line {
return -1
}
if entry.Line > line {
if entry.Line.Line > line {
return 1
}
@ -355,10 +373,15 @@ func (p *FstabParser) AnalyzeValues() []protocol.Diagnostic {
diagnostics := []protocol.Diagnostic{}
for _, entry := range p.entries {
newDiagnostics := entry.CheckIsValid()
switch entry.Type {
case FstabEntryTypeLine:
newDiagnostics := entry.Line.CheckIsValid()
if len(newDiagnostics) > 0 {
diagnostics = append(diagnostics, newDiagnostics...)
if len(newDiagnostics) > 0 {
diagnostics = append(diagnostics, newDiagnostics...)
}
case FstabEntryTypeComment:
// Do nothing
}
}

View File

@ -22,32 +22,33 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa
}
cursor := params.Position.Character
targetField := entry.GetFieldAtPosition(cursor)
line := entry.Line
targetField := line.GetFieldAtPosition(cursor)
switch targetField {
case FstabFieldSpec:
value, cursor := GetFieldSafely(entry.Fields.Spec, cursor)
value, cursor := GetFieldSafely(line.Fields.Spec, cursor)
return fstabdocumentation.SpecField.FetchCompletions(
value,
cursor,
), nil
case FstabFieldMountPoint:
value, cursor := GetFieldSafely(entry.Fields.MountPoint, cursor)
value, cursor := GetFieldSafely(line.Fields.MountPoint, cursor)
return fstabdocumentation.MountPointField.FetchCompletions(
value,
cursor,
), nil
case FstabFieldFileSystemType:
value, cursor := GetFieldSafely(entry.Fields.FilesystemType, cursor)
value, cursor := GetFieldSafely(line.Fields.FilesystemType, cursor)
return fstabdocumentation.FileSystemTypeField.FetchCompletions(
value,
cursor,
), nil
case FstabFieldOptions:
fileSystemType := entry.Fields.FilesystemType.Value
fileSystemType := line.Fields.FilesystemType.Value
var optionsField docvalues.Value
@ -57,7 +58,7 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa
optionsField = fstabdocumentation.DefaultMountOptionsField
}
value, cursor := GetFieldSafely(entry.Fields.Options, cursor)
value, cursor := GetFieldSafely(line.Fields.Options, cursor)
completions := optionsField.FetchCompletions(
value,
@ -66,14 +67,14 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa
return completions, nil
case FstabFieldFreq:
value, cursor := GetFieldSafely(entry.Fields.Freq, cursor)
value, cursor := GetFieldSafely(line.Fields.Freq, cursor)
return fstabdocumentation.FreqField.FetchCompletions(
value,
cursor,
), nil
case FstabFieldPass:
value, cursor := GetFieldSafely(entry.Fields.Pass, cursor)
value, cursor := GetFieldSafely(line.Fields.Pass, cursor)
return fstabdocumentation.PassField.FetchCompletions(
value,

View File

@ -0,0 +1,67 @@
package fstab
import (
docvalues "config-lsp/doc-values"
fstabdocumentation "config-lsp/handlers/fstab/documentation"
"strings"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentHover(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
cursor := params.Position.Character
parser := documentParserMap[params.TextDocument.URI]
entry, found := parser.GetEntry(params.Position.Line)
// Empty line
if !found {
return nil, nil
}
// Comment line
if entry.Type == FstabEntryTypeComment {
return nil, nil
}
line := entry.Line
targetField := line.GetFieldAtPosition(cursor)
switch targetField {
case FstabFieldSpec:
return &SpecHoverField, nil
case FstabFieldMountPoint:
return &MountPointHoverField, nil
case FstabFieldFileSystemType:
return &FileSystemTypeField, nil
case FstabFieldOptions:
fileSystemType := line.Fields.FilesystemType.Value
var optionsField docvalues.Value
if foundField, found := fstabdocumentation.MountOptionsMapField[fileSystemType]; found {
optionsField = foundField
} else {
optionsField = fstabdocumentation.DefaultMountOptionsField
}
relativeCursor := cursor - line.Fields.Options.Start
fieldInfo := optionsField.FetchHoverInfo(line.Fields.Options.Value, relativeCursor)
hover := protocol.Hover{
Contents: protocol.MarkupContent{
Kind: protocol.MarkupKindMarkdown,
Value: strings.Join(fieldInfo, "\n"),
},
}
return &hover, nil
case FstabFieldFreq:
return &FreqHoverField, nil
case FstabFieldPass:
return &PassHoverField, nil
}
return nil, nil
}

View File

@ -24,6 +24,7 @@ func SetUpRootHandler() {
TextDocumentDidOpen: TextDocumentDidOpen,
TextDocumentDidChange: TextDocumentDidChange,
TextDocumentCompletion: TextDocumentCompletion,
TextDocumentHover: TextDocumentHover,
}
server := server.NewServer(&lspHandler, lsName, false)

View File

@ -0,0 +1,21 @@
package roothandler
import (
"config-lsp/handlers/fstab"
"github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func TextDocumentHover(context *glsp.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
language := rootHandler.GetLanguageForDocument(params.TextDocument.URI)
switch language {
case LanguageFstab:
return fstab.TextDocumentHover(context, params)
case LanguageSSHDConfig:
return nil, nil
}
panic("root-handler/TextDocumentHover: unexpected language" + language)
}

View File

@ -136,6 +136,16 @@ func FindPreviousCharacter(line string, character string, startIndex int) (int,
return 0, false
}
func FindNextCharacter(line string, character string, startIndex int) (int, bool) {
for index := startIndex; index < len(line); index++ {
if string(line[index]) == character {
return index, true
}
}
return 0, false
}
func MergeMaps[T comparable, O any](maps ...map[T]O) map[T]O {
result := make(map[T]O)

7
utils/strings.go Normal file
View File

@ -0,0 +1,7 @@
package utils
import "strings"
func IndexOffset(s string, search string, start int) int {
return strings.Index(s[start:], search) + start
}