mirror of
https://github.com/Myzel394/config-lsp.git
synced 2025-06-18 23:15:26 +02:00
feat: Adding fstab; Adding root handler; Improvements
This commit is contained in:
parent
a6a4c04af3
commit
49a12c9080
9
common/PARSER.md
Normal file
9
common/PARSER.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Parser
|
||||
|
||||
The parser used in `config-lsp` work on the following principles:
|
||||
|
||||
1. Read the configuration file and divide each line into sections
|
||||
2. On changes, run the "analyzer", which will then actually parse the sections and check for errors
|
||||
3. On completion requests, check which section is being requested and return completions for it
|
||||
4. On hover requests, check which section is being requested and return the hover information for it
|
||||
|
@ -2,10 +2,42 @@ 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
|
||||
}
|
||||
|
||||
func (e ParseError) Error() string {
|
||||
return "Parse error"
|
||||
}
|
||||
func (e ParseError) ToDiagnostic() protocol.Diagnostic {
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
return protocol.Diagnostic{
|
||||
Severity: &severity,
|
||||
Message: e.Err.Error(),
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: e.Line,
|
||||
Character: 0,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: e.Line,
|
||||
Character: 999999,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type SimpleConfigPosition struct {
|
||||
Line uint32
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ func fetchPasswdInfo() ([]passwdInfo, error) {
|
||||
// if `separatorForMultiple` is not empty, it will return an ArrayValue
|
||||
func UserValue(separatorForMultiple string, enforceValues bool) Value {
|
||||
return CustomValue{
|
||||
FetchValue: func() Value {
|
||||
FetchValue: func(context CustomValueContext) Value {
|
||||
infos, err := fetchPasswdInfo()
|
||||
|
||||
if err != nil {
|
||||
@ -125,7 +125,7 @@ func fetchGroupInfo() ([]groupInfo, error) {
|
||||
|
||||
func GroupValue(separatorForMultiple string, enforceValues bool) Value {
|
||||
return CustomValue{
|
||||
FetchValue: func() Value {
|
||||
FetchValue: func(context CustomValueContext) Value {
|
||||
infos, err := fetchGroupInfo()
|
||||
|
||||
if err != nil {
|
||||
|
@ -4,8 +4,20 @@ import (
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
type CustomValueContext interface {
|
||||
GetIsContext() bool
|
||||
}
|
||||
|
||||
type EmptyValueContext struct{}
|
||||
|
||||
func (EmptyValueContext) GetIsContext() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var EmptyValueContextInstance = EmptyValueContext{}
|
||||
|
||||
type CustomValue struct {
|
||||
FetchValue func() Value
|
||||
FetchValue func(context CustomValueContext) Value
|
||||
}
|
||||
|
||||
func (v CustomValue) GetTypeDescription() []string {
|
||||
@ -13,9 +25,9 @@ func (v CustomValue) GetTypeDescription() []string {
|
||||
}
|
||||
|
||||
func (v CustomValue) CheckIsValid(value string) error {
|
||||
return v.FetchValue().CheckIsValid(value)
|
||||
return v.FetchValue(EmptyValueContextInstance).CheckIsValid(value)
|
||||
}
|
||||
|
||||
func (v CustomValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem {
|
||||
return v.FetchValue().FetchCompletions(line, cursor)
|
||||
return v.FetchValue(EmptyValueContextInstance).FetchCompletions(line, cursor)
|
||||
}
|
||||
|
@ -14,8 +14,17 @@ func (e KeyValueAssignmentError) Error() string {
|
||||
return "This is not valid key-value assignment"
|
||||
}
|
||||
|
||||
type KeyValueAssignmentContext struct {
|
||||
SelectedKey string
|
||||
}
|
||||
|
||||
func (KeyValueAssignmentContext) GetIsContext() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type KeyValueAssignmentValue struct {
|
||||
Key Value
|
||||
Key Value
|
||||
// If this is a `CustomValue`, it will receive a `KeyValueAssignmentContext`
|
||||
Value Value
|
||||
ValueIsOptional bool
|
||||
Separator string
|
||||
@ -38,6 +47,24 @@ func (v KeyValueAssignmentValue) GetTypeDescription() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (v KeyValueAssignmentValue) getValue(selectedKey string) Value {
|
||||
switch v.Value.(type) {
|
||||
case CustomValue:
|
||||
{
|
||||
customValue := v.Value.(CustomValue)
|
||||
context := KeyValueAssignmentContext{
|
||||
SelectedKey: selectedKey,
|
||||
}
|
||||
|
||||
return customValue.FetchValue(context)
|
||||
}
|
||||
default:
|
||||
{
|
||||
return v.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v KeyValueAssignmentValue) CheckIsValid(value string) error {
|
||||
parts := strings.Split(value, v.Separator)
|
||||
|
||||
@ -60,7 +87,7 @@ func (v KeyValueAssignmentValue) CheckIsValid(value string) error {
|
||||
return KeyValueAssignmentError{}
|
||||
}
|
||||
|
||||
err = v.Value.CheckIsValid(parts[1])
|
||||
err = v.getValue(parts[0]).CheckIsValid(parts[1])
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@ -77,10 +104,11 @@ func (v KeyValueAssignmentValue) FetchCompletions(line string, cursor uint32) []
|
||||
relativePosition, found := utils.FindPreviousCharacter(line, v.Separator, int(cursor-1))
|
||||
|
||||
if found {
|
||||
selectedKey := line[:uint32(relativePosition)]
|
||||
line = line[uint32(relativePosition+len(v.Separator)):]
|
||||
cursor -= uint32(relativePosition)
|
||||
|
||||
return v.Value.FetchCompletions(line, cursor)
|
||||
return v.getValue(selectedKey).FetchCompletions(line, cursor)
|
||||
} else {
|
||||
return v.Key.FetchCompletions(line, cursor)
|
||||
}
|
||||
|
50
handlers/fstab/documentation.go
Normal file
50
handlers/fstab/documentation.go
Normal file
@ -0,0 +1,50 @@
|
||||
package fstab
|
||||
|
||||
import (
|
||||
docvalues "config-lsp/doc-values"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var uuidField = docvalues.RegexValue{
|
||||
Regex: *regexp.MustCompile(`[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`),
|
||||
}
|
||||
var labelField = docvalues.RegexValue{
|
||||
Regex: *regexp.MustCompile(`\S+`),
|
||||
}
|
||||
|
||||
var specField = docvalues.OrValue{
|
||||
Values: []docvalues.Value{
|
||||
// docvalues.PathValue{
|
||||
// RequiredType: docvalues.PathTypeFile & docvalues.PathTypeExistenceOptional,
|
||||
// },
|
||||
docvalues.KeyValueAssignmentValue{
|
||||
Separator: "=",
|
||||
ValueIsOptional: false,
|
||||
Key: docvalues.EnumValue{
|
||||
EnforceValues: true,
|
||||
Values: []docvalues.EnumString{
|
||||
docvalues.CreateEnumString("UUID"),
|
||||
docvalues.CreateEnumString("LABEL"),
|
||||
docvalues.CreateEnumString("PARTUUID"),
|
||||
docvalues.CreateEnumString("PARTLABEL"),
|
||||
},
|
||||
},
|
||||
Value: docvalues.CustomValue{
|
||||
FetchValue: func(rawContext docvalues.CustomValueContext) docvalues.Value {
|
||||
context := rawContext.(docvalues.KeyValueAssignmentContext)
|
||||
|
||||
switch context.SelectedKey {
|
||||
case "UUID":
|
||||
case "PARTUUID":
|
||||
return uuidField
|
||||
case "LABEL":
|
||||
case "PARTLABEL":
|
||||
return labelField
|
||||
}
|
||||
|
||||
return docvalues.StringValue{}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
240
handlers/fstab/parser.go
Normal file
240
handlers/fstab/parser.go
Normal file
@ -0,0 +1,240 @@
|
||||
package fstab
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
var ignoreLinePattern = regexp.MustCompile(`^\s*(#|$)`)
|
||||
var whitespacePattern = regexp.MustCompile(`\s+`)
|
||||
|
||||
type MalformedLineError struct{}
|
||||
|
||||
func (e MalformedLineError) Error() string {
|
||||
return "Malformed line"
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
Value string
|
||||
Start uint32
|
||||
End uint32
|
||||
}
|
||||
|
||||
func (f *Field) CreateRange(fieldLine uint32) protocol.Range {
|
||||
return protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: fieldLine,
|
||||
Character: f.Start,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: fieldLine,
|
||||
Character: f.End,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type FstabFields struct {
|
||||
Spec *Field
|
||||
MountPoint *Field
|
||||
FilesystemType *Field
|
||||
Options *Field
|
||||
Freq *Field
|
||||
Pass *Field
|
||||
}
|
||||
|
||||
type FstabEntry struct {
|
||||
Line uint32
|
||||
Fields FstabFields
|
||||
}
|
||||
|
||||
func (e *FstabEntry) CheckIsValid() []protocol.Diagnostic {
|
||||
if e.Fields.Spec == nil || e.Fields.MountPoint == nil || e.Fields.FilesystemType == nil || e.Fields.Options == nil || e.Fields.Freq == nil || e.Fields.Pass == nil {
|
||||
severity := protocol.DiagnosticSeverityHint
|
||||
|
||||
return []protocol.Diagnostic{
|
||||
{
|
||||
Message: "This line is not fully filled",
|
||||
Severity: &severity,
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: e.Line,
|
||||
Character: 0,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: e.Line,
|
||||
Character: 9999,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics := make([]protocol.Diagnostic, 0)
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
|
||||
err := specField.CheckIsValid(e.Fields.Spec.Value)
|
||||
|
||||
if err != nil {
|
||||
diagnostics = append(diagnostics, protocol.Diagnostic{
|
||||
Range: e.Fields.Spec.CreateRange(e.Line),
|
||||
Message: err.Error(),
|
||||
Severity: &severity,
|
||||
})
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
||||
|
||||
type FstabParser struct {
|
||||
entries []FstabEntry
|
||||
}
|
||||
|
||||
func (p *FstabParser) AddLine(line string, lineNumber int) error {
|
||||
fields := whitespacePattern.Split(line, -1)
|
||||
|
||||
if len(fields) == 0 {
|
||||
return MalformedLineError{}
|
||||
}
|
||||
|
||||
var spec Field
|
||||
var mountPoint Field
|
||||
var filesystemType Field
|
||||
var options Field
|
||||
var freq Field
|
||||
var pass Field
|
||||
|
||||
switch len(fields) {
|
||||
case 6:
|
||||
value := fields[5]
|
||||
start := uint32(strings.Index(line, value))
|
||||
pass = Field{
|
||||
Value: fields[5],
|
||||
Start: start,
|
||||
End: start + uint32(len(value)),
|
||||
}
|
||||
case 5:
|
||||
value := fields[4]
|
||||
start := uint32(strings.Index(line, value))
|
||||
|
||||
freq = Field{
|
||||
Value: value,
|
||||
Start: start,
|
||||
End: start + uint32(len(value)),
|
||||
}
|
||||
case 4:
|
||||
value := fields[3]
|
||||
start := uint32(strings.Index(line, value))
|
||||
|
||||
options = Field{
|
||||
Value: value,
|
||||
Start: start,
|
||||
End: start + uint32(len(value)),
|
||||
}
|
||||
case 3:
|
||||
value := fields[2]
|
||||
start := uint32(strings.Index(line, value))
|
||||
|
||||
filesystemType = Field{
|
||||
Value: value,
|
||||
Start: start,
|
||||
End: start + uint32(len(value)),
|
||||
}
|
||||
case 2:
|
||||
value := fields[1]
|
||||
start := uint32(strings.Index(line, value))
|
||||
|
||||
mountPoint = Field{
|
||||
Value: value,
|
||||
Start: start,
|
||||
End: start + uint32(len(value)),
|
||||
}
|
||||
case 1:
|
||||
value := fields[0]
|
||||
start := uint32(strings.Index(line, value))
|
||||
|
||||
spec = Field{
|
||||
Value: value,
|
||||
Start: start,
|
||||
End: start + uint32(len(value)),
|
||||
}
|
||||
}
|
||||
|
||||
entry := FstabEntry{
|
||||
Line: uint32(lineNumber),
|
||||
Fields: FstabFields{
|
||||
Spec: &spec,
|
||||
MountPoint: &mountPoint,
|
||||
FilesystemType: &filesystemType,
|
||||
Options: &options,
|
||||
Freq: &freq,
|
||||
Pass: &pass,
|
||||
},
|
||||
}
|
||||
p.entries = append(p.entries, entry)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *FstabParser) ParseFromContent(content string) []common.ParseError {
|
||||
errors := []common.ParseError{}
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
for index, line := range lines {
|
||||
if ignoreLinePattern.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
|
||||
err := p.AddLine(line, index)
|
||||
|
||||
if err != nil {
|
||||
errors = append(errors, common.ParseError{
|
||||
Line: uint32(index),
|
||||
Err: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
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 {
|
||||
return -1
|
||||
}
|
||||
|
||||
if entry.Line > line {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
if !found {
|
||||
return FstabEntry{}, false
|
||||
}
|
||||
|
||||
return p.entries[index], true
|
||||
}
|
||||
|
||||
func (p *FstabParser) Clear() {
|
||||
p.entries = []FstabEntry{}
|
||||
}
|
||||
|
||||
func (p *FstabParser) AnalyzeValues() []protocol.Diagnostic {
|
||||
diagnostics := []protocol.Diagnostic{}
|
||||
|
||||
for _, entry := range p.entries {
|
||||
newDiagnostics := entry.CheckIsValid()
|
||||
|
||||
if len(newDiagnostics) > 0 {
|
||||
diagnostics = append(diagnostics, newDiagnostics...)
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics
|
||||
}
|
7
handlers/fstab/shared.go
Normal file
7
handlers/fstab/shared.go
Normal file
@ -0,0 +1,7 @@
|
||||
package fstab
|
||||
|
||||
import (
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
var documentParserMap = map[protocol.DocumentUri]*FstabParser{}
|
40
handlers/fstab/text-document-did-change.go
Normal file
40
handlers/fstab/text-document-did-change.go
Normal file
@ -0,0 +1,40 @@
|
||||
package fstab
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
"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)
|
||||
|
||||
parser := documentParserMap[params.TextDocument.URI]
|
||||
parser.Clear()
|
||||
|
||||
diagnostics := make([]protocol.Diagnostic, 0)
|
||||
errors := parser.ParseFromContent(content)
|
||||
|
||||
if len(errors) > 0 {
|
||||
diagnostics = append(diagnostics, utils.Map(
|
||||
errors,
|
||||
func(err common.ParseError) protocol.Diagnostic {
|
||||
return err.ToDiagnostic()
|
||||
},
|
||||
)...)
|
||||
}
|
||||
|
||||
diagnostics = append(diagnostics, parser.AnalyzeValues()...)
|
||||
|
||||
if len(diagnostics) > 0 {
|
||||
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
32
handlers/fstab/text-document-did-open.go
Normal file
32
handlers/fstab/text-document-did-open.go
Normal file
@ -0,0 +1,32 @@
|
||||
package fstab
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
"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 := FstabParser{}
|
||||
|
||||
documentParserMap[params.TextDocument.URI] = &parser
|
||||
|
||||
errors := parser.ParseFromContent(params.TextDocument.Text)
|
||||
diagnostics := utils.Map(
|
||||
errors,
|
||||
func(err common.ParseError) protocol.Diagnostic {
|
||||
return err.ToDiagnostic()
|
||||
},
|
||||
)
|
||||
|
||||
common.SendDiagnostics(context, params.TextDocument.URI, diagnostics)
|
||||
|
||||
return nil
|
||||
}
|
@ -711,14 +711,14 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
},
|
||||
},
|
||||
),
|
||||
"PerSourceNetBlockSize": common.NewOption(`Specifies the number of bits of source address that are grouped together for the purposes of applying PerSourceMaxStartups limits. Values for IPv4 and optionally IPv6 may be specified, separated by a colon. The default is 32:128, which means each address is considered individually.`,
|
||||
docvalues.KeyValueAssignmentValue{
|
||||
Separator: ":",
|
||||
ValueIsOptional: false,
|
||||
Key: docvalues.NumberValue{Min: &ZERO},
|
||||
Value: docvalues.NumberValue{Min: &ZERO},
|
||||
},
|
||||
),
|
||||
"PerSourceNetBlockSize": common.NewOption(`Specifies the number of bits of source address that are grouped together for the purposes of applying PerSourceMaxStartups limits. Values for IPv4 and optionally IPv6 may be specified, separated by a colon. The default is 32:128, which means each address is considered individually.`,
|
||||
docvalues.KeyValueAssignmentValue{
|
||||
Separator: ":",
|
||||
ValueIsOptional: false,
|
||||
Key: docvalues.NumberValue{Min: &ZERO},
|
||||
Value: docvalues.NumberValue{Min: &ZERO},
|
||||
},
|
||||
),
|
||||
"PidFile": common.NewOption(`Specifies the file that contains the process ID of the SSH daemon, or none to not write one. The default is /var/run/sshd.pid.`,
|
||||
docvalues.StringValue{},
|
||||
),
|
||||
@ -821,12 +821,12 @@ See PATTERNS in ssh_config(5) for more information on patterns. This keyword may
|
||||
"StrictModes": common.NewOption(`Specifies whether sshd(8) should check file modes and ownership of the user's files and home directory before accepting login. This is normally desirable because novices sometimes accidentally leave their directory or files world-writable. The default is yes. Note that this does not apply to ChrootDirectory, whose permissions and ownership are checked unconditionally.`,
|
||||
BooleanEnumValue,
|
||||
),
|
||||
"Subsystem": common.NewOption(`Configures an external subsystem (e.g. file transfer daemon). Arguments should be a subsystem name and a command (with optional arguments) to execute upon subsystem request.
|
||||
"Subsystem": common.NewOption(`Configures an external subsystem (e.g. file transfer daemon). Arguments should be a subsystem name and a command (with optional arguments) to execute upon subsystem request.
|
||||
The command sftp-server implements the SFTP file transfer subsystem.
|
||||
Alternately the name internal-sftp implements an in- process SFTP server. This may simplify configurations using ChrootDirectory to force a different filesystem root on clients. It accepts the same command line arguments as sftp-server and even though it is in- process, settings such as LogLevel or SyslogFacility do not apply to it and must be set explicitly via command line arguments.
|
||||
By default no subsystems are defined.`,
|
||||
docvalues.StringValue{},
|
||||
),
|
||||
docvalues.StringValue{},
|
||||
),
|
||||
"SyslogFacility": common.NewOption(`Gives the facility code that is used when logging messages from sshd(8). The possible values are: DAEMON, USER, AUTH, LOCAL0, LOCAL1, LOCAL2, LOCAL3, LOCAL4, LOCAL5, LOCAL6, LOCAL7. The default is AUTH.`,
|
||||
docvalues.EnumValue{
|
||||
EnforceValues: true,
|
||||
|
@ -11,6 +11,8 @@ import (
|
||||
func TextDocumentDidOpen(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error {
|
||||
readBytes, err := os.ReadFile(params.TextDocument.URI[len("file://"):])
|
||||
|
||||
println("opened i la language eta", params.TextDocument.LanguageID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
53
main.go
53
main.go
@ -1,67 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
openssh "config-lsp/handlers/openssh"
|
||||
roothandler "config-lsp/root-handler"
|
||||
|
||||
"github.com/tliron/commonlog"
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
"github.com/tliron/glsp/server"
|
||||
|
||||
// Must include a backend implementation
|
||||
// See CommonLog for other options: https://github.com/tliron/commonlog
|
||||
_ "github.com/tliron/commonlog/simple"
|
||||
)
|
||||
|
||||
const lsName = "config-lsp"
|
||||
|
||||
var (
|
||||
version string = "0.0.1"
|
||||
handler protocol.Handler
|
||||
)
|
||||
|
||||
func main() {
|
||||
// This increases logging verbosity (optional)
|
||||
commonlog.Configure(1, nil)
|
||||
|
||||
handler = protocol.Handler{
|
||||
Initialize: initialize,
|
||||
Initialized: initialized,
|
||||
Shutdown: shutdown,
|
||||
SetTrace: setTrace,
|
||||
TextDocumentDidOpen: openssh.TextDocumentDidOpen,
|
||||
TextDocumentDidChange: openssh.TextDocumentDidChange,
|
||||
TextDocumentCompletion: openssh.TextDocumentCompletion,
|
||||
}
|
||||
|
||||
server := server.NewServer(&handler, lsName, false)
|
||||
|
||||
server.RunStdio()
|
||||
}
|
||||
|
||||
func initialize(context *glsp.Context, params *protocol.InitializeParams) (any, error) {
|
||||
capabilities := handler.CreateServerCapabilities()
|
||||
capabilities.TextDocumentSync = protocol.TextDocumentSyncKindFull
|
||||
|
||||
return protocol.InitializeResult{
|
||||
Capabilities: capabilities,
|
||||
ServerInfo: &protocol.InitializeResultServerInfo{
|
||||
Name: lsName,
|
||||
Version: &version,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func initialized(context *glsp.Context, params *protocol.InitializedParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func shutdown(context *glsp.Context) error {
|
||||
protocol.SetTraceValue(protocol.TraceValueOff)
|
||||
return nil
|
||||
}
|
||||
|
||||
func setTrace(context *glsp.Context, params *protocol.SetTraceParams) error {
|
||||
protocol.SetTraceValue(params.Value)
|
||||
return nil
|
||||
roothandler.SetUpRootHandler()
|
||||
}
|
||||
|
58
root-handler/handler.go
Normal file
58
root-handler/handler.go
Normal file
@ -0,0 +1,58 @@
|
||||
package roothandler
|
||||
|
||||
import (
|
||||
"github.com/tliron/glsp"
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
|
||||
"github.com/tliron/glsp/server"
|
||||
)
|
||||
|
||||
const lsName = "config-lsp"
|
||||
|
||||
var version string = "0.0.1"
|
||||
|
||||
var lspHandler protocol.Handler
|
||||
|
||||
// The root handler which handles all the LSP requests and then forwards them to the appropriate handler
|
||||
func SetUpRootHandler() {
|
||||
rootHandler = NewRootHandler()
|
||||
lspHandler = protocol.Handler{
|
||||
Initialize: initialize,
|
||||
Initialized: initialized,
|
||||
Shutdown: shutdown,
|
||||
SetTrace: setTrace,
|
||||
TextDocumentDidOpen: TextDocumentDidOpen,
|
||||
TextDocumentDidChange: TextDocumentDidChange,
|
||||
}
|
||||
|
||||
server := server.NewServer(&lspHandler, lsName, false)
|
||||
|
||||
server.RunStdio()
|
||||
}
|
||||
|
||||
func initialize(context *glsp.Context, params *protocol.InitializeParams) (any, error) {
|
||||
capabilities := lspHandler.CreateServerCapabilities()
|
||||
capabilities.TextDocumentSync = protocol.TextDocumentSyncKindFull
|
||||
|
||||
return protocol.InitializeResult{
|
||||
Capabilities: capabilities,
|
||||
ServerInfo: &protocol.InitializeResultServerInfo{
|
||||
Name: lsName,
|
||||
Version: &version,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func initialized(context *glsp.Context, params *protocol.InitializedParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func shutdown(context *glsp.Context) error {
|
||||
protocol.SetTraceValue(protocol.TraceValueOff)
|
||||
return nil
|
||||
}
|
||||
|
||||
func setTrace(context *glsp.Context, params *protocol.SetTraceParams) error {
|
||||
protocol.SetTraceValue(params.Value)
|
||||
return nil
|
||||
}
|
101
root-handler/lsp-utils.go
Normal file
101
root-handler/lsp-utils.go
Normal file
@ -0,0 +1,101 @@
|
||||
package roothandler
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
"config-lsp/utils"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
type SupportedLanguage string
|
||||
|
||||
const (
|
||||
LanguageSSHDConfig SupportedLanguage = "sshd_config"
|
||||
LanguageFstab SupportedLanguage = "fstab"
|
||||
)
|
||||
|
||||
var AllSupportedLanguages = []string{
|
||||
string(LanguageSSHDConfig),
|
||||
string(LanguageFstab),
|
||||
}
|
||||
|
||||
type FatalFileNotReadableError struct {
|
||||
FileURI protocol.DocumentUri
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e FatalFileNotReadableError) Error() string {
|
||||
return fmt.Sprint("Fatal error! config-lsp was unable to read the file (%s); error: %s", e.FileURI, e.Err.Error())
|
||||
}
|
||||
|
||||
type UnsupportedLanguageError struct {
|
||||
SuggestedLanguage string
|
||||
}
|
||||
|
||||
func (e UnsupportedLanguageError) Error() string {
|
||||
return fmt.Sprint("Language '%s' is not supported. Choose one of: %s", e.SuggestedLanguage, strings.Join(AllSupportedLanguages, ", "))
|
||||
}
|
||||
|
||||
type LanguageUndetectableError struct{}
|
||||
|
||||
func (e LanguageUndetectableError) Error() string {
|
||||
return "Please add: '#?lsp.language=<language>' to the top of the file. config-lsp was unable to detect the appropriate language for this file."
|
||||
}
|
||||
|
||||
var valueToLanguageMap = map[string]SupportedLanguage{
|
||||
"sshd_config": LanguageSSHDConfig,
|
||||
"sshdconfig": LanguageSSHDConfig,
|
||||
"ssh_config": LanguageSSHDConfig,
|
||||
"sshconfig": LanguageSSHDConfig,
|
||||
|
||||
"fstab": LanguageFstab,
|
||||
"etc/fstab": LanguageFstab,
|
||||
}
|
||||
|
||||
var typeOverwriteRegex = regexp.MustCompile(`^#\?\s*lsp\.language\s*=\s*(\w+)\s*$`)
|
||||
|
||||
func DetectLanguage(
|
||||
content string,
|
||||
advertisedLanguage string,
|
||||
uri protocol.DocumentUri,
|
||||
) (SupportedLanguage, error) {
|
||||
if match := typeOverwriteRegex.FindStringSubmatch(content); match != nil {
|
||||
suggestedLanguage := strings.ToLower(match[1])
|
||||
|
||||
foundLanguage, ok := valueToLanguageMap[suggestedLanguage]
|
||||
|
||||
if ok {
|
||||
return foundLanguage, nil
|
||||
}
|
||||
|
||||
matchIndex := strings.Index(content, match[0])
|
||||
contentUntilMatch := content[:matchIndex]
|
||||
|
||||
return "", common.ParseError{
|
||||
Line: uint32(utils.CountCharacterOccurrences(contentUntilMatch, '\n')),
|
||||
Err: UnsupportedLanguageError{
|
||||
SuggestedLanguage: suggestedLanguage,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if language, ok := valueToLanguageMap[advertisedLanguage]; ok {
|
||||
return language, nil
|
||||
}
|
||||
|
||||
switch uri {
|
||||
case "file:///etc/ssh/sshd_config":
|
||||
case "file:///etc/ssh/ssh_config":
|
||||
return LanguageSSHDConfig, nil
|
||||
case "file:///etc/fstab":
|
||||
return LanguageFstab, nil
|
||||
}
|
||||
|
||||
return "", common.ParseError{
|
||||
Line: 0,
|
||||
Err: LanguageUndetectableError{},
|
||||
}
|
||||
}
|
29
root-handler/singleton.go
Normal file
29
root-handler/singleton.go
Normal file
@ -0,0 +1,29 @@
|
||||
package roothandler
|
||||
|
||||
import (
|
||||
protocol "github.com/tliron/glsp/protocol_3_16"
|
||||
)
|
||||
|
||||
var rootHandler RootHandler
|
||||
|
||||
type RootHandler struct {
|
||||
languageMap map[protocol.DocumentUri]SupportedLanguage
|
||||
}
|
||||
|
||||
func NewRootHandler() RootHandler {
|
||||
return RootHandler{
|
||||
languageMap: make(map[protocol.DocumentUri]SupportedLanguage),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RootHandler) AddDocument(uri protocol.DocumentUri, language SupportedLanguage) {
|
||||
h.languageMap[uri] = language
|
||||
}
|
||||
|
||||
func (h *RootHandler) GetLanguageForDocument(uri protocol.DocumentUri) SupportedLanguage {
|
||||
return h.languageMap[uri]
|
||||
}
|
||||
|
||||
func (h *RootHandler) RemoveDocument(uri protocol.DocumentUri) {
|
||||
delete(h.languageMap, uri)
|
||||
}
|
21
root-handler/text-document-did-change.go
Normal file
21
root-handler/text-document-did-change.go
Normal 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 TextDocumentDidChange(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error {
|
||||
language := rootHandler.GetLanguageForDocument(params.TextDocument.URI)
|
||||
|
||||
switch language {
|
||||
case LanguageFstab:
|
||||
return fstab.TextDocumentDidChange(context, params)
|
||||
case LanguageSSHDConfig:
|
||||
return nil
|
||||
}
|
||||
|
||||
panic("root-handler/TextDocumentDidChange: unexpected language" + language)
|
||||
}
|
71
root-handler/text-document-did-open.go
Normal file
71
root-handler/text-document-did-open.go
Normal file
@ -0,0 +1,71 @@
|
||||
package roothandler
|
||||
|
||||
import (
|
||||
"config-lsp/common"
|
||||
fstab "config-lsp/handlers/fstab"
|
||||
"fmt"
|
||||
|
||||
"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)
|
||||
|
||||
// Find the file type
|
||||
content := params.TextDocument.Text
|
||||
language, err := DetectLanguage(content, params.TextDocument.LanguageID, params.TextDocument.URI)
|
||||
|
||||
if err != nil {
|
||||
parseError := err.(common.ParseError)
|
||||
showParseError(
|
||||
context,
|
||||
params.TextDocument.URI,
|
||||
parseError,
|
||||
)
|
||||
|
||||
return parseError.Err
|
||||
}
|
||||
|
||||
// Everything okay, now we can handle the file
|
||||
rootHandler.AddDocument(params.TextDocument.URI, language)
|
||||
|
||||
switch language {
|
||||
case LanguageFstab:
|
||||
return fstab.TextDocumentDidOpen(context, params)
|
||||
case LanguageSSHDConfig:
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected roothandler.SupportedLanguage: %#v", language))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func showParseError(
|
||||
context *glsp.Context,
|
||||
uri protocol.DocumentUri,
|
||||
err common.ParseError,
|
||||
) {
|
||||
severity := protocol.DiagnosticSeverityError
|
||||
|
||||
common.SendDiagnostics(
|
||||
context,
|
||||
uri,
|
||||
[]protocol.Diagnostic{
|
||||
{
|
||||
Severity: &severity,
|
||||
Message: err.Err.Error(),
|
||||
Range: protocol.Range{
|
||||
Start: protocol.Position{
|
||||
Line: err.Line,
|
||||
Character: 0,
|
||||
},
|
||||
End: protocol.Position{
|
||||
Line: err.Line,
|
||||
Character: 99999,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
13
utils/text.go
Normal file
13
utils/text.go
Normal file
@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
func CountCharacterOccurrences(line string, character rune) int {
|
||||
count := 0
|
||||
|
||||
for _, c := range line {
|
||||
if c == character {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user