diff options
Diffstat (limited to 'internal/lsp/server.go')
| -rw-r--r-- | internal/lsp/server.go | 385 |
1 files changed, 230 insertions, 155 deletions
diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 13066f7..7b8bc88 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -10,29 +10,26 @@ import ( "sync" "time" + "codeberg.org/snonux/hexai/internal/appconfig" "codeberg.org/snonux/hexai/internal/llm" "codeberg.org/snonux/hexai/internal/logging" + "codeberg.org/snonux/hexai/internal/runtimeconfig" ) // Server implements a minimal LSP over stdio. type Server struct { - in *bufio.Reader - out io.Writer - outMu sync.Mutex - logger *log.Logger - exited bool - mu sync.RWMutex - docs map[string]*document - logContext bool - llmClient llm.Client - lastInput time.Time - maxTokens int - contextMode string - windowLines int - maxContextTokens int - triggerChars []string - // If set, used as the LSP coding temperature for all LLM calls - codingTemperature *float64 + in *bufio.Reader + out io.Writer + outMu sync.Mutex + logger *log.Logger + exited bool + mu sync.RWMutex + docs map[string]*document + logContext bool + configStore *runtimeconfig.Store + cfg appconfig.App + llmClient llm.Client + lastInput time.Time // LLM request stats llmReqTotal int64 llmSentBytesTotal int64 @@ -43,58 +40,18 @@ type Server struct { compCache map[string]string compCacheOrder []string // most-recent at end; cap ~10 // Outgoing JSON-RPC id counter for server-initiated requests - nextID int64 - // Minimum identifier chars required for manual invoke to bypass prefix checks - manualInvokeMinPrefix int - - // Debounce and throttle settings - completionDebounce time.Duration - throttleInterval time.Duration - lastLLMCall time.Time + nextID int64 + lastLLMCall time.Time // Dispatch table for JSON-RPC methods → handler functions handlers map[string]func(Request) - - // Configurable trigger characters - inlineOpen string - inlineClose string - chatSuffix string - chatPrefixes []string - inlineOpenChar byte - inlineCloseChar byte - chatSuffixChar byte - - // Prompt templates - // Completion - promptCompSysGeneral string - promptCompSysParams string - promptCompSysInline string - promptCompUserGeneral string - promptCompUserParams string - promptCompExtraHeader string - // Provider-native code completion - promptNativeCompletion string - // In-editor chat - promptChatSystem string - // Code actions - promptRewriteSystem string - promptDiagnosticsSystem string - promptDocumentSystem string - promptRewriteUser string - promptDiagnosticsUser string - promptDocumentUser string - promptGoTestSystem string - promptGoTestUser string - promptSimplifySystem string - promptSimplifyUser string - - // Custom actions configured by user - customActions []CustomAction } // ServerOptions collects configuration for NewServer to avoid long parameter lists. type ServerOptions struct { LogContext bool + ConfigStore *runtimeconfig.Store + Config *appconfig.App MaxTokens int ContextMode string WindowLines int @@ -149,121 +106,239 @@ type CustomAction struct { } func NewServer(r io.Reader, w io.Writer, logger *log.Logger, opts ServerOptions) *Server { - s := &Server{in: bufio.NewReader(r), out: w, logger: logger, docs: make(map[string]*document), logContext: opts.LogContext} - maxTokens := opts.MaxTokens - if maxTokens <= 0 { - maxTokens = 500 - } - s.maxTokens = maxTokens - contextMode := opts.ContextMode - if contextMode == "" { - contextMode = "file-on-new-func" + s := &Server{in: bufio.NewReader(r), out: w, logger: logger, docs: make(map[string]*document), logContext: opts.LogContext, configStore: opts.ConfigStore} + s.startTime = time.Now() + s.compCache = make(map[string]string) + s.applyOptions(opts) + // Initialize dispatch table + s.handlers = map[string]func(Request){ + "initialize": s.handleInitialize, + "initialized": func(_ Request) { s.handleInitialized() }, + "shutdown": s.handleShutdown, + "exit": func(_ Request) { s.handleExit() }, + "textDocument/didOpen": s.handleDidOpen, + "textDocument/didChange": s.handleDidChange, + "textDocument/didClose": s.handleDidClose, + "textDocument/completion": s.handleCompletion, + "textDocument/codeAction": s.handleCodeAction, + "codeAction/resolve": s.handleCodeActionResolve, + "workspace/executeCommand": s.handleExecuteCommand, } - windowLines := opts.WindowLines - if windowLines <= 0 { - windowLines = 120 + return s +} + +func (s *Server) applyOptions(opts ServerOptions) { + s.mu.Lock() + defer s.mu.Unlock() + s.logContext = opts.LogContext + if opts.ConfigStore != nil { + s.configStore = opts.ConfigStore } - maxContextTokens := opts.MaxContextTokens - if maxContextTokens <= 0 { - maxContextTokens = 2000 + if opts.Config != nil { + s.cfg = *opts.Config + } else if opts.ConfigStore != nil { + s.cfg = opts.ConfigStore.Snapshot() + } else { + s.cfg = appconfig.App{} + // populate from legacy ServerOptions fields + s.cfg.MaxTokens = opts.MaxTokens + s.cfg.ContextMode = opts.ContextMode + s.cfg.ContextWindowLines = opts.WindowLines + s.cfg.MaxContextTokens = opts.MaxContextTokens + s.cfg.TriggerCharacters = append([]string{}, opts.TriggerCharacters...) + s.cfg.CodingTemperature = opts.CodingTemperature + s.cfg.ManualInvokeMinPrefix = opts.ManualInvokeMinPrefix + s.cfg.CompletionDebounceMs = opts.CompletionDebounceMs + s.cfg.CompletionThrottleMs = opts.CompletionThrottleMs + s.cfg.InlineOpen = opts.InlineOpen + s.cfg.InlineClose = opts.InlineClose + s.cfg.ChatSuffix = opts.ChatSuffix + s.cfg.ChatPrefixes = append([]string{}, opts.ChatPrefixes...) + s.cfg.PromptCompletionSystemGeneral = opts.PromptCompSysGeneral + s.cfg.PromptCompletionSystemParams = opts.PromptCompSysParams + s.cfg.PromptCompletionSystemInline = opts.PromptCompSysInline + s.cfg.PromptCompletionUserGeneral = opts.PromptCompUserGeneral + s.cfg.PromptCompletionUserParams = opts.PromptCompUserParams + s.cfg.PromptCompletionExtraHeader = opts.PromptCompExtraHeader + s.cfg.PromptNativeCompletion = opts.PromptNativeCompletion + s.cfg.PromptChatSystem = opts.PromptChatSystem + s.cfg.PromptCodeActionRewriteSystem = opts.PromptRewriteSystem + s.cfg.PromptCodeActionDiagnosticsSystem = opts.PromptDiagnosticsSystem + s.cfg.PromptCodeActionDocumentSystem = opts.PromptDocumentSystem + s.cfg.PromptCodeActionRewriteUser = opts.PromptRewriteUser + s.cfg.PromptCodeActionDiagnosticsUser = opts.PromptDiagnosticsUser + s.cfg.PromptCodeActionDocumentUser = opts.PromptDocumentUser + s.cfg.PromptCodeActionGoTestSystem = opts.PromptGoTestSystem + s.cfg.PromptCodeActionGoTestUser = opts.PromptGoTestUser + s.cfg.PromptCodeActionSimplifySystem = opts.PromptSimplifySystem + s.cfg.PromptCodeActionSimplifyUser = opts.PromptSimplifyUser + s.cfg.CustomActions = make([]appconfig.CustomAction, len(opts.CustomActions)) + for i, ca := range opts.CustomActions { + s.cfg.CustomActions[i] = appconfig.CustomAction{ + ID: ca.ID, + Title: ca.Title, + Kind: ca.Kind, + Scope: ca.Scope, + Instruction: ca.Instruction, + System: ca.System, + User: ca.User, + } + } } - s.contextMode = contextMode - s.windowLines = windowLines - s.maxContextTokens = maxContextTokens - - s.startTime = time.Now() s.llmClient = opts.Client - if len(opts.TriggerCharacters) == 0 { - // Defaults (no space to avoid auto-trigger after whitespace) - s.triggerChars = []string{".", ":", "/", "_", ")", "{"} - } else { - s.triggerChars = append([]string{}, opts.TriggerCharacters...) +} + +// ApplyOptions updates the server's configuration at runtime. +func (s *Server) ApplyOptions(opts ServerOptions) { + s.applyOptions(opts) +} + +func (s *Server) currentLLMClient() llm.Client { + s.mu.RLock() + defer s.mu.RUnlock() + return s.llmClient +} + +func (s *Server) currentConfig() appconfig.App { + if s.configStore != nil { + return s.configStore.Snapshot() } - s.codingTemperature = opts.CodingTemperature - s.compCache = make(map[string]string) - s.manualInvokeMinPrefix = opts.ManualInvokeMinPrefix - if opts.CompletionDebounceMs > 0 { - s.completionDebounce = time.Duration(opts.CompletionDebounceMs) * time.Millisecond + s.mu.RLock() + defer s.mu.RUnlock() + return s.cfg +} + +func (s *Server) maxTokens() int { + cfg := s.currentConfig() + if cfg.MaxTokens <= 0 { + return 500 } - if opts.CompletionThrottleMs > 0 { - s.throttleInterval = time.Duration(opts.CompletionThrottleMs) * time.Millisecond + return cfg.MaxTokens +} + +func (s *Server) contextMode() string { + mode := strings.TrimSpace(s.currentConfig().ContextMode) + if mode == "" { + return "file-on-new-func" } - // Trigger character config (with sane defaults if missing) - if strings.TrimSpace(opts.InlineOpen) == "" { - s.inlineOpen = ">" - } else { - s.inlineOpen = opts.InlineOpen + return mode +} + +func (s *Server) windowLines() int { + cfg := s.currentConfig() + if cfg.ContextWindowLines <= 0 { + return 120 } - if strings.TrimSpace(opts.InlineClose) == "" { - s.inlineClose = ">" - } else { - s.inlineClose = opts.InlineClose + return cfg.ContextWindowLines +} + +func (s *Server) maxContextTokens() int { + cfg := s.currentConfig() + if cfg.MaxContextTokens <= 0 { + return 2000 } - if strings.TrimSpace(opts.ChatSuffix) == "" { - s.chatSuffix = ">" - } else { - s.chatSuffix = opts.ChatSuffix + return cfg.MaxContextTokens +} + +func (s *Server) triggerCharacters() []string { + cfg := s.currentConfig() + if len(cfg.TriggerCharacters) == 0 { + return []string{".", ":", "/", "_", ")", "{"} } - if len(opts.ChatPrefixes) == 0 { - s.chatPrefixes = []string{"?", "!", ":", ";"} - } else { - s.chatPrefixes = append([]string{}, opts.ChatPrefixes...) + return append([]string{}, cfg.TriggerCharacters...) +} + +func (s *Server) codingTemperature() *float64 { + cfg := s.currentConfig() + return cfg.CodingTemperature +} + +func (s *Server) manualInvokeMinPrefix() int { + return s.currentConfig().ManualInvokeMinPrefix +} + +func (s *Server) completionDebounce() time.Duration { + cfg := s.currentConfig() + if cfg.CompletionDebounceMs <= 0 { + return 0 } + return time.Duration(cfg.CompletionDebounceMs) * time.Millisecond +} - // Prompts - s.promptCompSysGeneral = opts.PromptCompSysGeneral - s.promptCompSysParams = opts.PromptCompSysParams - s.promptCompSysInline = opts.PromptCompSysInline - s.promptCompUserGeneral = opts.PromptCompUserGeneral - s.promptCompUserParams = opts.PromptCompUserParams - s.promptCompExtraHeader = opts.PromptCompExtraHeader - s.promptNativeCompletion = opts.PromptNativeCompletion - s.promptChatSystem = opts.PromptChatSystem - s.promptRewriteSystem = opts.PromptRewriteSystem - s.promptDiagnosticsSystem = opts.PromptDiagnosticsSystem - s.promptDocumentSystem = opts.PromptDocumentSystem - s.promptRewriteUser = opts.PromptRewriteUser - s.promptDiagnosticsUser = opts.PromptDiagnosticsUser - s.promptDocumentUser = opts.PromptDocumentUser - s.promptGoTestSystem = opts.PromptGoTestSystem - s.promptGoTestUser = opts.PromptGoTestUser - s.promptSimplifySystem = opts.PromptSimplifySystem - s.promptSimplifyUser = opts.PromptSimplifyUser +func (s *Server) completionThrottle() time.Duration { + cfg := s.currentConfig() + if cfg.CompletionThrottleMs <= 0 { + return 0 + } + return time.Duration(cfg.CompletionThrottleMs) * time.Millisecond +} - if len(opts.CustomActions) > 0 { - s.customActions = append([]CustomAction{}, opts.CustomActions...) +func (s *Server) inlineMarkers() (open string, close string, openChar byte, closeChar byte) { + cfg := s.currentConfig() + open = strings.TrimSpace(cfg.InlineOpen) + if open == "" { + open = ">" + } + close = strings.TrimSpace(cfg.InlineClose) + if close == "" { + close = ">" + } + openChar = '>' + if len(open) > 0 { + openChar = open[0] } + closeChar = '>' + if len(close) > 0 { + closeChar = close[0] + } + return open, close, openChar, closeChar +} - if s.inlineOpen != "" { - s.inlineOpenChar = s.inlineOpen[0] +func (s *Server) chatConfig() (suffix string, prefixes []string, suffixChar byte) { + cfg := s.currentConfig() + suffix = cfg.ChatSuffix + if suffix != "" { + suffix = strings.TrimSpace(suffix) + if suffix == "" { + suffix = ">" + } } else { - s.inlineOpenChar = '>' + suffix = "" } - if s.inlineClose != "" { - s.inlineCloseChar = s.inlineClose[0] + if len(cfg.ChatPrefixes) == 0 { + prefixes = []string{"?", "!", ":", ";"} } else { - s.inlineCloseChar = '>' + prefixes = append([]string{}, cfg.ChatPrefixes...) } - if s.chatSuffix != "" { - s.chatSuffixChar = s.chatSuffix[0] - } else { - s.chatSuffixChar = '>' + suffixChar = '>' + if len(suffix) > 0 { + suffixChar = suffix[0] } - // Initialize dispatch table - s.handlers = map[string]func(Request){ - "initialize": s.handleInitialize, - "initialized": func(_ Request) { s.handleInitialized() }, - "shutdown": s.handleShutdown, - "exit": func(_ Request) { s.handleExit() }, - "textDocument/didOpen": s.handleDidOpen, - "textDocument/didChange": s.handleDidChange, - "textDocument/didClose": s.handleDidClose, - "textDocument/completion": s.handleCompletion, - "textDocument/codeAction": s.handleCodeAction, - "codeAction/resolve": s.handleCodeActionResolve, - "workspace/executeCommand": s.handleExecuteCommand, + return suffix, prefixes, suffixChar +} + +func (s *Server) promptSet() appconfig.App { + return s.currentConfig() +} + +func (s *Server) customActions() []CustomAction { + cfg := s.currentConfig() + if len(cfg.CustomActions) == 0 { + return nil } - return s + customs := make([]CustomAction, 0, len(cfg.CustomActions)) + for _, ca := range cfg.CustomActions { + customs = append(customs, CustomAction{ + ID: ca.ID, + Title: ca.Title, + Kind: ca.Kind, + Scope: ca.Scope, + Instruction: ca.Instruction, + System: ca.System, + User: ca.User, + }) + } + return customs } func (s *Server) Run() error { |
