diff --git a/.gitignore b/.gitignore index c014b11..16ba9f7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ go.work.sum # env file .env + +test.lua diff --git a/common/parser.go b/common/parser.go new file mode 100644 index 0000000..8336d17 --- /dev/null +++ b/common/parser.go @@ -0,0 +1,154 @@ +package common + +import ( + "fmt" + "regexp" + "strings" +) + +type SimpleConfigPosition struct { + Line int +} + +type SimpleConfigLine struct { + Value string + Position SimpleConfigPosition +} + +func (l SimpleConfigLine) IsCursorAtRootOption(cursor int) bool { + if cursor <= len(l.Value) { + return true + } + + return false +} + +type SimpleConfigOptions struct { + Separator string + IgnorePattern regexp.Regexp + AvailableOptions *map[string]Option +} + +type SimpleConfigParser struct { + Lines map[string]SimpleConfigLine + Options SimpleConfigOptions +} + +type OptionAlreadyExistsError struct { + Option string +} +func (e OptionAlreadyExistsError) Error() string { + return fmt.Sprintf("Option %s already exists", e.Option) +} +type OptionUnknownError struct { + Option string +} +func (e OptionUnknownError) Error() string { + return fmt.Sprintf("Option '%s' does not exist", e.Option) +} +type MalformedLineError struct { + Line string +} +func (e MalformedLineError) Error() string { + return fmt.Sprintf("Malformed line: %s", e.Line) +} +type LineNotFoundError struct {} +func (e LineNotFoundError) Error() string { + return "Line not found" +} + +func (p *SimpleConfigParser) AddLine(line string, lineNumber int) error { + parts := strings.SplitN(line, p.Options.Separator, 2) + + if len(parts) == 0 { + return MalformedLineError{ + Line: line, + } + } + + option := parts[0] + + if _, exists := (*p.Options.AvailableOptions)[option]; !exists { + return OptionUnknownError{ + Option: option, + } + } + + value := "" + + if len(parts) > 1 { + value = parts[1] + } + + if _, exists := p.Lines[option]; exists { + return OptionAlreadyExistsError{ + Option: option, + } + } + + p.Lines[option] = SimpleConfigLine{ + Value: value, + Position: SimpleConfigPosition{ + Line: lineNumber, + }, + } + + return nil + +} + +func (p *SimpleConfigParser) ReplaceOption(option string, value string) { + p.Lines[option] = SimpleConfigLine{ + Value: value, + Position: SimpleConfigPosition{ + Line: p.Lines[option].Position.Line, + }, + } +} + +func (p *SimpleConfigParser) RemoveOption(option string) { + delete(p.Lines, option) +} + +func (p *SimpleConfigParser) UpsertOption(option string, value string) { + if _, exists := p.Lines[option]; exists { + p.ReplaceOption(option, value) + } else { + p.AddLine(option + p.Options.Separator + value, len(p.Lines)) + } +} + +func (p *SimpleConfigParser) ParseFromFile(content string) []error { + lines := strings.Split(content, "\n") + errors := make([]error, 0) + + for index, line := range lines { + if p.Options.IgnorePattern.MatchString(line) { + continue + } + + err := p.AddLine(line, index) + + if err != nil { + errors = append(errors, err) + } + } + + return errors +} + +func (p *SimpleConfigParser) Clear() { + clear(p.Lines) +} + +// TODO: Use better approach: Store an extra array of lines in order; with references to the SimpleConfigLine +func (p SimpleConfigParser) FindByLineNumber(lineNumber int) (string, SimpleConfigLine, error) { + for option, line := range p.Lines { + if line.Position.Line == lineNumber { + return option, line, nil + } + } + + return "", SimpleConfigLine{Value: "", Position: SimpleConfigPosition{Line: 0}}, LineNotFoundError{} +} + diff --git a/handlers/openssh/shared.go b/handlers/openssh/shared.go new file mode 100644 index 0000000..79cdc2c --- /dev/null +++ b/handlers/openssh/shared.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "config-lsp/common" + "regexp" +) + +func createOpenSSHConfigParser() common.SimpleConfigParser { + pattern, err := regexp.Compile(`^(?:#|\s*$)`) + + if (err != nil) { + panic(err) + } + + return common.SimpleConfigParser{ + Lines: make(map[string]common.SimpleConfigLine), + Options: common.SimpleConfigOptions{ + Separator: " ", + IgnorePattern: *pattern, + AvailableOptions: &Options, + }, + } +} + +var Parser = createOpenSSHConfigParser() + diff --git a/handlers/openssh/text-document-completion.go b/handlers/openssh/text-document-completion.go index 81a10e4..82313b2 100644 --- a/handlers/openssh/text-document-completion.go +++ b/handlers/openssh/text-document-completion.go @@ -2,7 +2,7 @@ package handlers import ( "config-lsp/common" - "strings" + "errors" "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" @@ -12,20 +12,19 @@ import ( ) func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (interface{}, error) { - line, err := common.GetLine(params.TextDocument.URI, int(params.Position.Line)) + option, line, err := Parser.FindByLineNumber(int(params.Position.Line)) - if err != nil { - return [...]protocol.CompletionItem{}, err - } - - rootOption := getCurrentOption(line, int(params.Position.Character)) - - if (rootOption == "") { + if err == nil { + if line.IsCursorAtRootOption(int(params.Position.Character)) { + return getRootCompletions(), nil + } else { + return getOptionCompletions(option), nil + } + } else if errors.Is(err, common.LineNotFoundError{}) { return getRootCompletions(), nil - } else { - return getOptionCompletions(rootOption), nil } + return nil, err } func getRootCompletions() []protocol.CompletionItem { @@ -73,17 +72,3 @@ func getOptionCompletions(optionName string) []protocol.CompletionItem { return []protocol.CompletionItem{} } -func getCurrentOption(line string, position int) string { - words := strings.Split(line, " ") - - if len(words) == 0 { - return "" - } - - if (position <= len(words[0])) { - return "" - } - - return words[0] -} - diff --git a/handlers/openssh/text-document-did-change.go b/handlers/openssh/text-document-did-change.go new file mode 100644 index 0000000..f3a84c8 --- /dev/null +++ b/handlers/openssh/text-document-did-change.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +// Todo: Implement incremental parsing +func TextDocumentDidChange(context *glsp.Context, params *protocol.DidChangeTextDocumentParams) error { + content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text + + Parser.Clear() + Parser.ParseFromFile(content) + + return nil +} diff --git a/handlers/openssh/text-document-did-open.go b/handlers/openssh/text-document-did-open.go new file mode 100644 index 0000000..15ccdd9 --- /dev/null +++ b/handlers/openssh/text-document-did-open.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "os" + + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TextDocumentDidOpen(context *glsp.Context, params *protocol.DidOpenTextDocumentParams) error { + readBytes, err := os.ReadFile(params.TextDocument.URI[len("file://"):]) + + if err != nil { + return err + } + + errors := Parser.ParseFromFile(string(readBytes)) + + if len(errors) > 0 { + return errors[0] + } + + return nil +} + + diff --git a/main.go b/main.go index 23f9927..b8d1bc7 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,6 @@ package main import ( - "fmt" - openssh "config-lsp/handlers/openssh" "github.com/tliron/commonlog" @@ -27,10 +25,12 @@ func main() { commonlog.Configure(1, nil) handler = protocol.Handler{ - Initialize: initialize, + Initialize: initialize, Initialized: initialized, - Shutdown: shutdown, - SetTrace: setTrace, + Shutdown: shutdown, + SetTrace: setTrace, + TextDocumentDidOpen: openssh.TextDocumentDidOpen, + TextDocumentDidChange: openssh.TextDocumentDidChange, TextDocumentCompletion: openssh.TextDocumentCompletion, } @@ -41,6 +41,7 @@ func main() { func initialize(context *glsp.Context, params *protocol.InitializeParams) (any, error) { capabilities := handler.CreateServerCapabilities() + capabilities.TextDocumentSync = protocol.TextDocumentSyncKindFull return protocol.InitializeResult{ Capabilities: capabilities,