feat: Adding fstab; Adding root handler; Improvements

This commit is contained in:
Myzel394 2024-08-04 14:59:12 +02:00
parent a6a4c04af3
commit 49a12c9080
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
19 changed files with 766 additions and 70 deletions

9
common/PARSER.md Normal file
View 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

View File

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

View File

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

View File

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

View File

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

View 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
View 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
View File

@ -0,0 +1,7 @@
package fstab
import (
protocol "github.com/tliron/glsp/protocol_3_16"
)
var documentParserMap = map[protocol.DocumentUri]*FstabParser{}

View 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
}

View 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
}

View File

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

View File

@ -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
View File

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

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

View 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
View 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
}