From 49a12c9080de16bf9ab9dc189b3f715dde8b315d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 4 Aug 2024 14:59:12 +0200 Subject: [PATCH] feat: Adding fstab; Adding root handler; Improvements --- common/PARSER.md | 9 + common/parser.go | 32 +++ doc-values/extra-values.go | 4 +- doc-values/value-custom.go | 18 +- doc-values/value-key-value-assignment.go | 34 ++- handlers/fstab/documentation.go | 50 +++++ handlers/fstab/parser.go | 240 +++++++++++++++++++++ handlers/fstab/shared.go | 7 + handlers/fstab/text-document-did-change.go | 40 ++++ handlers/fstab/text-document-did-open.go | 32 +++ handlers/openssh/documentation.go | 22 +- handlers/openssh/text-document-did-open.go | 2 + main.go | 53 +---- root-handler/handler.go | 58 +++++ root-handler/lsp-utils.go | 101 +++++++++ root-handler/singleton.go | 29 +++ root-handler/text-document-did-change.go | 21 ++ root-handler/text-document-did-open.go | 71 ++++++ utils/text.go | 13 ++ 19 files changed, 766 insertions(+), 70 deletions(-) create mode 100644 common/PARSER.md create mode 100644 handlers/fstab/documentation.go create mode 100644 handlers/fstab/parser.go create mode 100644 handlers/fstab/shared.go create mode 100644 handlers/fstab/text-document-did-change.go create mode 100644 handlers/fstab/text-document-did-open.go create mode 100644 root-handler/handler.go create mode 100644 root-handler/lsp-utils.go create mode 100644 root-handler/singleton.go create mode 100644 root-handler/text-document-did-change.go create mode 100644 root-handler/text-document-did-open.go create mode 100644 utils/text.go diff --git a/common/PARSER.md b/common/PARSER.md new file mode 100644 index 0000000..dcc5f4a --- /dev/null +++ b/common/PARSER.md @@ -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 + diff --git a/common/parser.go b/common/parser.go index 6e097d1..68dc743 100644 --- a/common/parser.go +++ b/common/parser.go @@ -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 } diff --git a/doc-values/extra-values.go b/doc-values/extra-values.go index dd2b07a..59aa99d 100644 --- a/doc-values/extra-values.go +++ b/doc-values/extra-values.go @@ -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 { diff --git a/doc-values/value-custom.go b/doc-values/value-custom.go index e0d0021..9fdc464 100644 --- a/doc-values/value-custom.go +++ b/doc-values/value-custom.go @@ -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) } diff --git a/doc-values/value-key-value-assignment.go b/doc-values/value-key-value-assignment.go index 72236ab..e098929 100644 --- a/doc-values/value-key-value-assignment.go +++ b/doc-values/value-key-value-assignment.go @@ -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) } diff --git a/handlers/fstab/documentation.go b/handlers/fstab/documentation.go new file mode 100644 index 0000000..fa97a52 --- /dev/null +++ b/handlers/fstab/documentation.go @@ -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{} + }, + }, + }, + }, +} diff --git a/handlers/fstab/parser.go b/handlers/fstab/parser.go new file mode 100644 index 0000000..25388a8 --- /dev/null +++ b/handlers/fstab/parser.go @@ -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 +} diff --git a/handlers/fstab/shared.go b/handlers/fstab/shared.go new file mode 100644 index 0000000..05aaebb --- /dev/null +++ b/handlers/fstab/shared.go @@ -0,0 +1,7 @@ +package fstab + +import ( + protocol "github.com/tliron/glsp/protocol_3_16" +) + +var documentParserMap = map[protocol.DocumentUri]*FstabParser{} diff --git a/handlers/fstab/text-document-did-change.go b/handlers/fstab/text-document-did-change.go new file mode 100644 index 0000000..b192dcf --- /dev/null +++ b/handlers/fstab/text-document-did-change.go @@ -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 +} diff --git a/handlers/fstab/text-document-did-open.go b/handlers/fstab/text-document-did-open.go new file mode 100644 index 0000000..215fcc0 --- /dev/null +++ b/handlers/fstab/text-document-did-open.go @@ -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 +} diff --git a/handlers/openssh/documentation.go b/handlers/openssh/documentation.go index 5ae2e03..c254a09 100644 --- a/handlers/openssh/documentation.go +++ b/handlers/openssh/documentation.go @@ -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, diff --git a/handlers/openssh/text-document-did-open.go b/handlers/openssh/text-document-did-open.go index 7c533e3..652cfba 100644 --- a/handlers/openssh/text-document-did-open.go +++ b/handlers/openssh/text-document-did-open.go @@ -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 } diff --git a/main.go b/main.go index 8765cca..763d422 100644 --- a/main.go +++ b/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() } diff --git a/root-handler/handler.go b/root-handler/handler.go new file mode 100644 index 0000000..f2d7c9d --- /dev/null +++ b/root-handler/handler.go @@ -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 +} diff --git a/root-handler/lsp-utils.go b/root-handler/lsp-utils.go new file mode 100644 index 0000000..9954660 --- /dev/null +++ b/root-handler/lsp-utils.go @@ -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=' 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{}, + } +} diff --git a/root-handler/singleton.go b/root-handler/singleton.go new file mode 100644 index 0000000..2fe7073 --- /dev/null +++ b/root-handler/singleton.go @@ -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) +} diff --git a/root-handler/text-document-did-change.go b/root-handler/text-document-did-change.go new file mode 100644 index 0000000..573459e --- /dev/null +++ b/root-handler/text-document-did-change.go @@ -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) +} diff --git a/root-handler/text-document-did-open.go b/root-handler/text-document-did-open.go new file mode 100644 index 0000000..8228249 --- /dev/null +++ b/root-handler/text-document-did-open.go @@ -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, + }, + }, + }, + }, + ) +} diff --git a/utils/text.go b/utils/text.go new file mode 100644 index 0000000..26183b3 --- /dev/null +++ b/utils/text.go @@ -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 +}