mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +00:00
add initial lsp support
This commit is contained in:
parent
afd9ad0560
commit
cfdd687216
47 changed files with 13996 additions and 456 deletions
|
@ -91,6 +91,12 @@ go build -o termai
|
|||
./termai
|
||||
```
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
TermAI builds upon the work of several open source projects and developers:
|
||||
|
||||
- [@isaacphi](https://github.com/isaacphi) - LSP client implementation
|
||||
|
||||
## License
|
||||
|
||||
[License information coming soon]
|
||||
|
|
4
cmd/lsp/main.go
Normal file
4
cmd/lsp/main.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package main
|
||||
|
||||
func main() {
|
||||
}
|
|
@ -36,6 +36,7 @@ var rootCmd = &cobra.Command{
|
|||
ctx := context.Background()
|
||||
|
||||
app := app.New(ctx, conn)
|
||||
defer app.Close()
|
||||
app.Logger.Info("Starting termai...")
|
||||
zone.NewGlobal()
|
||||
tui := tea.NewProgram(
|
||||
|
|
6
go.mod
6
go.mod
|
@ -1,6 +1,8 @@
|
|||
module github.com/kujtimiihoxha/termai
|
||||
|
||||
go 1.23.5
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2
|
||||
|
@ -11,6 +13,7 @@ require (
|
|||
github.com/charmbracelet/glamour v0.9.1
|
||||
github.com/charmbracelet/huh v0.6.0
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
github.com/go-logfmt/logfmt v0.6.0
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/google/generative-ai-go v0.19.0
|
||||
|
@ -53,7 +56,6 @@ require (
|
|||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
|
|
|
@ -7,6 +7,8 @@ import (
|
|||
"github.com/kujtimiihoxha/termai/internal/config"
|
||||
"github.com/kujtimiihoxha/termai/internal/db"
|
||||
"github.com/kujtimiihoxha/termai/internal/logging"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/watcher"
|
||||
"github.com/kujtimiihoxha/termai/internal/message"
|
||||
"github.com/kujtimiihoxha/termai/internal/permission"
|
||||
"github.com/kujtimiihoxha/termai/internal/session"
|
||||
|
@ -19,23 +21,59 @@ type App struct {
|
|||
Messages message.Service
|
||||
Permissions permission.Service
|
||||
|
||||
LSPClients map[string]*lsp.Client
|
||||
|
||||
Logger logging.Interface
|
||||
|
||||
ceanups []func()
|
||||
}
|
||||
|
||||
func New(ctx context.Context, conn *sql.DB) *App {
|
||||
cfg := config.Get()
|
||||
q := db.New(conn)
|
||||
log := logging.NewLogger(logging.Options{
|
||||
Level: config.Get().Log.Level,
|
||||
Level: cfg.Log.Level,
|
||||
})
|
||||
sessions := session.NewService(ctx, q)
|
||||
messages := message.NewService(ctx, q)
|
||||
|
||||
return &App{
|
||||
app := &App{
|
||||
Context: ctx,
|
||||
Sessions: sessions,
|
||||
Messages: messages,
|
||||
Permissions: permission.Default,
|
||||
Logger: log,
|
||||
LSPClients: make(map[string]*lsp.Client),
|
||||
}
|
||||
|
||||
for name, client := range cfg.LSP {
|
||||
lspClient, err := lsp.NewClient(client.Command, client.Args...)
|
||||
app.ceanups = append(app.ceanups, func() {
|
||||
lspClient.Close()
|
||||
})
|
||||
workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
|
||||
if err != nil {
|
||||
log.Error("Failed to create LSP client for", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = lspClient.InitializeLSPClient(ctx, config.WorkingDirectory())
|
||||
if err != nil {
|
||||
log.Error("Initialize failed", "error", err)
|
||||
continue
|
||||
}
|
||||
go workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
|
||||
app.LSPClients[name] = lspClient
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
func (a *App) Close() {
|
||||
for _, cleanup := range a.ceanups {
|
||||
cleanup()
|
||||
}
|
||||
for _, client := range a.LSPClients {
|
||||
client.Close()
|
||||
}
|
||||
a.Logger.Info("App closed")
|
||||
}
|
||||
|
|
|
@ -49,12 +49,21 @@ type Log struct {
|
|||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
type LSPConfig struct {
|
||||
Disabled bool `json:"enabled"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Options any `json:"options"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Data *Data `json:"data,omitempty"`
|
||||
Log *Log `json:"log,omitempty"`
|
||||
MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
|
||||
Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
|
||||
|
||||
LSP map[string]LSPConfig `json:"lsp,omitempty"`
|
||||
|
||||
Model *Model `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ package db
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createMessage = `-- name: CreateMessage :one
|
||||
|
@ -15,26 +14,20 @@ INSERT INTO messages (
|
|||
id,
|
||||
session_id,
|
||||
role,
|
||||
finished,
|
||||
content,
|
||||
tool_calls,
|
||||
tool_results,
|
||||
parts,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
)
|
||||
RETURNING id, session_id, role, content, thinking, finished, tool_calls, tool_results, created_at, updated_at
|
||||
RETURNING id, session_id, role, parts, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateMessageParams struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Role string `json:"role"`
|
||||
Finished bool `json:"finished"`
|
||||
Content string `json:"content"`
|
||||
ToolCalls sql.NullString `json:"tool_calls"`
|
||||
ToolResults sql.NullString `json:"tool_results"`
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Role string `json:"role"`
|
||||
Parts string `json:"parts"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
|
||||
|
@ -42,21 +35,14 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
|
|||
arg.ID,
|
||||
arg.SessionID,
|
||||
arg.Role,
|
||||
arg.Finished,
|
||||
arg.Content,
|
||||
arg.ToolCalls,
|
||||
arg.ToolResults,
|
||||
arg.Parts,
|
||||
)
|
||||
var i Message
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Role,
|
||||
&i.Content,
|
||||
&i.Thinking,
|
||||
&i.Finished,
|
||||
&i.ToolCalls,
|
||||
&i.ToolResults,
|
||||
&i.Parts,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
|
@ -84,7 +70,7 @@ func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) e
|
|||
}
|
||||
|
||||
const getMessage = `-- name: GetMessage :one
|
||||
SELECT id, session_id, role, content, thinking, finished, tool_calls, tool_results, created_at, updated_at
|
||||
SELECT id, session_id, role, parts, created_at, updated_at
|
||||
FROM messages
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
@ -96,11 +82,7 @@ func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
|
|||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Role,
|
||||
&i.Content,
|
||||
&i.Thinking,
|
||||
&i.Finished,
|
||||
&i.ToolCalls,
|
||||
&i.ToolResults,
|
||||
&i.Parts,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
|
@ -108,7 +90,7 @@ func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
|
|||
}
|
||||
|
||||
const listMessagesBySession = `-- name: ListMessagesBySession :many
|
||||
SELECT id, session_id, role, content, thinking, finished, tool_calls, tool_results, created_at, updated_at
|
||||
SELECT id, session_id, role, parts, created_at, updated_at
|
||||
FROM messages
|
||||
WHERE session_id = ?
|
||||
ORDER BY created_at ASC
|
||||
|
@ -127,11 +109,7 @@ func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) (
|
|||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Role,
|
||||
&i.Content,
|
||||
&i.Thinking,
|
||||
&i.Finished,
|
||||
&i.ToolCalls,
|
||||
&i.ToolResults,
|
||||
&i.Parts,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
|
@ -151,32 +129,17 @@ func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) (
|
|||
const updateMessage = `-- name: UpdateMessage :exec
|
||||
UPDATE messages
|
||||
SET
|
||||
content = ?,
|
||||
thinking = ?,
|
||||
tool_calls = ?,
|
||||
tool_results = ?,
|
||||
finished = ?,
|
||||
parts = ?,
|
||||
updated_at = strftime('%s', 'now')
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateMessageParams struct {
|
||||
Content string `json:"content"`
|
||||
Thinking string `json:"thinking"`
|
||||
ToolCalls sql.NullString `json:"tool_calls"`
|
||||
ToolResults sql.NullString `json:"tool_results"`
|
||||
Finished bool `json:"finished"`
|
||||
ID string `json:"id"`
|
||||
Parts string `json:"parts"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error {
|
||||
_, err := q.exec(ctx, q.updateMessageStmt, updateMessage,
|
||||
arg.Content,
|
||||
arg.Thinking,
|
||||
arg.ToolCalls,
|
||||
arg.ToolResults,
|
||||
arg.Finished,
|
||||
arg.ID,
|
||||
)
|
||||
_, err := q.exec(ctx, q.updateMessageStmt, updateMessage, arg.Parts, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -23,11 +23,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
|||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
thinking Text NOT NULL DEFAULT '',
|
||||
finished BOOLEAN NOT NULL DEFAULT 0,
|
||||
tool_calls TEXT,
|
||||
tool_results TEXT,
|
||||
parts TEXT NOT NULL default '[]',
|
||||
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
|
||||
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
|
||||
|
|
|
@ -9,16 +9,12 @@ import (
|
|||
)
|
||||
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
Thinking string `json:"thinking"`
|
||||
Finished bool `json:"finished"`
|
||||
ToolCalls sql.NullString `json:"tool_calls"`
|
||||
ToolResults sql.NullString `json:"tool_results"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
SessionID string `json:"session_id"`
|
||||
Role string `json:"role"`
|
||||
Parts string `json:"parts"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
|
|
|
@ -14,25 +14,18 @@ INSERT INTO messages (
|
|||
id,
|
||||
session_id,
|
||||
role,
|
||||
finished,
|
||||
content,
|
||||
tool_calls,
|
||||
tool_results,
|
||||
parts,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateMessage :exec
|
||||
UPDATE messages
|
||||
SET
|
||||
content = ?,
|
||||
thinking = ?,
|
||||
tool_calls = ?,
|
||||
tool_results = ?,
|
||||
finished = ?,
|
||||
parts = ?,
|
||||
updated_at = strftime('%s', 'now')
|
||||
WHERE id = ?;
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolRes
|
|||
if err != nil {
|
||||
return tools.NewTextErrorResponse(fmt.Sprintf("error: %s", err)), nil
|
||||
}
|
||||
return tools.NewTextResponse(response.Content), nil
|
||||
return tools.NewTextResponse(response.Content().String()), nil
|
||||
}
|
||||
|
||||
func NewAgentTool(parentSessionID string, app *app.App) tools.BaseTool {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/app"
|
||||
|
@ -33,8 +34,12 @@ func (c *agent) handleTitleGeneration(sessionID, content string) {
|
|||
c.Context,
|
||||
[]message.Message{
|
||||
{
|
||||
Role: message.User,
|
||||
Content: content,
|
||||
Role: message.User,
|
||||
Parts: []message.ContentPart{
|
||||
message.TextContent{
|
||||
Text: content,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
|
@ -49,6 +54,8 @@ func (c *agent) handleTitleGeneration(sessionID, content string) {
|
|||
}
|
||||
if response.Content != "" {
|
||||
session.Title = response.Content
|
||||
session.Title = strings.TrimSpace(session.Title)
|
||||
session.Title = strings.ReplaceAll(session.Title, "\n", " ")
|
||||
c.Sessions.Save(session)
|
||||
}
|
||||
}
|
||||
|
@ -79,17 +86,18 @@ func (c *agent) processEvent(
|
|||
) error {
|
||||
switch event.Type {
|
||||
case provider.EventThinkingDelta:
|
||||
assistantMsg.Thinking += event.Thinking
|
||||
assistantMsg.AppendReasoningContent(event.Content)
|
||||
return c.Messages.Update(*assistantMsg)
|
||||
case provider.EventContentDelta:
|
||||
assistantMsg.Content += event.Content
|
||||
assistantMsg.AppendContent(event.Content)
|
||||
return c.Messages.Update(*assistantMsg)
|
||||
case provider.EventError:
|
||||
log.Println("error", event.Error)
|
||||
return event.Error
|
||||
|
||||
case provider.EventComplete:
|
||||
assistantMsg.ToolCalls = event.Response.ToolCalls
|
||||
assistantMsg.SetToolCalls(event.Response.ToolCalls)
|
||||
assistantMsg.AddFinish(event.Response.FinishReason)
|
||||
err := c.Messages.Update(*assistantMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -157,18 +165,21 @@ func (c *agent) handleToolExecution(
|
|||
ctx context.Context,
|
||||
assistantMsg message.Message,
|
||||
) (*message.Message, error) {
|
||||
if len(assistantMsg.ToolCalls) == 0 {
|
||||
if len(assistantMsg.ToolCalls()) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
toolResults, err := c.ExecuteTools(ctx, assistantMsg.ToolCalls, c.tools)
|
||||
toolResults, err := c.ExecuteTools(ctx, assistantMsg.ToolCalls(), c.tools)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parts := make([]message.ContentPart, 0)
|
||||
for _, toolResult := range toolResults {
|
||||
parts = append(parts, toolResult)
|
||||
}
|
||||
msg, err := c.Messages.Create(assistantMsg.SessionID, message.CreateMessageParams{
|
||||
Role: message.Tool,
|
||||
ToolResults: toolResults,
|
||||
Role: message.Tool,
|
||||
Parts: parts,
|
||||
})
|
||||
|
||||
return &msg, err
|
||||
|
@ -185,8 +196,12 @@ func (c *agent) generate(sessionID string, content string) error {
|
|||
}
|
||||
|
||||
userMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{
|
||||
Role: message.User,
|
||||
Content: content,
|
||||
Role: message.User,
|
||||
Parts: []message.ContentPart{
|
||||
message.TextContent{
|
||||
Text: content,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -201,8 +216,8 @@ func (c *agent) generate(sessionID string, content string) error {
|
|||
}
|
||||
|
||||
assistantMsg, err := c.Messages.Create(sessionID, message.CreateMessageParams{
|
||||
Role: message.Assistant,
|
||||
Content: "",
|
||||
Role: message.Assistant,
|
||||
Parts: []message.ContentPart{},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -210,20 +225,20 @@ func (c *agent) generate(sessionID string, content string) error {
|
|||
for event := range eventChan {
|
||||
err = c.processEvent(sessionID, &assistantMsg, event)
|
||||
if err != nil {
|
||||
assistantMsg.Finished = true
|
||||
assistantMsg.AddFinish("error:" + err.Error())
|
||||
c.Messages.Update(assistantMsg)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
msg, err := c.handleToolExecution(c.Context, assistantMsg)
|
||||
assistantMsg.Finished = true
|
||||
|
||||
c.Messages.Update(assistantMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(assistantMsg.ToolCalls) == 0 {
|
||||
if len(assistantMsg.ToolCalls()) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
|
|
|
@ -44,20 +44,23 @@ func NewCoderAgent(app *app.App) (Agent, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
mcpTools := GetMcpTools(app.Context)
|
||||
otherTools := GetMcpTools(app.Context)
|
||||
if len(app.LSPClients) > 0 {
|
||||
otherTools = append(otherTools, tools.NewDiagnosticsTool(app.LSPClients))
|
||||
}
|
||||
return &coderAgent{
|
||||
agent: &agent{
|
||||
App: app,
|
||||
tools: append(
|
||||
[]tools.BaseTool{
|
||||
tools.NewBashTool(),
|
||||
tools.NewEditTool(),
|
||||
tools.NewEditTool(app.LSPClients),
|
||||
tools.NewGlobTool(),
|
||||
tools.NewGrepTool(),
|
||||
tools.NewLsTool(),
|
||||
tools.NewViewTool(),
|
||||
tools.NewWriteTool(),
|
||||
}, mcpTools...,
|
||||
tools.NewViewTool(app.LSPClients),
|
||||
tools.NewWriteTool(app.LSPClients),
|
||||
}, otherTools...,
|
||||
),
|
||||
model: model,
|
||||
agent: agentProvider,
|
||||
|
|
|
@ -34,7 +34,7 @@ func NewTaskAgent(app *app.App) (Agent, error) {
|
|||
tools.NewGlobTool(),
|
||||
tools.NewGrepTool(),
|
||||
tools.NewLsTool(),
|
||||
tools.NewViewTool(),
|
||||
tools.NewViewTool(app.LSPClients),
|
||||
},
|
||||
model: model,
|
||||
agent: agentProvider,
|
||||
|
|
|
@ -67,7 +67,7 @@ Never commit changes unless the user explicitly asks you to.`
|
|||
|
||||
envInfo := getEnvironmentInfo()
|
||||
|
||||
return fmt.Sprintf("%s\n\n%s", basePrompt, envInfo)
|
||||
return fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation())
|
||||
}
|
||||
|
||||
func CoderAnthropicSystemPrompt() string {
|
||||
|
@ -168,7 +168,7 @@ You MUST answer concisely with fewer than 4 lines of text (not including tool us
|
|||
|
||||
envInfo := getEnvironmentInfo()
|
||||
|
||||
return fmt.Sprintf("%s\n\n%s", basePrompt, envInfo)
|
||||
return fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation())
|
||||
}
|
||||
|
||||
func getEnvironmentInfo() string {
|
||||
|
@ -198,6 +198,25 @@ func isGitRepo(dir string) bool {
|
|||
return err == nil
|
||||
}
|
||||
|
||||
func lspInformation() string {
|
||||
cfg := config.Get()
|
||||
hasLSP := false
|
||||
for _, v := range cfg.LSP {
|
||||
if !v.Disabled {
|
||||
hasLSP = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLSP {
|
||||
return ""
|
||||
}
|
||||
return `# LSP Information
|
||||
Tools that support it will also include useful diagnostics such as linting and typechecking.
|
||||
These diagnostics will be automatically enabled when you run the tool, and will be displayed in the output at the bottom within the <file_diagnostics></file_diagnostics> and <project_diagnostics></project_diagnostics> tags.
|
||||
Take necessary actions to fix the issues.
|
||||
`
|
||||
}
|
||||
|
||||
func boolToYesNo(b bool) string {
|
||||
if b {
|
||||
return "Yes"
|
||||
|
|
|
@ -111,7 +111,7 @@ func (a *anthropicProvider) StreamResponse(ctx context.Context, messages []messa
|
|||
var thinkingParam anthropic.ThinkingConfigParamUnion
|
||||
lastMessage := messages[len(messages)-1]
|
||||
temperature := anthropic.Float(0)
|
||||
if lastMessage.Role == message.User && strings.Contains(strings.ToLower(lastMessage.Content), "think") {
|
||||
if lastMessage.Role == message.User && strings.Contains(strings.ToLower(lastMessage.Content().String()), "think") {
|
||||
thinkingParam = anthropic.ThinkingConfigParamUnion{
|
||||
OfThinkingConfigEnabled: &anthropic.ThinkingConfigEnabledParam{
|
||||
BudgetTokens: int64(float64(a.maxTokens) * 0.8),
|
||||
|
@ -187,9 +187,10 @@ func (a *anthropicProvider) StreamResponse(ctx context.Context, messages []messa
|
|||
eventChan <- ProviderEvent{
|
||||
Type: EventComplete,
|
||||
Response: &ProviderResponse{
|
||||
Content: content,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: tokenUsage,
|
||||
Content: content,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: tokenUsage,
|
||||
FinishReason: string(accumulatedMessage.StopReason),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -263,7 +264,7 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
|
|||
for i, msg := range messages {
|
||||
switch msg.Role {
|
||||
case message.User:
|
||||
content := anthropic.NewTextBlock(msg.Content)
|
||||
content := anthropic.NewTextBlock(msg.Content().String())
|
||||
if cachedBlocks < 2 {
|
||||
content.OfRequestTextBlock.CacheControl = anthropic.CacheControlEphemeralParam{
|
||||
Type: "ephemeral",
|
||||
|
@ -274,8 +275,8 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
|
|||
|
||||
case message.Assistant:
|
||||
blocks := []anthropic.ContentBlockParamUnion{}
|
||||
if msg.Content != "" {
|
||||
content := anthropic.NewTextBlock(msg.Content)
|
||||
if msg.Content().String() != "" {
|
||||
content := anthropic.NewTextBlock(msg.Content().String())
|
||||
if cachedBlocks < 2 {
|
||||
content.OfRequestTextBlock.CacheControl = anthropic.CacheControlEphemeralParam{
|
||||
Type: "ephemeral",
|
||||
|
@ -285,7 +286,7 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
|
|||
blocks = append(blocks, content)
|
||||
}
|
||||
|
||||
for _, toolCall := range msg.ToolCalls {
|
||||
for _, toolCall := range msg.ToolCalls() {
|
||||
var inputMap map[string]any
|
||||
err := json.Unmarshal([]byte(toolCall.Input), &inputMap)
|
||||
if err != nil {
|
||||
|
@ -297,8 +298,8 @@ func (a *anthropicProvider) convertToAnthropicMessages(messages []message.Messag
|
|||
anthropicMessages[i] = anthropic.NewAssistantMessage(blocks...)
|
||||
|
||||
case message.Tool:
|
||||
results := make([]anthropic.ContentBlockParamUnion, len(msg.ToolResults))
|
||||
for i, toolResult := range msg.ToolResults {
|
||||
results := make([]anthropic.ContentBlockParamUnion, len(msg.ToolResults()))
|
||||
for i, toolResult := range msg.ToolResults() {
|
||||
results[i] = anthropic.NewToolResultBlock(toolResult.ToolCallID, toolResult.Content, toolResult.IsError)
|
||||
}
|
||||
anthropicMessages[i] = anthropic.NewUserMessage(results...)
|
||||
|
|
|
@ -78,7 +78,6 @@ func (p *geminiProvider) Close() {
|
|||
}
|
||||
}
|
||||
|
||||
// convertToGeminiHistory converts the message history to Gemini's format
|
||||
func (p *geminiProvider) convertToGeminiHistory(messages []message.Message) []*genai.Content {
|
||||
var history []*genai.Content
|
||||
|
||||
|
@ -86,7 +85,7 @@ func (p *geminiProvider) convertToGeminiHistory(messages []message.Message) []*g
|
|||
switch msg.Role {
|
||||
case message.User:
|
||||
history = append(history, &genai.Content{
|
||||
Parts: []genai.Part{genai.Text(msg.Content)},
|
||||
Parts: []genai.Part{genai.Text(msg.Content().String())},
|
||||
Role: "user",
|
||||
})
|
||||
case message.Assistant:
|
||||
|
@ -95,14 +94,12 @@ func (p *geminiProvider) convertToGeminiHistory(messages []message.Message) []*g
|
|||
Parts: []genai.Part{},
|
||||
}
|
||||
|
||||
// Handle regular content
|
||||
if msg.Content != "" {
|
||||
content.Parts = append(content.Parts, genai.Text(msg.Content))
|
||||
if msg.Content().String() != "" {
|
||||
content.Parts = append(content.Parts, genai.Text(msg.Content().String()))
|
||||
}
|
||||
|
||||
// Handle tool calls if any
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
for _, call := range msg.ToolCalls {
|
||||
if len(msg.ToolCalls()) > 0 {
|
||||
for _, call := range msg.ToolCalls() {
|
||||
args, _ := parseJsonToMap(call.Input)
|
||||
content.Parts = append(content.Parts, genai.FunctionCall{
|
||||
Name: call.Name,
|
||||
|
@ -113,8 +110,7 @@ func (p *geminiProvider) convertToGeminiHistory(messages []message.Message) []*g
|
|||
|
||||
history = append(history, content)
|
||||
case message.Tool:
|
||||
for _, result := range msg.ToolResults {
|
||||
// Parse response content to map if possible
|
||||
for _, result := range msg.ToolResults() {
|
||||
response := map[string]interface{}{"result": result.Content}
|
||||
parsed, err := parseJsonToMap(result.Content)
|
||||
if err == nil {
|
||||
|
@ -123,7 +119,7 @@ func (p *geminiProvider) convertToGeminiHistory(messages []message.Message) []*g
|
|||
var toolCall message.ToolCall
|
||||
for _, msg := range messages {
|
||||
if msg.Role == message.Assistant {
|
||||
for _, call := range msg.ToolCalls {
|
||||
for _, call := range msg.ToolCalls() {
|
||||
if call.ID == result.ToolCallID {
|
||||
toolCall = call
|
||||
break
|
||||
|
@ -146,108 +142,6 @@ func (p *geminiProvider) convertToGeminiHistory(messages []message.Message) []*g
|
|||
return history
|
||||
}
|
||||
|
||||
// convertToolsToGeminiFunctionDeclarations converts tool definitions to Gemini's function declarations
|
||||
func (p *geminiProvider) convertToolsToGeminiFunctionDeclarations(tools []tools.BaseTool) []*genai.FunctionDeclaration {
|
||||
declarations := make([]*genai.FunctionDeclaration, len(tools))
|
||||
|
||||
for i, tool := range tools {
|
||||
info := tool.Info()
|
||||
|
||||
// Convert parameters to genai.Schema format
|
||||
properties := make(map[string]*genai.Schema)
|
||||
for name, param := range info.Parameters {
|
||||
// Try to extract type and description from the parameter
|
||||
paramMap, ok := param.(map[string]interface{})
|
||||
if !ok {
|
||||
// Default to string if unable to determine type
|
||||
properties[name] = &genai.Schema{Type: genai.TypeString}
|
||||
continue
|
||||
}
|
||||
|
||||
schemaType := genai.TypeString // Default
|
||||
var description string
|
||||
var itemsTypeSchema *genai.Schema
|
||||
if typeVal, found := paramMap["type"]; found {
|
||||
if typeStr, ok := typeVal.(string); ok {
|
||||
switch typeStr {
|
||||
case "string":
|
||||
schemaType = genai.TypeString
|
||||
case "number":
|
||||
schemaType = genai.TypeNumber
|
||||
case "integer":
|
||||
schemaType = genai.TypeInteger
|
||||
case "boolean":
|
||||
schemaType = genai.TypeBoolean
|
||||
case "array":
|
||||
schemaType = genai.TypeArray
|
||||
items, found := paramMap["items"]
|
||||
if found {
|
||||
itemsMap, ok := items.(map[string]interface{})
|
||||
if ok {
|
||||
itemsType, found := itemsMap["type"]
|
||||
if found {
|
||||
itemsTypeStr, ok := itemsType.(string)
|
||||
if ok {
|
||||
switch itemsTypeStr {
|
||||
case "string":
|
||||
itemsTypeSchema = &genai.Schema{
|
||||
Type: genai.TypeString,
|
||||
}
|
||||
case "number":
|
||||
itemsTypeSchema = &genai.Schema{
|
||||
Type: genai.TypeNumber,
|
||||
}
|
||||
case "integer":
|
||||
itemsTypeSchema = &genai.Schema{
|
||||
Type: genai.TypeInteger,
|
||||
}
|
||||
case "boolean":
|
||||
itemsTypeSchema = &genai.Schema{
|
||||
Type: genai.TypeBoolean,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "object":
|
||||
schemaType = genai.TypeObject
|
||||
if _, found := paramMap["properties"]; !found {
|
||||
continue
|
||||
}
|
||||
// TODO: Add support for other types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if desc, found := paramMap["description"]; found {
|
||||
if descStr, ok := desc.(string); ok {
|
||||
description = descStr
|
||||
}
|
||||
}
|
||||
|
||||
properties[name] = &genai.Schema{
|
||||
Type: schemaType,
|
||||
Description: description,
|
||||
Items: itemsTypeSchema,
|
||||
}
|
||||
}
|
||||
|
||||
declarations[i] = &genai.FunctionDeclaration{
|
||||
Name: info.Name,
|
||||
Description: info.Description,
|
||||
Parameters: &genai.Schema{
|
||||
Type: genai.TypeObject,
|
||||
Properties: properties,
|
||||
Required: info.Required,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return declarations
|
||||
}
|
||||
|
||||
// extractTokenUsage extracts token usage information from Gemini's response
|
||||
func (p *geminiProvider) extractTokenUsage(resp *genai.GenerateContentResponse) TokenUsage {
|
||||
if resp == nil || resp.UsageMetadata == nil {
|
||||
return TokenUsage{}
|
||||
|
@ -261,41 +155,28 @@ func (p *geminiProvider) extractTokenUsage(resp *genai.GenerateContentResponse)
|
|||
}
|
||||
}
|
||||
|
||||
// SendMessages sends a batch of messages to Gemini and returns the response
|
||||
func (p *geminiProvider) SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) {
|
||||
// Create a generative model
|
||||
model := p.client.GenerativeModel(p.model.APIModel)
|
||||
model.SetMaxOutputTokens(p.maxTokens)
|
||||
|
||||
// Set system instruction
|
||||
model.SystemInstruction = genai.NewUserContent(genai.Text(p.systemMessage))
|
||||
|
||||
// Set up tools if provided
|
||||
if len(tools) > 0 {
|
||||
declarations := p.convertToolsToGeminiFunctionDeclarations(tools)
|
||||
model.Tools = []*genai.Tool{{FunctionDeclarations: declarations}}
|
||||
}
|
||||
|
||||
// Create chat session and set history
|
||||
chat := model.StartChat()
|
||||
chat.History = p.convertToGeminiHistory(messages[:len(messages)-1]) // Exclude last message
|
||||
|
||||
// Get the most recent user message
|
||||
var lastUserMsg message.Message
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == message.User {
|
||||
lastUserMsg = messages[i]
|
||||
break
|
||||
for _, declaration := range declarations {
|
||||
model.Tools = append(model.Tools, &genai.Tool{FunctionDeclarations: []*genai.FunctionDeclaration{declaration}})
|
||||
}
|
||||
}
|
||||
|
||||
// Send the message
|
||||
resp, err := chat.SendMessage(ctx, genai.Text(lastUserMsg.Content))
|
||||
chat := model.StartChat()
|
||||
chat.History = p.convertToGeminiHistory(messages[:len(messages)-1]) // Exclude last message
|
||||
|
||||
lastUserMsg := messages[len(messages)-1]
|
||||
resp, err := chat.SendMessage(ctx, genai.Text(lastUserMsg.Content().String()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Process the response
|
||||
var content string
|
||||
var toolCalls []message.ToolCall
|
||||
|
||||
|
@ -317,7 +198,6 @@ func (p *geminiProvider) SendMessages(ctx context.Context, messages []message.Me
|
|||
}
|
||||
}
|
||||
|
||||
// Extract token usage
|
||||
tokenUsage := p.extractTokenUsage(resp)
|
||||
|
||||
return &ProviderResponse{
|
||||
|
@ -327,16 +207,12 @@ func (p *geminiProvider) SendMessages(ctx context.Context, messages []message.Me
|
|||
}, nil
|
||||
}
|
||||
|
||||
// StreamResponse streams the response from Gemini
|
||||
func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (<-chan ProviderEvent, error) {
|
||||
// Create a generative model
|
||||
model := p.client.GenerativeModel(p.model.APIModel)
|
||||
model.SetMaxOutputTokens(p.maxTokens)
|
||||
|
||||
// Set system instruction
|
||||
model.SystemInstruction = genai.NewUserContent(genai.Text(p.systemMessage))
|
||||
|
||||
// Set up tools if provided
|
||||
if len(tools) > 0 {
|
||||
declarations := p.convertToolsToGeminiFunctionDeclarations(tools)
|
||||
for _, declaration := range declarations {
|
||||
|
@ -344,14 +220,12 @@ func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.
|
|||
}
|
||||
}
|
||||
|
||||
// Create chat session and set history
|
||||
chat := model.StartChat()
|
||||
chat.History = p.convertToGeminiHistory(messages[:len(messages)-1]) // Exclude last message
|
||||
|
||||
lastUserMsg := messages[len(messages)-1]
|
||||
|
||||
// Start streaming
|
||||
iter := chat.SendMessageStream(ctx, genai.Text(lastUserMsg.Content))
|
||||
iter := chat.SendMessageStream(ctx, genai.Text(lastUserMsg.Content().String()))
|
||||
|
||||
eventChan := make(chan ProviderEvent)
|
||||
|
||||
|
@ -392,7 +266,6 @@ func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.
|
|||
}
|
||||
currentContent += newText
|
||||
case genai.FunctionCall:
|
||||
// For function calls, we assume they come complete, not streamed in parts
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(p.Args)
|
||||
newCall := message.ToolCall{
|
||||
|
@ -402,7 +275,6 @@ func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.
|
|||
Type: "function",
|
||||
}
|
||||
|
||||
// Check if this is a new tool call
|
||||
isNew := true
|
||||
for _, existing := range toolCalls {
|
||||
if existing.Name == newCall.Name && existing.Input == newCall.Input {
|
||||
|
@ -419,15 +291,15 @@ func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.
|
|||
}
|
||||
}
|
||||
|
||||
// Extract token usage from the final response
|
||||
tokenUsage := p.extractTokenUsage(finalResp)
|
||||
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventComplete,
|
||||
Response: &ProviderResponse{
|
||||
Content: currentContent,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: tokenUsage,
|
||||
Content: currentContent,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: tokenUsage,
|
||||
FinishReason: string(finalResp.Candidates[0].FinishReason.String()),
|
||||
},
|
||||
}
|
||||
}()
|
||||
|
@ -435,7 +307,99 @@ func (p *geminiProvider) StreamResponse(ctx context.Context, messages []message.
|
|||
return eventChan, nil
|
||||
}
|
||||
|
||||
// Helper function to parse JSON string into map
|
||||
func (p *geminiProvider) convertToolsToGeminiFunctionDeclarations(tools []tools.BaseTool) []*genai.FunctionDeclaration {
|
||||
declarations := make([]*genai.FunctionDeclaration, len(tools))
|
||||
|
||||
for i, tool := range tools {
|
||||
info := tool.Info()
|
||||
declarations[i] = &genai.FunctionDeclaration{
|
||||
Name: info.Name,
|
||||
Description: info.Description,
|
||||
Parameters: &genai.Schema{
|
||||
Type: genai.TypeObject,
|
||||
Properties: convertSchemaProperties(info.Parameters),
|
||||
Required: info.Required,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return declarations
|
||||
}
|
||||
|
||||
func convertSchemaProperties(parameters map[string]interface{}) map[string]*genai.Schema {
|
||||
properties := make(map[string]*genai.Schema)
|
||||
|
||||
for name, param := range parameters {
|
||||
properties[name] = convertToSchema(param)
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
func convertToSchema(param interface{}) *genai.Schema {
|
||||
schema := &genai.Schema{Type: genai.TypeString}
|
||||
|
||||
paramMap, ok := param.(map[string]interface{})
|
||||
if !ok {
|
||||
return schema
|
||||
}
|
||||
|
||||
if desc, ok := paramMap["description"].(string); ok {
|
||||
schema.Description = desc
|
||||
}
|
||||
|
||||
typeVal, hasType := paramMap["type"]
|
||||
if !hasType {
|
||||
return schema
|
||||
}
|
||||
|
||||
typeStr, ok := typeVal.(string)
|
||||
if !ok {
|
||||
return schema
|
||||
}
|
||||
|
||||
schema.Type = mapJSONTypeToGenAI(typeStr)
|
||||
|
||||
switch typeStr {
|
||||
case "array":
|
||||
schema.Items = processArrayItems(paramMap)
|
||||
case "object":
|
||||
if props, ok := paramMap["properties"].(map[string]interface{}); ok {
|
||||
schema.Properties = convertSchemaProperties(props)
|
||||
}
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
func processArrayItems(paramMap map[string]interface{}) *genai.Schema {
|
||||
items, ok := paramMap["items"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return convertToSchema(items)
|
||||
}
|
||||
|
||||
func mapJSONTypeToGenAI(jsonType string) genai.Type {
|
||||
switch jsonType {
|
||||
case "string":
|
||||
return genai.TypeString
|
||||
case "number":
|
||||
return genai.TypeNumber
|
||||
case "integer":
|
||||
return genai.TypeInteger
|
||||
case "boolean":
|
||||
return genai.TypeBoolean
|
||||
case "array":
|
||||
return genai.TypeArray
|
||||
case "object":
|
||||
return genai.TypeObject
|
||||
default:
|
||||
return genai.TypeString // Default to string for unknown types
|
||||
}
|
||||
}
|
||||
|
||||
func parseJsonToMap(jsonStr string) (map[string]interface{}, error) {
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal([]byte(jsonStr), &result)
|
||||
|
|
|
@ -84,22 +84,22 @@ func (p *openaiProvider) convertToOpenAIMessages(messages []message.Message) []o
|
|||
for _, msg := range messages {
|
||||
switch msg.Role {
|
||||
case message.User:
|
||||
chatMessages = append(chatMessages, openai.UserMessage(msg.Content))
|
||||
chatMessages = append(chatMessages, openai.UserMessage(msg.Content().String()))
|
||||
|
||||
case message.Assistant:
|
||||
assistantMsg := openai.ChatCompletionAssistantMessageParam{
|
||||
Role: "assistant",
|
||||
}
|
||||
|
||||
if msg.Content != "" {
|
||||
if msg.Content().String() != "" {
|
||||
assistantMsg.Content = openai.ChatCompletionAssistantMessageParamContentUnion{
|
||||
OfString: openai.String(msg.Content),
|
||||
OfString: openai.String(msg.Content().String()),
|
||||
}
|
||||
}
|
||||
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
assistantMsg.ToolCalls = make([]openai.ChatCompletionMessageToolCallParam, len(msg.ToolCalls))
|
||||
for i, call := range msg.ToolCalls {
|
||||
if len(msg.ToolCalls()) > 0 {
|
||||
assistantMsg.ToolCalls = make([]openai.ChatCompletionMessageToolCallParam, len(msg.ToolCalls()))
|
||||
for i, call := range msg.ToolCalls() {
|
||||
assistantMsg.ToolCalls[i] = openai.ChatCompletionMessageToolCallParam{
|
||||
ID: call.ID,
|
||||
Type: "function",
|
||||
|
@ -116,7 +116,7 @@ func (p *openaiProvider) convertToOpenAIMessages(messages []message.Message) []o
|
|||
})
|
||||
|
||||
case message.Tool:
|
||||
for _, result := range msg.ToolResults {
|
||||
for _, result := range msg.ToolResults() {
|
||||
chatMessages = append(chatMessages,
|
||||
openai.ToolMessage(result.Content, result.ToolCallID),
|
||||
)
|
||||
|
@ -276,3 +276,4 @@ func (p *openaiProvider) StreamResponse(ctx context.Context, messages []message.
|
|||
|
||||
return eventChan, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -27,9 +27,10 @@ type TokenUsage struct {
|
|||
}
|
||||
|
||||
type ProviderResponse struct {
|
||||
Content string
|
||||
ToolCalls []message.ToolCall
|
||||
Usage TokenUsage
|
||||
Content string
|
||||
ToolCalls []message.ToolCall
|
||||
Usage TokenUsage
|
||||
FinishReason string
|
||||
}
|
||||
|
||||
type ProviderEvent struct {
|
||||
|
|
229
internal/llm/tools/diagnostics.go
Normal file
229
internal/llm/tools/diagnostics.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
type diagnosticsTool struct {
|
||||
lspClients map[string]*lsp.Client
|
||||
}
|
||||
|
||||
const (
|
||||
DiagnosticsToolName = "diagnostics"
|
||||
)
|
||||
|
||||
type DiagnosticsParams struct {
|
||||
FilePath string `json:"file_path"`
|
||||
}
|
||||
|
||||
func (b *diagnosticsTool) Info() ToolInfo {
|
||||
return ToolInfo{
|
||||
Name: DiagnosticsToolName,
|
||||
Description: "Get diagnostics for a file and/or project.",
|
||||
Parameters: map[string]any{
|
||||
"file_path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The path to the file to get diagnostics for (leave w empty for project diagnostics)",
|
||||
},
|
||||
},
|
||||
Required: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *diagnosticsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
|
||||
var params DiagnosticsParams
|
||||
if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil {
|
||||
return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
|
||||
}
|
||||
|
||||
lsps := b.lspClients
|
||||
|
||||
if len(lsps) == 0 {
|
||||
return NewTextErrorResponse("no LSP clients available"), nil
|
||||
}
|
||||
|
||||
if params.FilePath == "" {
|
||||
notifyLspOpenFile(ctx, params.FilePath, lsps)
|
||||
}
|
||||
|
||||
output := appendDiagnostics(params.FilePath, lsps)
|
||||
|
||||
return NewTextResponse(output), nil
|
||||
}
|
||||
|
||||
func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*lsp.Client) {
|
||||
for _, client := range lsps {
|
||||
err := client.OpenFile(ctx, filePath)
|
||||
if err != nil {
|
||||
// Wait for the file to be opened and diagnostics to be received
|
||||
// TODO: see if we can do this in a more efficient way
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func appendDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
|
||||
fileDiagnostics := []string{}
|
||||
projectDiagnostics := []string{}
|
||||
|
||||
// Enhanced format function that includes more diagnostic information
|
||||
formatDiagnostic := func(pth string, diagnostic protocol.Diagnostic, source string) string {
|
||||
// Base components
|
||||
severity := "Info"
|
||||
switch diagnostic.Severity {
|
||||
case protocol.SeverityError:
|
||||
severity = "Error"
|
||||
case protocol.SeverityWarning:
|
||||
severity = "Warn"
|
||||
case protocol.SeverityHint:
|
||||
severity = "Hint"
|
||||
}
|
||||
|
||||
// Location information
|
||||
location := fmt.Sprintf("%s:%d:%d", pth, diagnostic.Range.Start.Line+1, diagnostic.Range.Start.Character+1)
|
||||
|
||||
// Source information (LSP name)
|
||||
sourceInfo := ""
|
||||
if diagnostic.Source != "" {
|
||||
sourceInfo = diagnostic.Source
|
||||
} else if source != "" {
|
||||
sourceInfo = source
|
||||
}
|
||||
|
||||
// Code information
|
||||
codeInfo := ""
|
||||
if diagnostic.Code != nil {
|
||||
codeInfo = fmt.Sprintf("[%v]", diagnostic.Code)
|
||||
}
|
||||
|
||||
// Tags information
|
||||
tagsInfo := ""
|
||||
if len(diagnostic.Tags) > 0 {
|
||||
tags := []string{}
|
||||
for _, tag := range diagnostic.Tags {
|
||||
switch tag {
|
||||
case protocol.Unnecessary:
|
||||
tags = append(tags, "unnecessary")
|
||||
case protocol.Deprecated:
|
||||
tags = append(tags, "deprecated")
|
||||
}
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
tagsInfo = fmt.Sprintf(" (%s)", strings.Join(tags, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble the full diagnostic message
|
||||
return fmt.Sprintf("%s: %s [%s]%s%s %s",
|
||||
severity,
|
||||
location,
|
||||
sourceInfo,
|
||||
codeInfo,
|
||||
tagsInfo,
|
||||
diagnostic.Message)
|
||||
}
|
||||
|
||||
for lspName, client := range lsps {
|
||||
diagnostics := client.GetDiagnostics()
|
||||
if len(diagnostics) > 0 {
|
||||
for location, diags := range diagnostics {
|
||||
isCurrentFile := location.Path() == filePath
|
||||
|
||||
// Group diagnostics by severity for better organization
|
||||
for _, diag := range diags {
|
||||
formattedDiag := formatDiagnostic(location.Path(), diag, lspName)
|
||||
|
||||
if isCurrentFile {
|
||||
fileDiagnostics = append(fileDiagnostics, formattedDiag)
|
||||
} else {
|
||||
projectDiagnostics = append(projectDiagnostics, formattedDiag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort diagnostics by severity (errors first) and then by location
|
||||
sort.Slice(fileDiagnostics, func(i, j int) bool {
|
||||
iIsError := strings.HasPrefix(fileDiagnostics[i], "Error")
|
||||
jIsError := strings.HasPrefix(fileDiagnostics[j], "Error")
|
||||
if iIsError != jIsError {
|
||||
return iIsError // Errors come first
|
||||
}
|
||||
return fileDiagnostics[i] < fileDiagnostics[j] // Then alphabetically
|
||||
})
|
||||
|
||||
sort.Slice(projectDiagnostics, func(i, j int) bool {
|
||||
iIsError := strings.HasPrefix(projectDiagnostics[i], "Error")
|
||||
jIsError := strings.HasPrefix(projectDiagnostics[j], "Error")
|
||||
if iIsError != jIsError {
|
||||
return iIsError
|
||||
}
|
||||
return projectDiagnostics[i] < projectDiagnostics[j]
|
||||
})
|
||||
|
||||
output := ""
|
||||
|
||||
if len(fileDiagnostics) > 0 {
|
||||
output += "\n<file_diagnostics>\n"
|
||||
if len(fileDiagnostics) > 10 {
|
||||
output += strings.Join(fileDiagnostics[:10], "\n")
|
||||
output += fmt.Sprintf("\n... and %d more diagnostics", len(fileDiagnostics)-10)
|
||||
} else {
|
||||
output += strings.Join(fileDiagnostics, "\n")
|
||||
}
|
||||
output += "\n</file_diagnostics>\n"
|
||||
}
|
||||
|
||||
if len(projectDiagnostics) > 0 {
|
||||
output += "\n<project_diagnostics>\n"
|
||||
if len(projectDiagnostics) > 10 {
|
||||
output += strings.Join(projectDiagnostics[:10], "\n")
|
||||
output += fmt.Sprintf("\n... and %d more diagnostics", len(projectDiagnostics)-10)
|
||||
} else {
|
||||
output += strings.Join(projectDiagnostics, "\n")
|
||||
}
|
||||
output += "\n</project_diagnostics>\n"
|
||||
}
|
||||
|
||||
// Add summary counts
|
||||
if len(fileDiagnostics) > 0 || len(projectDiagnostics) > 0 {
|
||||
fileErrors := countSeverity(fileDiagnostics, "Error")
|
||||
fileWarnings := countSeverity(fileDiagnostics, "Warn")
|
||||
projectErrors := countSeverity(projectDiagnostics, "Error")
|
||||
projectWarnings := countSeverity(projectDiagnostics, "Warn")
|
||||
|
||||
output += "\n<diagnostic_summary>\n"
|
||||
output += fmt.Sprintf("Current file: %d errors, %d warnings\n", fileErrors, fileWarnings)
|
||||
output += fmt.Sprintf("Project: %d errors, %d warnings\n", projectErrors, projectWarnings)
|
||||
output += "</diagnostic_summary>\n"
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
// Helper function to count diagnostics by severity
|
||||
func countSeverity(diagnostics []string, severity string) int {
|
||||
count := 0
|
||||
for _, diag := range diagnostics {
|
||||
if strings.HasPrefix(diag, severity) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func NewDiagnosticsTool(lspClients map[string]*lsp.Client) BaseTool {
|
||||
return &diagnosticsTool{
|
||||
lspClients,
|
||||
}
|
||||
}
|
|
@ -10,11 +10,14 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/config"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp"
|
||||
"github.com/kujtimiihoxha/termai/internal/permission"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
type editTool struct{}
|
||||
type editTool struct {
|
||||
lspClients map[string]*lsp.Client
|
||||
}
|
||||
|
||||
const (
|
||||
EditToolName = "edit"
|
||||
|
@ -71,6 +74,7 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
|
|||
params.FilePath = filepath.Join(wd, params.FilePath)
|
||||
}
|
||||
|
||||
notifyLspOpenFile(ctx, params.FilePath, e.lspClients)
|
||||
if params.OldString == "" {
|
||||
result, err := createNewFile(params.FilePath, params.NewString)
|
||||
if err != nil {
|
||||
|
@ -91,6 +95,9 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
|
|||
if err != nil {
|
||||
return NewTextErrorResponse(fmt.Sprintf("error replacing content: %s", err)), nil
|
||||
}
|
||||
|
||||
result = fmt.Sprintf("<result>\n%s\n</result>\n", result)
|
||||
result += appendDiagnostics(params.FilePath, e.lspClients)
|
||||
return NewTextResponse(result), nil
|
||||
}
|
||||
|
||||
|
@ -296,18 +303,18 @@ func GenerateDiff(oldContent, newContent string) string {
|
|||
|
||||
switch diff.Type {
|
||||
case diffmatchpatch.DiffInsert:
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
for line := range strings.SplitSeq(text, "\n") {
|
||||
_, _ = buff.WriteString("+ " + line + "\n")
|
||||
}
|
||||
case diffmatchpatch.DiffDelete:
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
for line := range strings.SplitSeq(text, "\n") {
|
||||
_, _ = buff.WriteString("- " + line + "\n")
|
||||
}
|
||||
case diffmatchpatch.DiffEqual:
|
||||
if len(text) > 40 {
|
||||
_, _ = buff.WriteString(" " + text[:20] + "..." + text[len(text)-20:] + "\n")
|
||||
} else {
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
for line := range strings.SplitSeq(text, "\n") {
|
||||
_, _ = buff.WriteString(" " + line + "\n")
|
||||
}
|
||||
}
|
||||
|
@ -366,6 +373,8 @@ When making edits:
|
|||
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
|
||||
}
|
||||
|
||||
func NewEditTool() BaseTool {
|
||||
return &editTool{}
|
||||
func NewEditTool(lspClients map[string]*lsp.Client) BaseTool {
|
||||
return &editTool{
|
||||
lspClients,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@ func (s *PersistentShell) killChildren() {
|
|||
return
|
||||
}
|
||||
|
||||
for _, pidStr := range strings.Split(string(output), "\n") {
|
||||
for pidStr := range strings.SplitSeq(string(output), "\n") {
|
||||
if pidStr = strings.TrimSpace(pidStr); pidStr != "" {
|
||||
var pid int
|
||||
fmt.Sscanf(pidStr, "%d", &pid)
|
||||
|
|
|
@ -11,9 +11,12 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/config"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp"
|
||||
)
|
||||
|
||||
type viewTool struct{}
|
||||
type viewTool struct {
|
||||
lspClients map[string]*lsp.Client
|
||||
}
|
||||
|
||||
const (
|
||||
ViewToolName = "view"
|
||||
|
@ -127,15 +130,18 @@ func (v *viewTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
|
|||
return NewTextErrorResponse(fmt.Sprintf("Failed to read file: %s", err)), nil
|
||||
}
|
||||
|
||||
notifyLspOpenFile(ctx, filePath, v.lspClients)
|
||||
output := "<file>\n"
|
||||
// Format the output with line numbers
|
||||
output := addLineNumbers(content, params.Offset+1)
|
||||
output += addLineNumbers(content, params.Offset+1)
|
||||
|
||||
// Add a note if the content was truncated
|
||||
if lineCount > params.Offset+len(strings.Split(content, "\n")) {
|
||||
output += fmt.Sprintf("\n\n(File has more lines. Use 'offset' parameter to read beyond line %d)",
|
||||
params.Offset+len(strings.Split(content, "\n")))
|
||||
}
|
||||
|
||||
output += "\n</file>\n"
|
||||
output += appendDiagnostics(filePath, v.lspClients)
|
||||
recordFileRead(filePath)
|
||||
return NewTextResponse(output), nil
|
||||
}
|
||||
|
@ -155,10 +161,10 @@ func addLineNumbers(content string, startLine int) string {
|
|||
numStr := fmt.Sprintf("%d", lineNum)
|
||||
|
||||
if len(numStr) >= 6 {
|
||||
result = append(result, fmt.Sprintf("%s\t%s", numStr, line))
|
||||
result = append(result, fmt.Sprintf("%s|%s", numStr, line))
|
||||
} else {
|
||||
paddedNum := fmt.Sprintf("%6s", numStr)
|
||||
result = append(result, fmt.Sprintf("%s\t|%s", paddedNum, line))
|
||||
result = append(result, fmt.Sprintf("%s|%s", paddedNum, line))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,8 +179,9 @@ func readTextFile(filePath string, offset, limit int) (string, int, error) {
|
|||
defer file.Close()
|
||||
|
||||
lineCount := 0
|
||||
|
||||
scanner := NewLineScanner(file)
|
||||
if offset > 0 {
|
||||
scanner := NewLineScanner(file)
|
||||
for lineCount < offset && scanner.Scan() {
|
||||
lineCount++
|
||||
}
|
||||
|
@ -192,7 +199,6 @@ func readTextFile(filePath string, offset, limit int) (string, int, error) {
|
|||
|
||||
var lines []string
|
||||
lineCount = offset
|
||||
scanner := NewLineScanner(file)
|
||||
|
||||
for scanner.Scan() && len(lines) < limit {
|
||||
lineCount++
|
||||
|
@ -290,6 +296,8 @@ TIPS:
|
|||
- When viewing large files, use the offset parameter to read specific sections`
|
||||
}
|
||||
|
||||
func NewViewTool() BaseTool {
|
||||
return &viewTool{}
|
||||
func NewViewTool(lspClients map[string]*lsp.Client) BaseTool {
|
||||
return &viewTool{
|
||||
lspClients,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/config"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp"
|
||||
"github.com/kujtimiihoxha/termai/internal/permission"
|
||||
)
|
||||
|
||||
type writeTool struct{}
|
||||
type writeTool struct {
|
||||
lspClients map[string]*lsp.Client
|
||||
}
|
||||
|
||||
const (
|
||||
WriteToolName = "write"
|
||||
|
@ -96,6 +99,8 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
|
|||
if err = os.MkdirAll(dir, 0o755); err != nil {
|
||||
return NewTextErrorResponse(fmt.Sprintf("Failed to create parent directories: %s", err)), nil
|
||||
}
|
||||
|
||||
notifyLspOpenFile(ctx, filePath, w.lspClients)
|
||||
p := permission.Default.Request(
|
||||
permission.CreatePermissionRequest{
|
||||
Path: filePath,
|
||||
|
@ -122,7 +127,10 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
|
|||
recordFileWrite(filePath)
|
||||
recordFileRead(filePath)
|
||||
|
||||
return NewTextResponse(fmt.Sprintf("File successfully written: %s", filePath)), nil
|
||||
result := fmt.Sprintf("File successfully written: %s", filePath)
|
||||
result = fmt.Sprintf("<result>\n%s\n</result>", result)
|
||||
result += appendDiagnostics(filePath, w.lspClients)
|
||||
return NewTextResponse(result), nil
|
||||
}
|
||||
|
||||
func writeDescription() string {
|
||||
|
@ -156,6 +164,8 @@ TIPS:
|
|||
- Always include descriptive comments when making changes to existing code`
|
||||
}
|
||||
|
||||
func NewWriteTool() BaseTool {
|
||||
return &writeTool{}
|
||||
func NewWriteTool(lspClients map[string]*lsp.Client) BaseTool {
|
||||
return &writeTool{
|
||||
lspClients,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,13 +8,14 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp"
|
||||
"github.com/kujtimiihoxha/termai/internal/permission"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriteTool_Info(t *testing.T) {
|
||||
tool := NewWriteTool()
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
info := tool.Info()
|
||||
|
||||
assert.Equal(t, WriteToolName, info.Name)
|
||||
|
@ -40,11 +41,11 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("creates a new file successfully", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
filePath := filepath.Join(tempDir, "new_file.txt")
|
||||
content := "This is a test content"
|
||||
|
||||
|
||||
params := WriteParams{
|
||||
FilePath: filePath,
|
||||
Content: content,
|
||||
|
@ -70,11 +71,11 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("creates file with nested directories", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
filePath := filepath.Join(tempDir, "nested/dirs/new_file.txt")
|
||||
content := "Content in nested directory"
|
||||
|
||||
|
||||
params := WriteParams{
|
||||
FilePath: filePath,
|
||||
Content: content,
|
||||
|
@ -100,17 +101,17 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("updates existing file", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
// Create a file first
|
||||
filePath := filepath.Join(tempDir, "existing_file.txt")
|
||||
initialContent := "Initial content"
|
||||
err := os.WriteFile(filePath, []byte(initialContent), 0644)
|
||||
err := os.WriteFile(filePath, []byte(initialContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
// Record the file read to avoid modification time check failure
|
||||
recordFileRead(filePath)
|
||||
|
||||
|
||||
// Update the file
|
||||
updatedContent := "Updated content"
|
||||
params := WriteParams{
|
||||
|
@ -138,8 +139,8 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("handles invalid parameters", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
call := ToolCall{
|
||||
Name: WriteToolName,
|
||||
Input: "invalid json",
|
||||
|
@ -152,8 +153,8 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("handles missing file_path", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
params := WriteParams{
|
||||
FilePath: "",
|
||||
Content: "Some content",
|
||||
|
@ -174,8 +175,8 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("handles missing content", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
params := WriteParams{
|
||||
FilePath: filepath.Join(tempDir, "file.txt"),
|
||||
Content: "",
|
||||
|
@ -196,13 +197,13 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("handles writing to a directory path", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
// Create a directory
|
||||
dirPath := filepath.Join(tempDir, "test_dir")
|
||||
err := os.Mkdir(dirPath, 0755)
|
||||
err := os.Mkdir(dirPath, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
params := WriteParams{
|
||||
FilePath: dirPath,
|
||||
Content: "Some content",
|
||||
|
@ -223,8 +224,8 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("handles permission denied", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(false)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
filePath := filepath.Join(tempDir, "permission_denied.txt")
|
||||
params := WriteParams{
|
||||
FilePath: filePath,
|
||||
|
@ -242,7 +243,7 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
response, err := tool.Run(context.Background(), call)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, response.Content, "Permission denied")
|
||||
|
||||
|
||||
// Verify file was not created
|
||||
_, err = os.Stat(filePath)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
|
@ -250,14 +251,14 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("detects file modified since last read", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
// Create a file
|
||||
filePath := filepath.Join(tempDir, "modified_file.txt")
|
||||
initialContent := "Initial content"
|
||||
err := os.WriteFile(filePath, []byte(initialContent), 0644)
|
||||
err := os.WriteFile(filePath, []byte(initialContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
// Record an old read time
|
||||
fileRecordMutex.Lock()
|
||||
fileRecords[filePath] = fileRecord{
|
||||
|
@ -265,7 +266,7 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
readTime: time.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
fileRecordMutex.Unlock()
|
||||
|
||||
|
||||
// Try to update the file
|
||||
params := WriteParams{
|
||||
FilePath: filePath,
|
||||
|
@ -283,7 +284,7 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
response, err := tool.Run(context.Background(), call)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, response.Content, "has been modified since it was last read")
|
||||
|
||||
|
||||
// Verify file was not modified
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
require.NoError(t, err)
|
||||
|
@ -292,17 +293,17 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
|
||||
t.Run("skips writing when content is identical", func(t *testing.T) {
|
||||
permission.Default = newMockPermissionService(true)
|
||||
tool := NewWriteTool()
|
||||
|
||||
tool := NewWriteTool(make(map[string]*lsp.Client))
|
||||
|
||||
// Create a file
|
||||
filePath := filepath.Join(tempDir, "identical_content.txt")
|
||||
content := "Content that won't change"
|
||||
err := os.WriteFile(filePath, []byte(content), 0644)
|
||||
err := os.WriteFile(filePath, []byte(content), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
// Record a read time
|
||||
recordFileRead(filePath)
|
||||
|
||||
|
||||
// Try to write the same content
|
||||
params := WriteParams{
|
||||
FilePath: filePath,
|
||||
|
@ -321,4 +322,5 @@ func TestWriteTool_Run(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Contains(t, response.Content, "already contains the exact content")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
429
internal/lsp/client.go
Normal file
429
internal/lsp/client.go
Normal file
|
@ -0,0 +1,429 @@
|
|||
package lsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdout *bufio.Reader
|
||||
stderr io.ReadCloser
|
||||
|
||||
// Request ID counter
|
||||
nextID atomic.Int32
|
||||
|
||||
// Response handlers
|
||||
handlers map[int32]chan *Message
|
||||
handlersMu sync.RWMutex
|
||||
|
||||
// Server request handlers
|
||||
serverRequestHandlers map[string]ServerRequestHandler
|
||||
serverHandlersMu sync.RWMutex
|
||||
|
||||
// Notification handlers
|
||||
notificationHandlers map[string]NotificationHandler
|
||||
notificationMu sync.RWMutex
|
||||
|
||||
// Diagnostic cache
|
||||
diagnostics map[protocol.DocumentUri][]protocol.Diagnostic
|
||||
diagnosticsMu sync.RWMutex
|
||||
|
||||
// Files are currently opened by the LSP
|
||||
openFiles map[string]*OpenFileInfo
|
||||
openFilesMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewClient(command string, args ...string) (*Client, error) {
|
||||
cmd := exec.Command(command, args...)
|
||||
// Copy env
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
Cmd: cmd,
|
||||
stdin: stdin,
|
||||
stdout: bufio.NewReader(stdout),
|
||||
stderr: stderr,
|
||||
handlers: make(map[int32]chan *Message),
|
||||
notificationHandlers: make(map[string]NotificationHandler),
|
||||
serverRequestHandlers: make(map[string]ServerRequestHandler),
|
||||
diagnostics: make(map[protocol.DocumentUri][]protocol.Diagnostic),
|
||||
openFiles: make(map[string]*OpenFileInfo),
|
||||
}
|
||||
|
||||
// Start the LSP server process
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start LSP server: %w", err)
|
||||
}
|
||||
|
||||
// Handle stderr in a separate goroutine
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
fmt.Fprintf(os.Stderr, "LSP Server: %s\n", scanner.Text())
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading stderr: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start message handling loop
|
||||
go client.handleMessages()
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) RegisterNotificationHandler(method string, handler NotificationHandler) {
|
||||
c.notificationMu.Lock()
|
||||
defer c.notificationMu.Unlock()
|
||||
c.notificationHandlers[method] = handler
|
||||
}
|
||||
|
||||
func (c *Client) RegisterServerRequestHandler(method string, handler ServerRequestHandler) {
|
||||
c.serverHandlersMu.Lock()
|
||||
defer c.serverHandlersMu.Unlock()
|
||||
c.serverRequestHandlers[method] = handler
|
||||
}
|
||||
|
||||
func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (*protocol.InitializeResult, error) {
|
||||
initParams := &protocol.InitializeParams{
|
||||
WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
|
||||
WorkspaceFolders: []protocol.WorkspaceFolder{
|
||||
{
|
||||
URI: protocol.URI("file://" + workspaceDir),
|
||||
Name: workspaceDir,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
XInitializeParams: protocol.XInitializeParams{
|
||||
ProcessID: int32(os.Getpid()),
|
||||
ClientInfo: &protocol.ClientInfo{
|
||||
Name: "mcp-language-server",
|
||||
Version: "0.1.0",
|
||||
},
|
||||
RootPath: workspaceDir,
|
||||
RootURI: protocol.DocumentUri("file://" + workspaceDir),
|
||||
Capabilities: protocol.ClientCapabilities{
|
||||
Workspace: protocol.WorkspaceClientCapabilities{
|
||||
Configuration: true,
|
||||
DidChangeConfiguration: protocol.DidChangeConfigurationClientCapabilities{
|
||||
DynamicRegistration: true,
|
||||
},
|
||||
DidChangeWatchedFiles: protocol.DidChangeWatchedFilesClientCapabilities{
|
||||
DynamicRegistration: true,
|
||||
RelativePatternSupport: true,
|
||||
},
|
||||
},
|
||||
TextDocument: protocol.TextDocumentClientCapabilities{
|
||||
Synchronization: &protocol.TextDocumentSyncClientCapabilities{
|
||||
DynamicRegistration: true,
|
||||
DidSave: true,
|
||||
},
|
||||
Completion: protocol.CompletionClientCapabilities{
|
||||
CompletionItem: protocol.ClientCompletionItemOptions{},
|
||||
},
|
||||
CodeLens: &protocol.CodeLensClientCapabilities{
|
||||
DynamicRegistration: true,
|
||||
},
|
||||
DocumentSymbol: protocol.DocumentSymbolClientCapabilities{},
|
||||
CodeAction: protocol.CodeActionClientCapabilities{
|
||||
CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
|
||||
CodeActionKind: protocol.ClientCodeActionKindOptions{
|
||||
ValueSet: []protocol.CodeActionKind{},
|
||||
},
|
||||
},
|
||||
},
|
||||
PublishDiagnostics: protocol.PublishDiagnosticsClientCapabilities{
|
||||
VersionSupport: true,
|
||||
},
|
||||
SemanticTokens: protocol.SemanticTokensClientCapabilities{
|
||||
Requests: protocol.ClientSemanticTokensRequestOptions{
|
||||
Range: &protocol.Or_ClientSemanticTokensRequestOptions_range{},
|
||||
Full: &protocol.Or_ClientSemanticTokensRequestOptions_full{},
|
||||
},
|
||||
TokenTypes: []string{},
|
||||
TokenModifiers: []string{},
|
||||
Formats: []protocol.TokenFormat{},
|
||||
},
|
||||
},
|
||||
Window: protocol.WindowClientCapabilities{},
|
||||
},
|
||||
InitializationOptions: map[string]any{
|
||||
"codelenses": map[string]bool{
|
||||
"generate": true,
|
||||
"regenerate_cgo": true,
|
||||
"test": true,
|
||||
"tidy": true,
|
||||
"upgrade_dependency": true,
|
||||
"vendor": true,
|
||||
"vulncheck": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var result protocol.InitializeResult
|
||||
if err := c.Call(ctx, "initialize", initParams, &result); err != nil {
|
||||
return nil, fmt.Errorf("initialize failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
|
||||
return nil, fmt.Errorf("initialized notification failed: %w", err)
|
||||
}
|
||||
|
||||
// Register handlers
|
||||
c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
|
||||
c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
|
||||
c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
|
||||
c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
|
||||
c.RegisterNotificationHandler("textDocument/publishDiagnostics",
|
||||
func(params json.RawMessage) { HandleDiagnostics(c, params) })
|
||||
|
||||
// Notify the LSP server
|
||||
err := c.Initialized(ctx, protocol.InitializedParams{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initialization failed: %w", err)
|
||||
}
|
||||
|
||||
// LSP sepecific Initialization
|
||||
path := strings.ToLower(c.Cmd.Path)
|
||||
switch {
|
||||
case strings.Contains(path, "typescript-language-server"):
|
||||
// err := initializeTypescriptLanguageServer(ctx, c, workspaceDir)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
// Try to close all open files first
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Attempt to close files but continue shutdown regardless
|
||||
c.CloseAllFiles(ctx)
|
||||
|
||||
// Close stdin to signal the server
|
||||
if err := c.stdin.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close stdin: %w", err)
|
||||
}
|
||||
|
||||
// Use a channel to handle the Wait with timeout
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.Cmd.Wait()
|
||||
}()
|
||||
|
||||
// Wait for process to exit with timeout
|
||||
select {
|
||||
case err := <-done:
|
||||
return err
|
||||
case <-time.After(2 * time.Second):
|
||||
// If we timeout, try to kill the process
|
||||
if err := c.Cmd.Process.Kill(); err != nil {
|
||||
return fmt.Errorf("failed to kill process: %w", err)
|
||||
}
|
||||
return fmt.Errorf("process killed after timeout")
|
||||
}
|
||||
}
|
||||
|
||||
type ServerState int
|
||||
|
||||
const (
|
||||
StateStarting ServerState = iota
|
||||
StateReady
|
||||
StateError
|
||||
)
|
||||
|
||||
func (c *Client) WaitForServerReady(ctx context.Context) error {
|
||||
// TODO: wait for specific messages or poll workspace/symbol
|
||||
time.Sleep(time.Second * 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
type OpenFileInfo struct {
|
||||
Version int32
|
||||
URI protocol.DocumentUri
|
||||
}
|
||||
|
||||
func (c *Client) OpenFile(ctx context.Context, filepath string) error {
|
||||
uri := fmt.Sprintf("file://%s", filepath)
|
||||
|
||||
c.openFilesMu.Lock()
|
||||
if _, exists := c.openFiles[uri]; exists {
|
||||
c.openFilesMu.Unlock()
|
||||
return nil // Already open
|
||||
}
|
||||
c.openFilesMu.Unlock()
|
||||
|
||||
// Skip files that do not exist or cannot be read
|
||||
content, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading file: %w", err)
|
||||
}
|
||||
|
||||
params := protocol.DidOpenTextDocumentParams{
|
||||
TextDocument: protocol.TextDocumentItem{
|
||||
URI: protocol.DocumentUri(uri),
|
||||
LanguageID: DetectLanguageID(uri),
|
||||
Version: 1,
|
||||
Text: string(content),
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.Notify(ctx, "textDocument/didOpen", params); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.openFilesMu.Lock()
|
||||
c.openFiles[uri] = &OpenFileInfo{
|
||||
Version: 1,
|
||||
URI: protocol.DocumentUri(uri),
|
||||
}
|
||||
c.openFilesMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
|
||||
uri := fmt.Sprintf("file://%s", filepath)
|
||||
|
||||
content, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading file: %w", err)
|
||||
}
|
||||
|
||||
c.openFilesMu.Lock()
|
||||
fileInfo, isOpen := c.openFiles[uri]
|
||||
if !isOpen {
|
||||
c.openFilesMu.Unlock()
|
||||
return fmt.Errorf("cannot notify change for unopened file: %s", filepath)
|
||||
}
|
||||
|
||||
// Increment version
|
||||
fileInfo.Version++
|
||||
version := fileInfo.Version
|
||||
c.openFilesMu.Unlock()
|
||||
|
||||
params := protocol.DidChangeTextDocumentParams{
|
||||
TextDocument: protocol.VersionedTextDocumentIdentifier{
|
||||
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
|
||||
URI: protocol.DocumentUri(uri),
|
||||
},
|
||||
Version: version,
|
||||
},
|
||||
ContentChanges: []protocol.TextDocumentContentChangeEvent{
|
||||
{
|
||||
Value: protocol.TextDocumentContentChangeWholeDocument{
|
||||
Text: string(content),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return c.Notify(ctx, "textDocument/didChange", params)
|
||||
}
|
||||
|
||||
func (c *Client) CloseFile(ctx context.Context, filepath string) error {
|
||||
uri := fmt.Sprintf("file://%s", filepath)
|
||||
|
||||
c.openFilesMu.Lock()
|
||||
if _, exists := c.openFiles[uri]; !exists {
|
||||
c.openFilesMu.Unlock()
|
||||
return nil // Already closed
|
||||
}
|
||||
c.openFilesMu.Unlock()
|
||||
|
||||
params := protocol.DidCloseTextDocumentParams{
|
||||
TextDocument: protocol.TextDocumentIdentifier{
|
||||
URI: protocol.DocumentUri(uri),
|
||||
},
|
||||
}
|
||||
log.Println("Closing", params.TextDocument.URI.Dir())
|
||||
if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.openFilesMu.Lock()
|
||||
delete(c.openFiles, uri)
|
||||
c.openFilesMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) IsFileOpen(filepath string) bool {
|
||||
uri := fmt.Sprintf("file://%s", filepath)
|
||||
c.openFilesMu.RLock()
|
||||
defer c.openFilesMu.RUnlock()
|
||||
_, exists := c.openFiles[uri]
|
||||
return exists
|
||||
}
|
||||
|
||||
// CloseAllFiles closes all currently open files
|
||||
func (c *Client) CloseAllFiles(ctx context.Context) {
|
||||
c.openFilesMu.Lock()
|
||||
filesToClose := make([]string, 0, len(c.openFiles))
|
||||
|
||||
// First collect all URIs that need to be closed
|
||||
for uri := range c.openFiles {
|
||||
// Convert URI back to file path by trimming "file://" prefix
|
||||
filePath := strings.TrimPrefix(uri, "file://")
|
||||
filesToClose = append(filesToClose, filePath)
|
||||
}
|
||||
c.openFilesMu.Unlock()
|
||||
|
||||
// Then close them all
|
||||
for _, filePath := range filesToClose {
|
||||
err := c.CloseFile(ctx, filePath)
|
||||
if err != nil && debug {
|
||||
log.Printf("Error closing file %s: %v", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("Closed %d files", len(filesToClose))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetFileDiagnostics(uri protocol.DocumentUri) []protocol.Diagnostic {
|
||||
c.diagnosticsMu.RLock()
|
||||
defer c.diagnosticsMu.RUnlock()
|
||||
|
||||
return c.diagnostics[uri]
|
||||
}
|
||||
|
||||
func (c *Client) GetDiagnostics() map[protocol.DocumentUri][]protocol.Diagnostic {
|
||||
return c.diagnostics
|
||||
}
|
104
internal/lsp/handlers.go
Normal file
104
internal/lsp/handlers.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package lsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/util"
|
||||
)
|
||||
|
||||
// Requests
|
||||
|
||||
func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
|
||||
return []map[string]any{{}}, nil
|
||||
}
|
||||
|
||||
func HandleRegisterCapability(params json.RawMessage) (any, error) {
|
||||
var registerParams protocol.RegistrationParams
|
||||
if err := json.Unmarshal(params, ®isterParams); err != nil {
|
||||
log.Printf("Error unmarshaling registration params: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, reg := range registerParams.Registrations {
|
||||
switch reg.Method {
|
||||
case "workspace/didChangeWatchedFiles":
|
||||
// Parse the registration options
|
||||
optionsJSON, err := json.Marshal(reg.RegisterOptions)
|
||||
if err != nil {
|
||||
log.Printf("Error marshaling registration options: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var options protocol.DidChangeWatchedFilesRegistrationOptions
|
||||
if err := json.Unmarshal(optionsJSON, &options); err != nil {
|
||||
log.Printf("Error unmarshaling registration options: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Store the file watchers registrations
|
||||
notifyFileWatchRegistration(reg.ID, options.Watchers)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func HandleApplyEdit(params json.RawMessage) (any, error) {
|
||||
var edit protocol.ApplyWorkspaceEditParams
|
||||
if err := json.Unmarshal(params, &edit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err := util.ApplyWorkspaceEdit(edit.Edit)
|
||||
if err != nil {
|
||||
log.Printf("Error applying workspace edit: %v", err)
|
||||
return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
|
||||
}
|
||||
|
||||
return protocol.ApplyWorkspaceEditResult{Applied: true}, nil
|
||||
}
|
||||
|
||||
// FileWatchRegistrationHandler is a function that will be called when file watch registrations are received
|
||||
type FileWatchRegistrationHandler func(id string, watchers []protocol.FileSystemWatcher)
|
||||
|
||||
// fileWatchHandler holds the current handler for file watch registrations
|
||||
var fileWatchHandler FileWatchRegistrationHandler
|
||||
|
||||
// RegisterFileWatchHandler sets the handler for file watch registrations
|
||||
func RegisterFileWatchHandler(handler FileWatchRegistrationHandler) {
|
||||
fileWatchHandler = handler
|
||||
}
|
||||
|
||||
// notifyFileWatchRegistration notifies the handler about new file watch registrations
|
||||
func notifyFileWatchRegistration(id string, watchers []protocol.FileSystemWatcher) {
|
||||
if fileWatchHandler != nil {
|
||||
fileWatchHandler(id, watchers)
|
||||
}
|
||||
}
|
||||
|
||||
// Notifications
|
||||
|
||||
func HandleServerMessage(params json.RawMessage) {
|
||||
var msg struct {
|
||||
Type int `json:"type"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(params, &msg); err == nil {
|
||||
log.Printf("Server message: %s\n", msg.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleDiagnostics(client *Client, params json.RawMessage) {
|
||||
var diagParams protocol.PublishDiagnosticsParams
|
||||
if err := json.Unmarshal(params, &diagParams); err != nil {
|
||||
log.Printf("Error unmarshaling diagnostic params: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
client.diagnosticsMu.Lock()
|
||||
defer client.diagnosticsMu.Unlock()
|
||||
|
||||
client.diagnostics[diagParams.URI] = diagParams.Diagnostics
|
||||
}
|
132
internal/lsp/language.go
Normal file
132
internal/lsp/language.go
Normal file
|
@ -0,0 +1,132 @@
|
|||
package lsp
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
func DetectLanguageID(uri string) protocol.LanguageKind {
|
||||
ext := strings.ToLower(filepath.Ext(uri))
|
||||
switch ext {
|
||||
case ".abap":
|
||||
return protocol.LangABAP
|
||||
case ".bat":
|
||||
return protocol.LangWindowsBat
|
||||
case ".bib", ".bibtex":
|
||||
return protocol.LangBibTeX
|
||||
case ".clj":
|
||||
return protocol.LangClojure
|
||||
case ".coffee":
|
||||
return protocol.LangCoffeescript
|
||||
case ".c":
|
||||
return protocol.LangC
|
||||
case ".cpp", ".cxx", ".cc", ".c++":
|
||||
return protocol.LangCPP
|
||||
case ".cs":
|
||||
return protocol.LangCSharp
|
||||
case ".css":
|
||||
return protocol.LangCSS
|
||||
case ".d":
|
||||
return protocol.LangD
|
||||
case ".pas", ".pascal":
|
||||
return protocol.LangDelphi
|
||||
case ".diff", ".patch":
|
||||
return protocol.LangDiff
|
||||
case ".dart":
|
||||
return protocol.LangDart
|
||||
case ".dockerfile":
|
||||
return protocol.LangDockerfile
|
||||
case ".ex", ".exs":
|
||||
return protocol.LangElixir
|
||||
case ".erl", ".hrl":
|
||||
return protocol.LangErlang
|
||||
case ".fs", ".fsi", ".fsx", ".fsscript":
|
||||
return protocol.LangFSharp
|
||||
case ".gitcommit":
|
||||
return protocol.LangGitCommit
|
||||
case ".gitrebase":
|
||||
return protocol.LangGitRebase
|
||||
case ".go":
|
||||
return protocol.LangGo
|
||||
case ".groovy":
|
||||
return protocol.LangGroovy
|
||||
case ".hbs", ".handlebars":
|
||||
return protocol.LangHandlebars
|
||||
case ".hs":
|
||||
return protocol.LangHaskell
|
||||
case ".html", ".htm":
|
||||
return protocol.LangHTML
|
||||
case ".ini":
|
||||
return protocol.LangIni
|
||||
case ".java":
|
||||
return protocol.LangJava
|
||||
case ".js":
|
||||
return protocol.LangJavaScript
|
||||
case ".jsx":
|
||||
return protocol.LangJavaScriptReact
|
||||
case ".json":
|
||||
return protocol.LangJSON
|
||||
case ".tex", ".latex":
|
||||
return protocol.LangLaTeX
|
||||
case ".less":
|
||||
return protocol.LangLess
|
||||
case ".lua":
|
||||
return protocol.LangLua
|
||||
case ".makefile", "makefile":
|
||||
return protocol.LangMakefile
|
||||
case ".md", ".markdown":
|
||||
return protocol.LangMarkdown
|
||||
case ".m":
|
||||
return protocol.LangObjectiveC
|
||||
case ".mm":
|
||||
return protocol.LangObjectiveCPP
|
||||
case ".pl":
|
||||
return protocol.LangPerl
|
||||
case ".pm":
|
||||
return protocol.LangPerl6
|
||||
case ".php":
|
||||
return protocol.LangPHP
|
||||
case ".ps1", ".psm1":
|
||||
return protocol.LangPowershell
|
||||
case ".pug", ".jade":
|
||||
return protocol.LangPug
|
||||
case ".py":
|
||||
return protocol.LangPython
|
||||
case ".r":
|
||||
return protocol.LangR
|
||||
case ".cshtml", ".razor":
|
||||
return protocol.LangRazor
|
||||
case ".rb":
|
||||
return protocol.LangRuby
|
||||
case ".rs":
|
||||
return protocol.LangRust
|
||||
case ".scss":
|
||||
return protocol.LangSCSS
|
||||
case ".sass":
|
||||
return protocol.LangSASS
|
||||
case ".scala":
|
||||
return protocol.LangScala
|
||||
case ".shader":
|
||||
return protocol.LangShaderLab
|
||||
case ".sh", ".bash", ".zsh", ".ksh":
|
||||
return protocol.LangShellScript
|
||||
case ".sql":
|
||||
return protocol.LangSQL
|
||||
case ".swift":
|
||||
return protocol.LangSwift
|
||||
case ".ts":
|
||||
return protocol.LangTypeScript
|
||||
case ".tsx":
|
||||
return protocol.LangTypeScriptReact
|
||||
case ".xml":
|
||||
return protocol.LangXML
|
||||
case ".xsl":
|
||||
return protocol.LangXSL
|
||||
case ".yaml", ".yml":
|
||||
return protocol.LangYAML
|
||||
default:
|
||||
return protocol.LanguageKind("") // Unknown language
|
||||
}
|
||||
}
|
554
internal/lsp/methods.go
Normal file
554
internal/lsp/methods.go
Normal file
|
@ -0,0 +1,554 @@
|
|||
// Generated code. Do not edit
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
// Implementation sends a textDocument/implementation request to the LSP server.
|
||||
// A request to resolve the implementation locations of a symbol at a given text document position. The request's parameter is of type TextDocumentPositionParams the response is of type Definition or a Thenable that resolves to such.
|
||||
func (c *Client) Implementation(ctx context.Context, params protocol.ImplementationParams) (protocol.Or_Result_textDocument_implementation, error) {
|
||||
var result protocol.Or_Result_textDocument_implementation
|
||||
err := c.Call(ctx, "textDocument/implementation", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// TypeDefinition sends a textDocument/typeDefinition request to the LSP server.
|
||||
// A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type TextDocumentPositionParams the response is of type Definition or a Thenable that resolves to such.
|
||||
func (c *Client) TypeDefinition(ctx context.Context, params protocol.TypeDefinitionParams) (protocol.Or_Result_textDocument_typeDefinition, error) {
|
||||
var result protocol.Or_Result_textDocument_typeDefinition
|
||||
err := c.Call(ctx, "textDocument/typeDefinition", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DocumentColor sends a textDocument/documentColor request to the LSP server.
|
||||
// A request to list all color symbols found in a given text document. The request's parameter is of type DocumentColorParams the response is of type ColorInformation ColorInformation[] or a Thenable that resolves to such.
|
||||
func (c *Client) DocumentColor(ctx context.Context, params protocol.DocumentColorParams) ([]protocol.ColorInformation, error) {
|
||||
var result []protocol.ColorInformation
|
||||
err := c.Call(ctx, "textDocument/documentColor", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ColorPresentation sends a textDocument/colorPresentation request to the LSP server.
|
||||
// A request to list all presentation for a color. The request's parameter is of type ColorPresentationParams the response is of type ColorInformation ColorInformation[] or a Thenable that resolves to such.
|
||||
func (c *Client) ColorPresentation(ctx context.Context, params protocol.ColorPresentationParams) ([]protocol.ColorPresentation, error) {
|
||||
var result []protocol.ColorPresentation
|
||||
err := c.Call(ctx, "textDocument/colorPresentation", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FoldingRange sends a textDocument/foldingRange request to the LSP server.
|
||||
// A request to provide folding ranges in a document. The request's parameter is of type FoldingRangeParams, the response is of type FoldingRangeList or a Thenable that resolves to such.
|
||||
func (c *Client) FoldingRange(ctx context.Context, params protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
|
||||
var result []protocol.FoldingRange
|
||||
err := c.Call(ctx, "textDocument/foldingRange", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Declaration sends a textDocument/declaration request to the LSP server.
|
||||
// A request to resolve the type definition locations of a symbol at a given text document position. The request's parameter is of type TextDocumentPositionParams the response is of type Declaration or a typed array of DeclarationLink or a Thenable that resolves to such.
|
||||
func (c *Client) Declaration(ctx context.Context, params protocol.DeclarationParams) (protocol.Or_Result_textDocument_declaration, error) {
|
||||
var result protocol.Or_Result_textDocument_declaration
|
||||
err := c.Call(ctx, "textDocument/declaration", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// SelectionRange sends a textDocument/selectionRange request to the LSP server.
|
||||
// A request to provide selection ranges in a document. The request's parameter is of type SelectionRangeParams, the response is of type SelectionRange SelectionRange[] or a Thenable that resolves to such.
|
||||
func (c *Client) SelectionRange(ctx context.Context, params protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) {
|
||||
var result []protocol.SelectionRange
|
||||
err := c.Call(ctx, "textDocument/selectionRange", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// PrepareCallHierarchy sends a textDocument/prepareCallHierarchy request to the LSP server.
|
||||
// A request to result a CallHierarchyItem in a document at a given position. Can be used as an input to an incoming or outgoing call hierarchy. Since 3.16.0
|
||||
func (c *Client) PrepareCallHierarchy(ctx context.Context, params protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) {
|
||||
var result []protocol.CallHierarchyItem
|
||||
err := c.Call(ctx, "textDocument/prepareCallHierarchy", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// IncomingCalls sends a callHierarchy/incomingCalls request to the LSP server.
|
||||
// A request to resolve the incoming calls for a given CallHierarchyItem. Since 3.16.0
|
||||
func (c *Client) IncomingCalls(ctx context.Context, params protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) {
|
||||
var result []protocol.CallHierarchyIncomingCall
|
||||
err := c.Call(ctx, "callHierarchy/incomingCalls", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// OutgoingCalls sends a callHierarchy/outgoingCalls request to the LSP server.
|
||||
// A request to resolve the outgoing calls for a given CallHierarchyItem. Since 3.16.0
|
||||
func (c *Client) OutgoingCalls(ctx context.Context, params protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) {
|
||||
var result []protocol.CallHierarchyOutgoingCall
|
||||
err := c.Call(ctx, "callHierarchy/outgoingCalls", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// SemanticTokensFull sends a textDocument/semanticTokens/full request to the LSP server.
|
||||
// Since 3.16.0
|
||||
func (c *Client) SemanticTokensFull(ctx context.Context, params protocol.SemanticTokensParams) (protocol.SemanticTokens, error) {
|
||||
var result protocol.SemanticTokens
|
||||
err := c.Call(ctx, "textDocument/semanticTokens/full", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// SemanticTokensFullDelta sends a textDocument/semanticTokens/full/delta request to the LSP server.
|
||||
// Since 3.16.0
|
||||
func (c *Client) SemanticTokensFullDelta(ctx context.Context, params protocol.SemanticTokensDeltaParams) (protocol.Or_Result_textDocument_semanticTokens_full_delta, error) {
|
||||
var result protocol.Or_Result_textDocument_semanticTokens_full_delta
|
||||
err := c.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// SemanticTokensRange sends a textDocument/semanticTokens/range request to the LSP server.
|
||||
// Since 3.16.0
|
||||
func (c *Client) SemanticTokensRange(ctx context.Context, params protocol.SemanticTokensRangeParams) (protocol.SemanticTokens, error) {
|
||||
var result protocol.SemanticTokens
|
||||
err := c.Call(ctx, "textDocument/semanticTokens/range", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// LinkedEditingRange sends a textDocument/linkedEditingRange request to the LSP server.
|
||||
// A request to provide ranges that can be edited together. Since 3.16.0
|
||||
func (c *Client) LinkedEditingRange(ctx context.Context, params protocol.LinkedEditingRangeParams) (protocol.LinkedEditingRanges, error) {
|
||||
var result protocol.LinkedEditingRanges
|
||||
err := c.Call(ctx, "textDocument/linkedEditingRange", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// WillCreateFiles sends a workspace/willCreateFiles request to the LSP server.
|
||||
// The will create files request is sent from the client to the server before files are actually created as long as the creation is triggered from within the client. The request can return a WorkspaceEdit which will be applied to workspace before the files are created. Hence the WorkspaceEdit can not manipulate the content of the file to be created. Since 3.16.0
|
||||
func (c *Client) WillCreateFiles(ctx context.Context, params protocol.CreateFilesParams) (protocol.WorkspaceEdit, error) {
|
||||
var result protocol.WorkspaceEdit
|
||||
err := c.Call(ctx, "workspace/willCreateFiles", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// WillRenameFiles sends a workspace/willRenameFiles request to the LSP server.
|
||||
// The will rename files request is sent from the client to the server before files are actually renamed as long as the rename is triggered from within the client. Since 3.16.0
|
||||
func (c *Client) WillRenameFiles(ctx context.Context, params protocol.RenameFilesParams) (protocol.WorkspaceEdit, error) {
|
||||
var result protocol.WorkspaceEdit
|
||||
err := c.Call(ctx, "workspace/willRenameFiles", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// WillDeleteFiles sends a workspace/willDeleteFiles request to the LSP server.
|
||||
// The did delete files notification is sent from the client to the server when files were deleted from within the client. Since 3.16.0
|
||||
func (c *Client) WillDeleteFiles(ctx context.Context, params protocol.DeleteFilesParams) (protocol.WorkspaceEdit, error) {
|
||||
var result protocol.WorkspaceEdit
|
||||
err := c.Call(ctx, "workspace/willDeleteFiles", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Moniker sends a textDocument/moniker request to the LSP server.
|
||||
// A request to get the moniker of a symbol at a given text document position. The request parameter is of type TextDocumentPositionParams. The response is of type Moniker Moniker[] or null.
|
||||
func (c *Client) Moniker(ctx context.Context, params protocol.MonikerParams) ([]protocol.Moniker, error) {
|
||||
var result []protocol.Moniker
|
||||
err := c.Call(ctx, "textDocument/moniker", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// PrepareTypeHierarchy sends a textDocument/prepareTypeHierarchy request to the LSP server.
|
||||
// A request to result a TypeHierarchyItem in a document at a given position. Can be used as an input to a subtypes or supertypes type hierarchy. Since 3.17.0
|
||||
func (c *Client) PrepareTypeHierarchy(ctx context.Context, params protocol.TypeHierarchyPrepareParams) ([]protocol.TypeHierarchyItem, error) {
|
||||
var result []protocol.TypeHierarchyItem
|
||||
err := c.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Supertypes sends a typeHierarchy/supertypes request to the LSP server.
|
||||
// A request to resolve the supertypes for a given TypeHierarchyItem. Since 3.17.0
|
||||
func (c *Client) Supertypes(ctx context.Context, params protocol.TypeHierarchySupertypesParams) ([]protocol.TypeHierarchyItem, error) {
|
||||
var result []protocol.TypeHierarchyItem
|
||||
err := c.Call(ctx, "typeHierarchy/supertypes", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Subtypes sends a typeHierarchy/subtypes request to the LSP server.
|
||||
// A request to resolve the subtypes for a given TypeHierarchyItem. Since 3.17.0
|
||||
func (c *Client) Subtypes(ctx context.Context, params protocol.TypeHierarchySubtypesParams) ([]protocol.TypeHierarchyItem, error) {
|
||||
var result []protocol.TypeHierarchyItem
|
||||
err := c.Call(ctx, "typeHierarchy/subtypes", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// InlineValue sends a textDocument/inlineValue request to the LSP server.
|
||||
// A request to provide inline values in a document. The request's parameter is of type InlineValueParams, the response is of type InlineValue InlineValue[] or a Thenable that resolves to such. Since 3.17.0
|
||||
func (c *Client) InlineValue(ctx context.Context, params protocol.InlineValueParams) ([]protocol.InlineValue, error) {
|
||||
var result []protocol.InlineValue
|
||||
err := c.Call(ctx, "textDocument/inlineValue", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// InlayHint sends a textDocument/inlayHint request to the LSP server.
|
||||
// A request to provide inlay hints in a document. The request's parameter is of type InlayHintsParams, the response is of type InlayHint InlayHint[] or a Thenable that resolves to such. Since 3.17.0
|
||||
func (c *Client) InlayHint(ctx context.Context, params protocol.InlayHintParams) ([]protocol.InlayHint, error) {
|
||||
var result []protocol.InlayHint
|
||||
err := c.Call(ctx, "textDocument/inlayHint", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Resolve sends a inlayHint/resolve request to the LSP server.
|
||||
// A request to resolve additional properties for an inlay hint. The request's parameter is of type InlayHint, the response is of type InlayHint or a Thenable that resolves to such. Since 3.17.0
|
||||
func (c *Client) Resolve(ctx context.Context, params protocol.InlayHint) (protocol.InlayHint, error) {
|
||||
var result protocol.InlayHint
|
||||
err := c.Call(ctx, "inlayHint/resolve", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Diagnostic sends a textDocument/diagnostic request to the LSP server.
|
||||
// The document diagnostic request definition. Since 3.17.0
|
||||
func (c *Client) Diagnostic(ctx context.Context, params protocol.DocumentDiagnosticParams) (protocol.DocumentDiagnosticReport, error) {
|
||||
var result protocol.DocumentDiagnosticReport
|
||||
err := c.Call(ctx, "textDocument/diagnostic", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DiagnosticWorkspace sends a workspace/diagnostic request to the LSP server.
|
||||
// The workspace diagnostic request definition. Since 3.17.0
|
||||
func (c *Client) DiagnosticWorkspace(ctx context.Context, params protocol.WorkspaceDiagnosticParams) (protocol.WorkspaceDiagnosticReport, error) {
|
||||
var result protocol.WorkspaceDiagnosticReport
|
||||
err := c.Call(ctx, "workspace/diagnostic", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// InlineCompletion sends a textDocument/inlineCompletion request to the LSP server.
|
||||
// A request to provide inline completions in a document. The request's parameter is of type InlineCompletionParams, the response is of type InlineCompletion InlineCompletion[] or a Thenable that resolves to such. Since 3.18.0 PROPOSED
|
||||
func (c *Client) InlineCompletion(ctx context.Context, params protocol.InlineCompletionParams) (protocol.Or_Result_textDocument_inlineCompletion, error) {
|
||||
var result protocol.Or_Result_textDocument_inlineCompletion
|
||||
err := c.Call(ctx, "textDocument/inlineCompletion", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// TextDocumentContent sends a workspace/textDocumentContent request to the LSP server.
|
||||
// The workspace/textDocumentContent request is sent from the client to the server to request the content of a text document. Since 3.18.0 PROPOSED
|
||||
func (c *Client) TextDocumentContent(ctx context.Context, params protocol.TextDocumentContentParams) (string, error) {
|
||||
var result string
|
||||
err := c.Call(ctx, "workspace/textDocumentContent", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Initialize sends a initialize request to the LSP server.
|
||||
// The initialize request is sent from the client to the server. It is sent once as the request after starting up the server. The requests parameter is of type InitializeParams the response if of type InitializeResult of a Thenable that resolves to such.
|
||||
func (c *Client) Initialize(ctx context.Context, params protocol.ParamInitialize) (protocol.InitializeResult, error) {
|
||||
var result protocol.InitializeResult
|
||||
err := c.Call(ctx, "initialize", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Shutdown sends a shutdown request to the LSP server.
|
||||
// A shutdown request is sent from the client to the server. It is sent once when the client decides to shutdown the server. The only notification that is sent after a shutdown request is the exit event.
|
||||
func (c *Client) Shutdown(ctx context.Context) error {
|
||||
return c.Call(ctx, "shutdown", nil, nil)
|
||||
}
|
||||
|
||||
// WillSaveWaitUntil sends a textDocument/willSaveWaitUntil request to the LSP server.
|
||||
// A document will save request is sent from the client to the server before the document is actually saved. The request can return an array of TextEdits which will be applied to the text document before it is saved. Please note that clients might drop results if computing the text edits took too long or if a server constantly fails on this request. This is done to keep the save fast and reliable.
|
||||
func (c *Client) WillSaveWaitUntil(ctx context.Context, params protocol.WillSaveTextDocumentParams) ([]protocol.TextEdit, error) {
|
||||
var result []protocol.TextEdit
|
||||
err := c.Call(ctx, "textDocument/willSaveWaitUntil", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Completion sends a textDocument/completion request to the LSP server.
|
||||
// Request to request completion at a given text document position. The request's parameter is of type TextDocumentPosition the response is of type CompletionItem CompletionItem[] or CompletionList or a Thenable that resolves to such. The request can delay the computation of the CompletionItem.detail detail and CompletionItem.documentation documentation properties to the completionItem/resolve request. However, properties that are needed for the initial sorting and filtering, like sortText, filterText, insertText, and textEdit, must not be changed during resolve.
|
||||
func (c *Client) Completion(ctx context.Context, params protocol.CompletionParams) (protocol.Or_Result_textDocument_completion, error) {
|
||||
var result protocol.Or_Result_textDocument_completion
|
||||
err := c.Call(ctx, "textDocument/completion", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ResolveCompletionItem sends a completionItem/resolve request to the LSP server.
|
||||
// Request to resolve additional information for a given completion item.The request's parameter is of type CompletionItem the response is of type CompletionItem or a Thenable that resolves to such.
|
||||
func (c *Client) ResolveCompletionItem(ctx context.Context, params protocol.CompletionItem) (protocol.CompletionItem, error) {
|
||||
var result protocol.CompletionItem
|
||||
err := c.Call(ctx, "completionItem/resolve", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Hover sends a textDocument/hover request to the LSP server.
|
||||
// Request to request hover information at a given text document position. The request's parameter is of type TextDocumentPosition the response is of type Hover or a Thenable that resolves to such.
|
||||
func (c *Client) Hover(ctx context.Context, params protocol.HoverParams) (protocol.Hover, error) {
|
||||
var result protocol.Hover
|
||||
err := c.Call(ctx, "textDocument/hover", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// SignatureHelp sends a textDocument/signatureHelp request to the LSP server.
|
||||
func (c *Client) SignatureHelp(ctx context.Context, params protocol.SignatureHelpParams) (protocol.SignatureHelp, error) {
|
||||
var result protocol.SignatureHelp
|
||||
err := c.Call(ctx, "textDocument/signatureHelp", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Definition sends a textDocument/definition request to the LSP server.
|
||||
// A request to resolve the definition location of a symbol at a given text document position. The request's parameter is of type TextDocumentPosition the response is of either type Definition or a typed array of DefinitionLink or a Thenable that resolves to such.
|
||||
func (c *Client) Definition(ctx context.Context, params protocol.DefinitionParams) (protocol.Or_Result_textDocument_definition, error) {
|
||||
var result protocol.Or_Result_textDocument_definition
|
||||
err := c.Call(ctx, "textDocument/definition", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// References sends a textDocument/references request to the LSP server.
|
||||
// A request to resolve project-wide references for the symbol denoted by the given text document position. The request's parameter is of type ReferenceParams the response is of type Location Location[] or a Thenable that resolves to such.
|
||||
func (c *Client) References(ctx context.Context, params protocol.ReferenceParams) ([]protocol.Location, error) {
|
||||
var result []protocol.Location
|
||||
err := c.Call(ctx, "textDocument/references", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DocumentHighlight sends a textDocument/documentHighlight request to the LSP server.
|
||||
// Request to resolve a DocumentHighlight for a given text document position. The request's parameter is of type TextDocumentPosition the request response is an array of type DocumentHighlight or a Thenable that resolves to such.
|
||||
func (c *Client) DocumentHighlight(ctx context.Context, params protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) {
|
||||
var result []protocol.DocumentHighlight
|
||||
err := c.Call(ctx, "textDocument/documentHighlight", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DocumentSymbol sends a textDocument/documentSymbol request to the LSP server.
|
||||
// A request to list all symbols found in a given text document. The request's parameter is of type TextDocumentIdentifier the response is of type SymbolInformation SymbolInformation[] or a Thenable that resolves to such.
|
||||
func (c *Client) DocumentSymbol(ctx context.Context, params protocol.DocumentSymbolParams) (protocol.Or_Result_textDocument_documentSymbol, error) {
|
||||
var result protocol.Or_Result_textDocument_documentSymbol
|
||||
err := c.Call(ctx, "textDocument/documentSymbol", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// CodeAction sends a textDocument/codeAction request to the LSP server.
|
||||
// A request to provide commands for the given text document and range.
|
||||
func (c *Client) CodeAction(ctx context.Context, params protocol.CodeActionParams) ([]protocol.Or_Result_textDocument_codeAction_Item0_Elem, error) {
|
||||
var result []protocol.Or_Result_textDocument_codeAction_Item0_Elem
|
||||
err := c.Call(ctx, "textDocument/codeAction", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ResolveCodeAction sends a codeAction/resolve request to the LSP server.
|
||||
// Request to resolve additional information for a given code action.The request's parameter is of type CodeAction the response is of type CodeAction or a Thenable that resolves to such.
|
||||
func (c *Client) ResolveCodeAction(ctx context.Context, params protocol.CodeAction) (protocol.CodeAction, error) {
|
||||
var result protocol.CodeAction
|
||||
err := c.Call(ctx, "codeAction/resolve", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Symbol sends a workspace/symbol request to the LSP server.
|
||||
// A request to list project-wide symbols matching the query string given by the WorkspaceSymbolParams. The response is of type SymbolInformation SymbolInformation[] or a Thenable that resolves to such. Since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients need to advertise support for WorkspaceSymbols via the client capability workspace.symbol.resolveSupport.
|
||||
func (c *Client) Symbol(ctx context.Context, params protocol.WorkspaceSymbolParams) (protocol.Or_Result_workspace_symbol, error) {
|
||||
var result protocol.Or_Result_workspace_symbol
|
||||
err := c.Call(ctx, "workspace/symbol", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ResolveWorkspaceSymbol sends a workspaceSymbol/resolve request to the LSP server.
|
||||
// A request to resolve the range inside the workspace symbol's location. Since 3.17.0
|
||||
func (c *Client) ResolveWorkspaceSymbol(ctx context.Context, params protocol.WorkspaceSymbol) (protocol.WorkspaceSymbol, error) {
|
||||
var result protocol.WorkspaceSymbol
|
||||
err := c.Call(ctx, "workspaceSymbol/resolve", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// CodeLens sends a textDocument/codeLens request to the LSP server.
|
||||
// A request to provide code lens for the given text document.
|
||||
func (c *Client) CodeLens(ctx context.Context, params protocol.CodeLensParams) ([]protocol.CodeLens, error) {
|
||||
var result []protocol.CodeLens
|
||||
err := c.Call(ctx, "textDocument/codeLens", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ResolveCodeLens sends a codeLens/resolve request to the LSP server.
|
||||
// A request to resolve a command for a given code lens.
|
||||
func (c *Client) ResolveCodeLens(ctx context.Context, params protocol.CodeLens) (protocol.CodeLens, error) {
|
||||
var result protocol.CodeLens
|
||||
err := c.Call(ctx, "codeLens/resolve", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DocumentLink sends a textDocument/documentLink request to the LSP server.
|
||||
// A request to provide document links
|
||||
func (c *Client) DocumentLink(ctx context.Context, params protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
|
||||
var result []protocol.DocumentLink
|
||||
err := c.Call(ctx, "textDocument/documentLink", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ResolveDocumentLink sends a documentLink/resolve request to the LSP server.
|
||||
// Request to resolve additional information for a given document link. The request's parameter is of type DocumentLink the response is of type DocumentLink or a Thenable that resolves to such.
|
||||
func (c *Client) ResolveDocumentLink(ctx context.Context, params protocol.DocumentLink) (protocol.DocumentLink, error) {
|
||||
var result protocol.DocumentLink
|
||||
err := c.Call(ctx, "documentLink/resolve", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Formatting sends a textDocument/formatting request to the LSP server.
|
||||
// A request to format a whole document.
|
||||
func (c *Client) Formatting(ctx context.Context, params protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
|
||||
var result []protocol.TextEdit
|
||||
err := c.Call(ctx, "textDocument/formatting", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// RangeFormatting sends a textDocument/rangeFormatting request to the LSP server.
|
||||
// A request to format a range in a document.
|
||||
func (c *Client) RangeFormatting(ctx context.Context, params protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
|
||||
var result []protocol.TextEdit
|
||||
err := c.Call(ctx, "textDocument/rangeFormatting", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// RangesFormatting sends a textDocument/rangesFormatting request to the LSP server.
|
||||
// A request to format ranges in a document. Since 3.18.0 PROPOSED
|
||||
func (c *Client) RangesFormatting(ctx context.Context, params protocol.DocumentRangesFormattingParams) ([]protocol.TextEdit, error) {
|
||||
var result []protocol.TextEdit
|
||||
err := c.Call(ctx, "textDocument/rangesFormatting", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// OnTypeFormatting sends a textDocument/onTypeFormatting request to the LSP server.
|
||||
// A request to format a document on type.
|
||||
func (c *Client) OnTypeFormatting(ctx context.Context, params protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {
|
||||
var result []protocol.TextEdit
|
||||
err := c.Call(ctx, "textDocument/onTypeFormatting", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Rename sends a textDocument/rename request to the LSP server.
|
||||
// A request to rename a symbol.
|
||||
func (c *Client) Rename(ctx context.Context, params protocol.RenameParams) (protocol.WorkspaceEdit, error) {
|
||||
var result protocol.WorkspaceEdit
|
||||
err := c.Call(ctx, "textDocument/rename", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// PrepareRename sends a textDocument/prepareRename request to the LSP server.
|
||||
// A request to test and perform the setup necessary for a rename. Since 3.16 - support for default behavior
|
||||
func (c *Client) PrepareRename(ctx context.Context, params protocol.PrepareRenameParams) (protocol.PrepareRenameResult, error) {
|
||||
var result protocol.PrepareRenameResult
|
||||
err := c.Call(ctx, "textDocument/prepareRename", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// ExecuteCommand sends a workspace/executeCommand request to the LSP server.
|
||||
// A request send from the client to the server to execute a command. The request might return a workspace edit which the client will apply to the workspace.
|
||||
func (c *Client) ExecuteCommand(ctx context.Context, params protocol.ExecuteCommandParams) (any, error) {
|
||||
var result any
|
||||
err := c.Call(ctx, "workspace/executeCommand", params, &result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// DidChangeWorkspaceFolders sends a workspace/didChangeWorkspaceFolders notification to the LSP server.
|
||||
// The workspace/didChangeWorkspaceFolders notification is sent from the client to the server when the workspace folder configuration changes.
|
||||
func (c *Client) DidChangeWorkspaceFolders(ctx context.Context, params protocol.DidChangeWorkspaceFoldersParams) error {
|
||||
return c.Notify(ctx, "workspace/didChangeWorkspaceFolders", params)
|
||||
}
|
||||
|
||||
// WorkDoneProgressCancel sends a window/workDoneProgress/cancel notification to the LSP server.
|
||||
// The window/workDoneProgress/cancel notification is sent from the client to the server to cancel a progress initiated on the server side.
|
||||
func (c *Client) WorkDoneProgressCancel(ctx context.Context, params protocol.WorkDoneProgressCancelParams) error {
|
||||
return c.Notify(ctx, "window/workDoneProgress/cancel", params)
|
||||
}
|
||||
|
||||
// DidCreateFiles sends a workspace/didCreateFiles notification to the LSP server.
|
||||
// The did create files notification is sent from the client to the server when files were created from within the client. Since 3.16.0
|
||||
func (c *Client) DidCreateFiles(ctx context.Context, params protocol.CreateFilesParams) error {
|
||||
return c.Notify(ctx, "workspace/didCreateFiles", params)
|
||||
}
|
||||
|
||||
// DidRenameFiles sends a workspace/didRenameFiles notification to the LSP server.
|
||||
// The did rename files notification is sent from the client to the server when files were renamed from within the client. Since 3.16.0
|
||||
func (c *Client) DidRenameFiles(ctx context.Context, params protocol.RenameFilesParams) error {
|
||||
return c.Notify(ctx, "workspace/didRenameFiles", params)
|
||||
}
|
||||
|
||||
// DidDeleteFiles sends a workspace/didDeleteFiles notification to the LSP server.
|
||||
// The will delete files request is sent from the client to the server before files are actually deleted as long as the deletion is triggered from within the client. Since 3.16.0
|
||||
func (c *Client) DidDeleteFiles(ctx context.Context, params protocol.DeleteFilesParams) error {
|
||||
return c.Notify(ctx, "workspace/didDeleteFiles", params)
|
||||
}
|
||||
|
||||
// DidOpenNotebookDocument sends a notebookDocument/didOpen notification to the LSP server.
|
||||
// A notification sent when a notebook opens. Since 3.17.0
|
||||
func (c *Client) DidOpenNotebookDocument(ctx context.Context, params protocol.DidOpenNotebookDocumentParams) error {
|
||||
return c.Notify(ctx, "notebookDocument/didOpen", params)
|
||||
}
|
||||
|
||||
// DidChangeNotebookDocument sends a notebookDocument/didChange notification to the LSP server.
|
||||
func (c *Client) DidChangeNotebookDocument(ctx context.Context, params protocol.DidChangeNotebookDocumentParams) error {
|
||||
return c.Notify(ctx, "notebookDocument/didChange", params)
|
||||
}
|
||||
|
||||
// DidSaveNotebookDocument sends a notebookDocument/didSave notification to the LSP server.
|
||||
// A notification sent when a notebook document is saved. Since 3.17.0
|
||||
func (c *Client) DidSaveNotebookDocument(ctx context.Context, params protocol.DidSaveNotebookDocumentParams) error {
|
||||
return c.Notify(ctx, "notebookDocument/didSave", params)
|
||||
}
|
||||
|
||||
// DidCloseNotebookDocument sends a notebookDocument/didClose notification to the LSP server.
|
||||
// A notification sent when a notebook closes. Since 3.17.0
|
||||
func (c *Client) DidCloseNotebookDocument(ctx context.Context, params protocol.DidCloseNotebookDocumentParams) error {
|
||||
return c.Notify(ctx, "notebookDocument/didClose", params)
|
||||
}
|
||||
|
||||
// Initialized sends a initialized notification to the LSP server.
|
||||
// The initialized notification is sent from the client to the server after the client is fully initialized and the server is allowed to send requests from the server to the client.
|
||||
func (c *Client) Initialized(ctx context.Context, params protocol.InitializedParams) error {
|
||||
return c.Notify(ctx, "initialized", params)
|
||||
}
|
||||
|
||||
// Exit sends a exit notification to the LSP server.
|
||||
// The exit event is sent from the client to the server to ask the server to exit its process.
|
||||
func (c *Client) Exit(ctx context.Context) error {
|
||||
return c.Notify(ctx, "exit", nil)
|
||||
}
|
||||
|
||||
// DidChangeConfiguration sends a workspace/didChangeConfiguration notification to the LSP server.
|
||||
// The configuration change notification is sent from the client to the server when the client's configuration has changed. The notification contains the changed configuration as defined by the language client.
|
||||
func (c *Client) DidChangeConfiguration(ctx context.Context, params protocol.DidChangeConfigurationParams) error {
|
||||
return c.Notify(ctx, "workspace/didChangeConfiguration", params)
|
||||
}
|
||||
|
||||
// DidOpen sends a textDocument/didOpen notification to the LSP server.
|
||||
// The document open notification is sent from the client to the server to signal newly opened text documents. The document's truth is now managed by the client and the server must not try to read the document's truth using the document's uri. Open in this sense means it is managed by the client. It doesn't necessarily mean that its content is presented in an editor. An open notification must not be sent more than once without a corresponding close notification send before. This means open and close notification must be balanced and the max open count is one.
|
||||
func (c *Client) DidOpen(ctx context.Context, params protocol.DidOpenTextDocumentParams) error {
|
||||
return c.Notify(ctx, "textDocument/didOpen", params)
|
||||
}
|
||||
|
||||
// DidChange sends a textDocument/didChange notification to the LSP server.
|
||||
// The document change notification is sent from the client to the server to signal changes to a text document.
|
||||
func (c *Client) DidChange(ctx context.Context, params protocol.DidChangeTextDocumentParams) error {
|
||||
return c.Notify(ctx, "textDocument/didChange", params)
|
||||
}
|
||||
|
||||
// DidClose sends a textDocument/didClose notification to the LSP server.
|
||||
// The document close notification is sent from the client to the server when the document got closed in the client. The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the truth now exists on disk). As with the open notification the close notification is about managing the document's content. Receiving a close notification doesn't mean that the document was open in an editor before. A close notification requires a previous open notification to be sent.
|
||||
func (c *Client) DidClose(ctx context.Context, params protocol.DidCloseTextDocumentParams) error {
|
||||
return c.Notify(ctx, "textDocument/didClose", params)
|
||||
}
|
||||
|
||||
// DidSave sends a textDocument/didSave notification to the LSP server.
|
||||
// The document save notification is sent from the client to the server when the document got saved in the client.
|
||||
func (c *Client) DidSave(ctx context.Context, params protocol.DidSaveTextDocumentParams) error {
|
||||
return c.Notify(ctx, "textDocument/didSave", params)
|
||||
}
|
||||
|
||||
// WillSave sends a textDocument/willSave notification to the LSP server.
|
||||
// A document will save notification is sent from the client to the server before the document is actually saved.
|
||||
func (c *Client) WillSave(ctx context.Context, params protocol.WillSaveTextDocumentParams) error {
|
||||
return c.Notify(ctx, "textDocument/willSave", params)
|
||||
}
|
||||
|
||||
// DidChangeWatchedFiles sends a workspace/didChangeWatchedFiles notification to the LSP server.
|
||||
// The watched files notification is sent from the client to the server when the client detects changes to file watched by the language client.
|
||||
func (c *Client) DidChangeWatchedFiles(ctx context.Context, params protocol.DidChangeWatchedFilesParams) error {
|
||||
return c.Notify(ctx, "workspace/didChangeWatchedFiles", params)
|
||||
}
|
||||
|
||||
// SetTrace sends a $/setTrace notification to the LSP server.
|
||||
func (c *Client) SetTrace(ctx context.Context, params protocol.SetTraceParams) error {
|
||||
return c.Notify(ctx, "$/setTrace", params)
|
||||
}
|
||||
|
||||
// Progress sends a $/progress notification to the LSP server.
|
||||
func (c *Client) Progress(ctx context.Context, params protocol.ProgressParams) error {
|
||||
return c.Notify(ctx, "$/progress", params)
|
||||
}
|
48
internal/lsp/protocol.go
Normal file
48
internal/lsp/protocol.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package lsp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Message represents a JSON-RPC 2.0 message
|
||||
type Message struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID int32 `json:"id,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *ResponseError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// ResponseError represents a JSON-RPC 2.0 error
|
||||
type ResponseError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewRequest(id int32, method string, params any) (*Message, error) {
|
||||
paramsJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Message{
|
||||
JSONRPC: "2.0",
|
||||
ID: id,
|
||||
Method: method,
|
||||
Params: paramsJSON,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewNotification(method string, params any) (*Message, error) {
|
||||
paramsJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Message{
|
||||
JSONRPC: "2.0",
|
||||
Method: method,
|
||||
Params: paramsJSON,
|
||||
}, nil
|
||||
}
|
27
internal/lsp/protocol/LICENSE
Normal file
27
internal/lsp/protocol/LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
|||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
117
internal/lsp/protocol/interface.go
Normal file
117
internal/lsp/protocol/interface.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package protocol
|
||||
|
||||
import "fmt"
|
||||
|
||||
// TextEditResult is an interface for types that represent workspace symbols
|
||||
type WorkspaceSymbolResult interface {
|
||||
GetName() string
|
||||
GetLocation() Location
|
||||
isWorkspaceSymbol() // marker method
|
||||
}
|
||||
|
||||
func (ws *WorkspaceSymbol) GetName() string { return ws.Name }
|
||||
func (ws *WorkspaceSymbol) GetLocation() Location {
|
||||
switch v := ws.Location.Value.(type) {
|
||||
case Location:
|
||||
return v
|
||||
case LocationUriOnly:
|
||||
return Location{URI: v.URI}
|
||||
}
|
||||
return Location{}
|
||||
}
|
||||
func (ws *WorkspaceSymbol) isWorkspaceSymbol() {}
|
||||
|
||||
func (si *SymbolInformation) GetName() string { return si.Name }
|
||||
func (si *SymbolInformation) GetLocation() Location { return si.Location }
|
||||
func (si *SymbolInformation) isWorkspaceSymbol() {}
|
||||
|
||||
// Results converts the Value to a slice of WorkspaceSymbolResult
|
||||
func (r Or_Result_workspace_symbol) Results() ([]WorkspaceSymbolResult, error) {
|
||||
if r.Value == nil {
|
||||
return make([]WorkspaceSymbolResult, 0), nil
|
||||
}
|
||||
switch v := r.Value.(type) {
|
||||
case []WorkspaceSymbol:
|
||||
results := make([]WorkspaceSymbolResult, len(v))
|
||||
for i := range v {
|
||||
results[i] = &v[i]
|
||||
}
|
||||
return results, nil
|
||||
case []SymbolInformation:
|
||||
results := make([]WorkspaceSymbolResult, len(v))
|
||||
for i := range v {
|
||||
results[i] = &v[i]
|
||||
}
|
||||
return results, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown symbol type: %T", r.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// TextEditResult is an interface for types that represent document symbols
|
||||
type DocumentSymbolResult interface {
|
||||
GetRange() Range
|
||||
GetName() string
|
||||
isDocumentSymbol() // marker method
|
||||
}
|
||||
|
||||
func (ds *DocumentSymbol) GetRange() Range { return ds.Range }
|
||||
func (ds *DocumentSymbol) GetName() string { return ds.Name }
|
||||
func (ds *DocumentSymbol) isDocumentSymbol() {}
|
||||
|
||||
func (si *SymbolInformation) GetRange() Range { return si.Location.Range }
|
||||
|
||||
// Note: SymbolInformation already has GetName() implemented above
|
||||
func (si *SymbolInformation) isDocumentSymbol() {}
|
||||
|
||||
// Results converts the Value to a slice of DocumentSymbolResult
|
||||
func (r Or_Result_textDocument_documentSymbol) Results() ([]DocumentSymbolResult, error) {
|
||||
if r.Value == nil {
|
||||
return make([]DocumentSymbolResult, 0), nil
|
||||
}
|
||||
switch v := r.Value.(type) {
|
||||
case []DocumentSymbol:
|
||||
results := make([]DocumentSymbolResult, len(v))
|
||||
for i := range v {
|
||||
results[i] = &v[i]
|
||||
}
|
||||
return results, nil
|
||||
case []SymbolInformation:
|
||||
results := make([]DocumentSymbolResult, len(v))
|
||||
for i := range v {
|
||||
results[i] = &v[i]
|
||||
}
|
||||
return results, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown document symbol type: %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
// TextEditResult is an interface for types that can be used as text edits
|
||||
type TextEditResult interface {
|
||||
GetRange() Range
|
||||
GetNewText() string
|
||||
isTextEdit() // marker method
|
||||
}
|
||||
|
||||
func (te *TextEdit) GetRange() Range { return te.Range }
|
||||
func (te *TextEdit) GetNewText() string { return te.NewText }
|
||||
func (te *TextEdit) isTextEdit() {}
|
||||
|
||||
// Convert Or_TextDocumentEdit_edits_Elem to TextEdit
|
||||
func (e Or_TextDocumentEdit_edits_Elem) AsTextEdit() (TextEdit, error) {
|
||||
if e.Value == nil {
|
||||
return TextEdit{}, fmt.Errorf("nil text edit")
|
||||
}
|
||||
switch v := e.Value.(type) {
|
||||
case TextEdit:
|
||||
return v, nil
|
||||
case AnnotatedTextEdit:
|
||||
return TextEdit{
|
||||
Range: v.Range,
|
||||
NewText: v.NewText,
|
||||
}, nil
|
||||
default:
|
||||
return TextEdit{}, fmt.Errorf("unknown text edit type: %T", e.Value)
|
||||
}
|
||||
}
|
58
internal/lsp/protocol/pattern_interfaces.go
Normal file
58
internal/lsp/protocol/pattern_interfaces.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package protocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PatternInfo is an interface for types that represent glob patterns
|
||||
type PatternInfo interface {
|
||||
GetPattern() string
|
||||
GetBasePath() string
|
||||
isPattern() // marker method
|
||||
}
|
||||
|
||||
// StringPattern implements PatternInfo for string patterns
|
||||
type StringPattern struct {
|
||||
Pattern string
|
||||
}
|
||||
|
||||
func (p StringPattern) GetPattern() string { return p.Pattern }
|
||||
func (p StringPattern) GetBasePath() string { return "" }
|
||||
func (p StringPattern) isPattern() {}
|
||||
|
||||
// RelativePatternInfo implements PatternInfo for RelativePattern
|
||||
type RelativePatternInfo struct {
|
||||
RP RelativePattern
|
||||
BasePath string
|
||||
}
|
||||
|
||||
func (p RelativePatternInfo) GetPattern() string { return string(p.RP.Pattern) }
|
||||
func (p RelativePatternInfo) GetBasePath() string { return p.BasePath }
|
||||
func (p RelativePatternInfo) isPattern() {}
|
||||
|
||||
// AsPattern converts GlobPattern to a PatternInfo object
|
||||
func (g *GlobPattern) AsPattern() (PatternInfo, error) {
|
||||
if g.Value == nil {
|
||||
return nil, fmt.Errorf("nil pattern")
|
||||
}
|
||||
|
||||
switch v := g.Value.(type) {
|
||||
case string:
|
||||
return StringPattern{Pattern: v}, nil
|
||||
case RelativePattern:
|
||||
// Handle BaseURI which could be string or DocumentUri
|
||||
basePath := ""
|
||||
switch baseURI := v.BaseURI.Value.(type) {
|
||||
case string:
|
||||
basePath = strings.TrimPrefix(baseURI, "file://")
|
||||
case DocumentUri:
|
||||
basePath = strings.TrimPrefix(string(baseURI), "file://")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value)
|
||||
}
|
||||
return RelativePatternInfo{RP: v, BasePath: basePath}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown pattern type: %T", g.Value)
|
||||
}
|
||||
}
|
30
internal/lsp/protocol/tables.go
Normal file
30
internal/lsp/protocol/tables.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package protocol
|
||||
|
||||
var TableKindMap = map[SymbolKind]string{
|
||||
File: "File",
|
||||
Module: "Module",
|
||||
Namespace: "Namespace",
|
||||
Package: "Package",
|
||||
Class: "Class",
|
||||
Method: "Method",
|
||||
Property: "Property",
|
||||
Field: "Field",
|
||||
Constructor: "Constructor",
|
||||
Enum: "Enum",
|
||||
Interface: "Interface",
|
||||
Function: "Function",
|
||||
Variable: "Variable",
|
||||
Constant: "Constant",
|
||||
String: "String",
|
||||
Number: "Number",
|
||||
Boolean: "Boolean",
|
||||
Array: "Array",
|
||||
Object: "Object",
|
||||
Key: "Key",
|
||||
Null: "Null",
|
||||
EnumMember: "EnumMember",
|
||||
Struct: "Struct",
|
||||
Event: "Event",
|
||||
Operator: "Operator",
|
||||
TypeParameter: "TypeParameter",
|
||||
}
|
81
internal/lsp/protocol/tsdocument-changes.go
Normal file
81
internal/lsp/protocol/tsdocument-changes.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DocumentChange is a union of various file edit operations.
|
||||
//
|
||||
// Exactly one field of this struct is non-nil; see [DocumentChange.Valid].
|
||||
//
|
||||
// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#resourceChanges
|
||||
type DocumentChange struct {
|
||||
TextDocumentEdit *TextDocumentEdit
|
||||
CreateFile *CreateFile
|
||||
RenameFile *RenameFile
|
||||
DeleteFile *DeleteFile
|
||||
}
|
||||
|
||||
// Valid reports whether the DocumentChange sum-type value is valid,
|
||||
// that is, exactly one of create, delete, edit, or rename.
|
||||
func (ch DocumentChange) Valid() bool {
|
||||
n := 0
|
||||
if ch.TextDocumentEdit != nil {
|
||||
n++
|
||||
}
|
||||
if ch.CreateFile != nil {
|
||||
n++
|
||||
}
|
||||
if ch.RenameFile != nil {
|
||||
n++
|
||||
}
|
||||
if ch.DeleteFile != nil {
|
||||
n++
|
||||
}
|
||||
return n == 1
|
||||
}
|
||||
|
||||
func (d *DocumentChange) UnmarshalJSON(data []byte) error {
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := m["textDocument"]; ok {
|
||||
d.TextDocumentEdit = new(TextDocumentEdit)
|
||||
return json.Unmarshal(data, d.TextDocumentEdit)
|
||||
}
|
||||
|
||||
// The {Create,Rename,Delete}File types all share a 'kind' field.
|
||||
kind := m["kind"]
|
||||
switch kind {
|
||||
case "create":
|
||||
d.CreateFile = new(CreateFile)
|
||||
return json.Unmarshal(data, d.CreateFile)
|
||||
case "rename":
|
||||
d.RenameFile = new(RenameFile)
|
||||
return json.Unmarshal(data, d.RenameFile)
|
||||
case "delete":
|
||||
d.DeleteFile = new(DeleteFile)
|
||||
return json.Unmarshal(data, d.DeleteFile)
|
||||
}
|
||||
return fmt.Errorf("DocumentChanges: unexpected kind: %q", kind)
|
||||
}
|
||||
|
||||
func (d *DocumentChange) MarshalJSON() ([]byte, error) {
|
||||
if d.TextDocumentEdit != nil {
|
||||
return json.Marshal(d.TextDocumentEdit)
|
||||
} else if d.CreateFile != nil {
|
||||
return json.Marshal(d.CreateFile)
|
||||
} else if d.RenameFile != nil {
|
||||
return json.Marshal(d.RenameFile)
|
||||
} else if d.DeleteFile != nil {
|
||||
return json.Marshal(d.DeleteFile)
|
||||
}
|
||||
return nil, fmt.Errorf("empty DocumentChanges union value")
|
||||
}
|
3072
internal/lsp/protocol/tsjson.go
Normal file
3072
internal/lsp/protocol/tsjson.go
Normal file
File diff suppressed because it is too large
Load diff
6907
internal/lsp/protocol/tsprotocol.go
Normal file
6907
internal/lsp/protocol/tsprotocol.go
Normal file
File diff suppressed because it is too large
Load diff
218
internal/lsp/protocol/uri.go
Normal file
218
internal/lsp/protocol/uri.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
// This file declares URI, DocumentUri, and its methods.
|
||||
//
|
||||
// For the LSP definition of these types, see
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// A DocumentUri is the URI of a client editor document.
|
||||
//
|
||||
// According to the LSP specification:
|
||||
//
|
||||
// Care should be taken to handle encoding in URIs. For
|
||||
// example, some clients (such as VS Code) may encode colons
|
||||
// in drive letters while others do not. The URIs below are
|
||||
// both valid, but clients and servers should be consistent
|
||||
// with the form they use themselves to ensure the other party
|
||||
// doesn’t interpret them as distinct URIs. Clients and
|
||||
// servers should not assume that each other are encoding the
|
||||
// same way (for example a client encoding colons in drive
|
||||
// letters cannot assume server responses will have encoded
|
||||
// colons). The same applies to casing of drive letters - one
|
||||
// party should not assume the other party will return paths
|
||||
// with drive letters cased the same as it.
|
||||
//
|
||||
// file:///c:/project/readme.md
|
||||
// file:///C%3A/project/readme.md
|
||||
//
|
||||
// This is done during JSON unmarshalling;
|
||||
// see [DocumentUri.UnmarshalText] for details.
|
||||
type DocumentUri string
|
||||
|
||||
// A URI is an arbitrary URL (e.g. https), not necessarily a file.
|
||||
type URI = string
|
||||
|
||||
// UnmarshalText implements decoding of DocumentUri values.
|
||||
//
|
||||
// In particular, it implements a systematic correction of various odd
|
||||
// features of the definition of DocumentUri in the LSP spec that
|
||||
// appear to be workarounds for bugs in VS Code. For example, it may
|
||||
// URI-encode the URI itself, so that colon becomes %3A, and it may
|
||||
// send file://foo.go URIs that have two slashes (not three) and no
|
||||
// hostname.
|
||||
//
|
||||
// We use UnmarshalText, not UnmarshalJSON, because it is called even
|
||||
// for non-addressable values such as keys and values of map[K]V,
|
||||
// where there is no pointer of type *K or *V on which to call
|
||||
// UnmarshalJSON. (See Go issue #28189 for more detail.)
|
||||
//
|
||||
// Non-empty DocumentUris are valid "file"-scheme URIs.
|
||||
// The empty DocumentUri is valid.
|
||||
func (uri *DocumentUri) UnmarshalText(data []byte) (err error) {
|
||||
*uri, err = ParseDocumentUri(string(data))
|
||||
return
|
||||
}
|
||||
|
||||
// Path returns the file path for the given URI.
|
||||
//
|
||||
// DocumentUri("").Path() returns the empty string.
|
||||
//
|
||||
// Path panics if called on a URI that is not a valid filename.
|
||||
func (uri DocumentUri) Path() string {
|
||||
filename, err := filename(uri)
|
||||
if err != nil {
|
||||
// e.g. ParseRequestURI failed.
|
||||
//
|
||||
// This can only affect DocumentUris created by
|
||||
// direct string manipulation; all DocumentUris
|
||||
// received from the client pass through
|
||||
// ParseRequestURI, which ensures validity.
|
||||
panic(err)
|
||||
}
|
||||
return filepath.FromSlash(filename)
|
||||
}
|
||||
|
||||
// Dir returns the URI for the directory containing the receiver.
|
||||
func (uri DocumentUri) Dir() DocumentUri {
|
||||
// This function could be more efficiently implemented by avoiding any call
|
||||
// to Path(), but at least consolidates URI manipulation.
|
||||
return URIFromPath(uri.DirPath())
|
||||
}
|
||||
|
||||
// DirPath returns the file path to the directory containing this URI, which
|
||||
// must be a file URI.
|
||||
func (uri DocumentUri) DirPath() string {
|
||||
return filepath.Dir(uri.Path())
|
||||
}
|
||||
|
||||
func filename(uri DocumentUri) (string, error) {
|
||||
if uri == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// This conservative check for the common case
|
||||
// of a simple non-empty absolute POSIX filename
|
||||
// avoids the allocation of a net.URL.
|
||||
if strings.HasPrefix(string(uri), "file:///") {
|
||||
rest := string(uri)[len("file://"):] // leave one slash
|
||||
for i := range len(rest) {
|
||||
b := rest[i]
|
||||
// Reject these cases:
|
||||
if b < ' ' || b == 0x7f || // control character
|
||||
b == '%' || b == '+' || // URI escape
|
||||
b == ':' || // Windows drive letter
|
||||
b == '@' || b == '&' || b == '?' { // authority or query
|
||||
goto slow
|
||||
}
|
||||
}
|
||||
return rest, nil
|
||||
}
|
||||
slow:
|
||||
|
||||
u, err := url.ParseRequestURI(string(uri))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.Scheme != fileScheme {
|
||||
return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri)
|
||||
}
|
||||
// If the URI is a Windows URI, we trim the leading "/" and uppercase
|
||||
// the drive letter, which will never be case sensitive.
|
||||
if isWindowsDriveURIPath(u.Path) {
|
||||
u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:]
|
||||
}
|
||||
|
||||
return u.Path, nil
|
||||
}
|
||||
|
||||
// ParseDocumentUri interprets a string as a DocumentUri, applying VS
|
||||
// Code workarounds; see [DocumentUri.UnmarshalText] for details.
|
||||
func ParseDocumentUri(s string) (DocumentUri, error) {
|
||||
if s == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s, "file://") {
|
||||
return "", fmt.Errorf("DocumentUri scheme is not 'file': %s", s)
|
||||
}
|
||||
|
||||
// VS Code sends URLs with only two slashes,
|
||||
// which are invalid. golang/go#39789.
|
||||
if !strings.HasPrefix(s, "file:///") {
|
||||
s = "file:///" + s[len("file://"):]
|
||||
}
|
||||
|
||||
// Even though the input is a URI, it may not be in canonical form. VS Code
|
||||
// in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize.
|
||||
path, err := url.PathUnescape(s[len("file://"):])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// File URIs from Windows may have lowercase drive letters.
|
||||
// Since drive letters are guaranteed to be case insensitive,
|
||||
// we change them to uppercase to remain consistent.
|
||||
// For example, file:///c:/x/y/z becomes file:///C:/x/y/z.
|
||||
if isWindowsDriveURIPath(path) {
|
||||
path = path[:1] + strings.ToUpper(string(path[1])) + path[2:]
|
||||
}
|
||||
u := url.URL{Scheme: fileScheme, Path: path}
|
||||
return DocumentUri(u.String()), nil
|
||||
}
|
||||
|
||||
// URIFromPath returns DocumentUri for the supplied file path.
|
||||
// Given "", it returns "".
|
||||
func URIFromPath(path string) DocumentUri {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
if !isWindowsDrivePath(path) {
|
||||
if abs, err := filepath.Abs(path); err == nil {
|
||||
path = abs
|
||||
}
|
||||
}
|
||||
// Check the file path again, in case it became absolute.
|
||||
if isWindowsDrivePath(path) {
|
||||
path = "/" + strings.ToUpper(string(path[0])) + path[1:]
|
||||
}
|
||||
path = filepath.ToSlash(path)
|
||||
u := url.URL{
|
||||
Scheme: fileScheme,
|
||||
Path: path,
|
||||
}
|
||||
return DocumentUri(u.String())
|
||||
}
|
||||
|
||||
const fileScheme = "file"
|
||||
|
||||
// isWindowsDrivePath returns true if the file path is of the form used by
|
||||
// Windows. We check if the path begins with a drive letter, followed by a ":".
|
||||
// For example: C:/x/y/z.
|
||||
func isWindowsDrivePath(path string) bool {
|
||||
if len(path) < 3 {
|
||||
return false
|
||||
}
|
||||
return unicode.IsLetter(rune(path[0])) && path[1] == ':'
|
||||
}
|
||||
|
||||
// isWindowsDriveURIPath returns true if the file URI is of the format used by
|
||||
// Windows URIs. The url.Parse package does not specially handle Windows paths
|
||||
// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
|
||||
func isWindowsDriveURIPath(uri string) bool {
|
||||
if len(uri) < 4 {
|
||||
return false
|
||||
}
|
||||
return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
|
||||
}
|
269
internal/lsp/transport.go
Normal file
269
internal/lsp/transport.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
package lsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var debug = os.Getenv("DEBUG") != ""
|
||||
|
||||
// Write writes an LSP message to the given writer
|
||||
func WriteMessage(w io.Writer, msg *Message) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal message: %w", err)
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("%v", msg.Method)
|
||||
log.Printf("-> Sending: %s", string(data))
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write header: %w", err)
|
||||
}
|
||||
|
||||
_, err = w.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write message: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadMessage reads a single LSP message from the given reader
|
||||
func ReadMessage(r *bufio.Reader) (*Message, error) {
|
||||
// Read headers
|
||||
var contentLength int
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if debug {
|
||||
log.Printf("<- Header: %s", line)
|
||||
}
|
||||
|
||||
if line == "" {
|
||||
break // End of headers
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "Content-Length: ") {
|
||||
_, err := fmt.Sscanf(line, "Content-Length: %d", &contentLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid Content-Length: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("<- Reading content with length: %d", contentLength)
|
||||
}
|
||||
|
||||
// Read content
|
||||
content := make([]byte, contentLength)
|
||||
_, err := io.ReadFull(r, content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read content: %w", err)
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("<- Received: %s", string(content))
|
||||
}
|
||||
|
||||
// Parse message
|
||||
var msg Message
|
||||
if err := json.Unmarshal(content, &msg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal message: %w", err)
|
||||
}
|
||||
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// handleMessages reads and dispatches messages in a loop
|
||||
func (c *Client) handleMessages() {
|
||||
for {
|
||||
msg, err := ReadMessage(c.stdout)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Printf("Error reading message: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle server->client request (has both Method and ID)
|
||||
if msg.Method != "" && msg.ID != 0 {
|
||||
if debug {
|
||||
log.Printf("Received request from server: method=%s id=%d", msg.Method, msg.ID)
|
||||
}
|
||||
|
||||
response := &Message{
|
||||
JSONRPC: "2.0",
|
||||
ID: msg.ID,
|
||||
}
|
||||
|
||||
// Look up handler for this method
|
||||
c.serverHandlersMu.RLock()
|
||||
handler, ok := c.serverRequestHandlers[msg.Method]
|
||||
c.serverHandlersMu.RUnlock()
|
||||
|
||||
if ok {
|
||||
result, err := handler(msg.Params)
|
||||
if err != nil {
|
||||
response.Error = &ResponseError{
|
||||
Code: -32603,
|
||||
Message: err.Error(),
|
||||
}
|
||||
} else {
|
||||
rawJSON, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
response.Error = &ResponseError{
|
||||
Code: -32603,
|
||||
Message: fmt.Sprintf("failed to marshal response: %v", err),
|
||||
}
|
||||
} else {
|
||||
response.Result = rawJSON
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.Error = &ResponseError{
|
||||
Code: -32601,
|
||||
Message: fmt.Sprintf("method not found: %s", msg.Method),
|
||||
}
|
||||
}
|
||||
|
||||
// Send response back to server
|
||||
if err := WriteMessage(c.stdin, response); err != nil {
|
||||
log.Printf("Error sending response to server: %v", err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle notification (has Method but no ID)
|
||||
if msg.Method != "" && msg.ID == 0 {
|
||||
c.notificationMu.RLock()
|
||||
handler, ok := c.notificationHandlers[msg.Method]
|
||||
c.notificationMu.RUnlock()
|
||||
|
||||
if ok {
|
||||
if debug {
|
||||
log.Printf("Handling notification: %s", msg.Method)
|
||||
}
|
||||
go handler(msg.Params)
|
||||
} else if debug {
|
||||
log.Printf("No handler for notification: %s", msg.Method)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle response to our request (has ID but no Method)
|
||||
if msg.ID != 0 && msg.Method == "" {
|
||||
c.handlersMu.RLock()
|
||||
ch, ok := c.handlers[msg.ID]
|
||||
c.handlersMu.RUnlock()
|
||||
|
||||
if ok {
|
||||
if debug {
|
||||
log.Printf("Sending response for ID %d to handler", msg.ID)
|
||||
}
|
||||
ch <- msg
|
||||
close(ch)
|
||||
} else if debug {
|
||||
log.Printf("No handler for response ID: %d", msg.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call makes a request and waits for the response
|
||||
func (c *Client) Call(ctx context.Context, method string, params any, result any) error {
|
||||
id := c.nextID.Add(1)
|
||||
|
||||
if debug {
|
||||
log.Printf("Making call: method=%s id=%d", method, id)
|
||||
}
|
||||
|
||||
msg, err := NewRequest(id, method, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Create response channel
|
||||
ch := make(chan *Message, 1)
|
||||
c.handlersMu.Lock()
|
||||
c.handlers[id] = ch
|
||||
c.handlersMu.Unlock()
|
||||
|
||||
defer func() {
|
||||
c.handlersMu.Lock()
|
||||
delete(c.handlers, id)
|
||||
c.handlersMu.Unlock()
|
||||
}()
|
||||
|
||||
// Send request
|
||||
if err := WriteMessage(c.stdin, msg); err != nil {
|
||||
return fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("Waiting for response to request ID: %d", id)
|
||||
}
|
||||
|
||||
// Wait for response
|
||||
resp := <-ch
|
||||
|
||||
if debug {
|
||||
log.Printf("Received response for request ID: %d", id)
|
||||
}
|
||||
|
||||
if resp.Error != nil {
|
||||
return fmt.Errorf("request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
// If result is a json.RawMessage, just copy the raw bytes
|
||||
if rawMsg, ok := result.(*json.RawMessage); ok {
|
||||
*rawMsg = resp.Result
|
||||
return nil
|
||||
}
|
||||
// Otherwise unmarshal into the provided type
|
||||
if err := json.Unmarshal(resp.Result, result); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal result: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Notify sends a notification (a request without an ID that doesn't expect a response)
|
||||
func (c *Client) Notify(ctx context.Context, method string, params any) error {
|
||||
if debug {
|
||||
log.Printf("Sending notification: method=%s", method)
|
||||
}
|
||||
|
||||
msg, err := NewNotification(method, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create notification: %w", err)
|
||||
}
|
||||
|
||||
if err := WriteMessage(c.stdin, msg); err != nil {
|
||||
return fmt.Errorf("failed to send notification: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type (
|
||||
NotificationHandler func(params json.RawMessage)
|
||||
ServerRequestHandler func(params json.RawMessage) (any, error)
|
||||
)
|
239
internal/lsp/util/edit.go
Normal file
239
internal/lsp/util/edit.go
Normal file
|
@ -0,0 +1,239 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error {
|
||||
path := strings.TrimPrefix(string(uri), "file://")
|
||||
|
||||
// Read the file content
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
// Detect line ending style
|
||||
var lineEnding string
|
||||
if bytes.Contains(content, []byte("\r\n")) {
|
||||
lineEnding = "\r\n"
|
||||
} else {
|
||||
lineEnding = "\n"
|
||||
}
|
||||
|
||||
// Track if file ends with a newline
|
||||
endsWithNewline := len(content) > 0 && bytes.HasSuffix(content, []byte(lineEnding))
|
||||
|
||||
// Split into lines without the endings
|
||||
lines := strings.Split(string(content), lineEnding)
|
||||
|
||||
// Check for overlapping edits
|
||||
for i := 0; i < len(edits); i++ {
|
||||
for j := i + 1; j < len(edits); j++ {
|
||||
if rangesOverlap(edits[i].Range, edits[j].Range) {
|
||||
return fmt.Errorf("overlapping edits detected between edit %d and %d", i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort edits in reverse order
|
||||
sortedEdits := make([]protocol.TextEdit, len(edits))
|
||||
copy(sortedEdits, edits)
|
||||
sort.Slice(sortedEdits, func(i, j int) bool {
|
||||
if sortedEdits[i].Range.Start.Line != sortedEdits[j].Range.Start.Line {
|
||||
return sortedEdits[i].Range.Start.Line > sortedEdits[j].Range.Start.Line
|
||||
}
|
||||
return sortedEdits[i].Range.Start.Character > sortedEdits[j].Range.Start.Character
|
||||
})
|
||||
|
||||
// Apply each edit
|
||||
for _, edit := range sortedEdits {
|
||||
newLines, err := applyTextEdit(lines, edit, lineEnding)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to apply edit: %w", err)
|
||||
}
|
||||
lines = newLines
|
||||
}
|
||||
|
||||
// Join lines with proper line endings
|
||||
var newContent strings.Builder
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
newContent.WriteString(lineEnding)
|
||||
}
|
||||
newContent.WriteString(line)
|
||||
}
|
||||
|
||||
// Only add a newline if the original file had one and we haven't already added it
|
||||
if endsWithNewline && !strings.HasSuffix(newContent.String(), lineEnding) {
|
||||
newContent.WriteString(lineEnding)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, []byte(newContent.String()), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyTextEdit(lines []string, edit protocol.TextEdit, lineEnding string) ([]string, error) {
|
||||
startLine := int(edit.Range.Start.Line)
|
||||
endLine := int(edit.Range.End.Line)
|
||||
startChar := int(edit.Range.Start.Character)
|
||||
endChar := int(edit.Range.End.Character)
|
||||
|
||||
// Validate positions
|
||||
if startLine < 0 || startLine >= len(lines) {
|
||||
return nil, fmt.Errorf("invalid start line: %d", startLine)
|
||||
}
|
||||
if endLine < 0 || endLine >= len(lines) {
|
||||
endLine = len(lines) - 1
|
||||
}
|
||||
|
||||
// Create result slice with initial capacity
|
||||
result := make([]string, 0, len(lines))
|
||||
|
||||
// Copy lines before edit
|
||||
result = append(result, lines[:startLine]...)
|
||||
|
||||
// Get the prefix of the start line
|
||||
startLineContent := lines[startLine]
|
||||
if startChar < 0 || startChar > len(startLineContent) {
|
||||
startChar = len(startLineContent)
|
||||
}
|
||||
prefix := startLineContent[:startChar]
|
||||
|
||||
// Get the suffix of the end line
|
||||
endLineContent := lines[endLine]
|
||||
if endChar < 0 || endChar > len(endLineContent) {
|
||||
endChar = len(endLineContent)
|
||||
}
|
||||
suffix := endLineContent[endChar:]
|
||||
|
||||
// Handle the edit
|
||||
if edit.NewText == "" {
|
||||
if prefix+suffix != "" {
|
||||
result = append(result, prefix+suffix)
|
||||
}
|
||||
} else {
|
||||
// Split new text into lines, being careful not to add extra newlines
|
||||
// newLines := strings.Split(strings.TrimRight(edit.NewText, "\n"), "\n")
|
||||
newLines := strings.Split(edit.NewText, "\n")
|
||||
|
||||
if len(newLines) == 1 {
|
||||
// Single line change
|
||||
result = append(result, prefix+newLines[0]+suffix)
|
||||
} else {
|
||||
// Multi-line change
|
||||
result = append(result, prefix+newLines[0])
|
||||
result = append(result, newLines[1:len(newLines)-1]...)
|
||||
result = append(result, newLines[len(newLines)-1]+suffix)
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining lines
|
||||
if endLine+1 < len(lines) {
|
||||
result = append(result, lines[endLine+1:]...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// applyDocumentChange applies a DocumentChange (create/rename/delete operations)
|
||||
func applyDocumentChange(change protocol.DocumentChange) error {
|
||||
if change.CreateFile != nil {
|
||||
path := strings.TrimPrefix(string(change.CreateFile.URI), "file://")
|
||||
if change.CreateFile.Options != nil {
|
||||
if change.CreateFile.Options.Overwrite {
|
||||
// Proceed with overwrite
|
||||
} else if change.CreateFile.Options.IgnoreIfExists {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil // File exists and we're ignoring it
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(""), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if change.DeleteFile != nil {
|
||||
path := strings.TrimPrefix(string(change.DeleteFile.URI), "file://")
|
||||
if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
return fmt.Errorf("failed to delete directory recursively: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return fmt.Errorf("failed to delete file: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if change.RenameFile != nil {
|
||||
oldPath := strings.TrimPrefix(string(change.RenameFile.OldURI), "file://")
|
||||
newPath := strings.TrimPrefix(string(change.RenameFile.NewURI), "file://")
|
||||
if change.RenameFile.Options != nil {
|
||||
if !change.RenameFile.Options.Overwrite {
|
||||
if _, err := os.Stat(newPath); err == nil {
|
||||
return fmt.Errorf("target file already exists and overwrite is not allowed: %s", newPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.Rename(oldPath, newPath); err != nil {
|
||||
return fmt.Errorf("failed to rename file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if change.TextDocumentEdit != nil {
|
||||
textEdits := make([]protocol.TextEdit, len(change.TextDocumentEdit.Edits))
|
||||
for i, edit := range change.TextDocumentEdit.Edits {
|
||||
var err error
|
||||
textEdits[i], err = edit.AsTextEdit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid edit type: %w", err)
|
||||
}
|
||||
}
|
||||
return applyTextEdits(change.TextDocumentEdit.TextDocument.URI, textEdits)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyWorkspaceEdit applies the given WorkspaceEdit to the filesystem
|
||||
func ApplyWorkspaceEdit(edit protocol.WorkspaceEdit) error {
|
||||
// Handle Changes field
|
||||
for uri, textEdits := range edit.Changes {
|
||||
if err := applyTextEdits(uri, textEdits); err != nil {
|
||||
return fmt.Errorf("failed to apply text edits: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DocumentChanges field
|
||||
for _, change := range edit.DocumentChanges {
|
||||
if err := applyDocumentChange(change); err != nil {
|
||||
return fmt.Errorf("failed to apply document change: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rangesOverlap(r1, r2 protocol.Range) bool {
|
||||
if r1.Start.Line > r2.End.Line || r2.Start.Line > r1.End.Line {
|
||||
return false
|
||||
}
|
||||
if r1.Start.Line == r2.End.Line && r1.Start.Character > r2.End.Character {
|
||||
return false
|
||||
}
|
||||
if r2.Start.Line == r1.End.Line && r2.Start.Character > r1.End.Character {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
633
internal/lsp/watcher/watcher.go
Normal file
633
internal/lsp/watcher/watcher.go
Normal file
|
@ -0,0 +1,633 @@
|
|||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
var debug = false // Force debug logging on
|
||||
|
||||
// WorkspaceWatcher manages LSP file watching
|
||||
type WorkspaceWatcher struct {
|
||||
client *lsp.Client
|
||||
workspacePath string
|
||||
|
||||
debounceTime time.Duration
|
||||
debounceMap map[string]*time.Timer
|
||||
debounceMu sync.Mutex
|
||||
|
||||
// File watchers registered by the server
|
||||
registrations []protocol.FileSystemWatcher
|
||||
registrationMu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewWorkspaceWatcher creates a new workspace watcher
|
||||
func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
|
||||
return &WorkspaceWatcher{
|
||||
client: client,
|
||||
debounceTime: 300 * time.Millisecond,
|
||||
debounceMap: make(map[string]*time.Timer),
|
||||
registrations: []protocol.FileSystemWatcher{},
|
||||
}
|
||||
}
|
||||
|
||||
// AddRegistrations adds file watchers to track
|
||||
func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
|
||||
w.registrationMu.Lock()
|
||||
defer w.registrationMu.Unlock()
|
||||
|
||||
// Add new watchers
|
||||
w.registrations = append(w.registrations, watchers...)
|
||||
|
||||
// Print detailed registration information for debugging
|
||||
if debug {
|
||||
log.Printf("Added %d file watcher registrations (id: %s), total: %d",
|
||||
len(watchers), id, len(w.registrations))
|
||||
|
||||
for i, watcher := range watchers {
|
||||
log.Printf("Registration #%d raw data:", i+1)
|
||||
|
||||
// Log the GlobPattern
|
||||
switch v := watcher.GlobPattern.Value.(type) {
|
||||
case string:
|
||||
log.Printf(" GlobPattern: string pattern '%s'", v)
|
||||
case protocol.RelativePattern:
|
||||
log.Printf(" GlobPattern: RelativePattern with pattern '%s'", v.Pattern)
|
||||
|
||||
// Log BaseURI details
|
||||
switch u := v.BaseURI.Value.(type) {
|
||||
case string:
|
||||
log.Printf(" BaseURI: string '%s'", u)
|
||||
case protocol.DocumentUri:
|
||||
log.Printf(" BaseURI: DocumentUri '%s'", u)
|
||||
default:
|
||||
log.Printf(" BaseURI: unknown type %T", u)
|
||||
}
|
||||
default:
|
||||
log.Printf(" GlobPattern: unknown type %T", v)
|
||||
}
|
||||
|
||||
// Log WatchKind
|
||||
watchKind := protocol.WatchKind(protocol.WatchChange | protocol.WatchCreate | protocol.WatchDelete)
|
||||
if watcher.Kind != nil {
|
||||
watchKind = *watcher.Kind
|
||||
}
|
||||
log.Printf(" WatchKind: %d (Create:%v, Change:%v, Delete:%v)",
|
||||
watchKind,
|
||||
watchKind&protocol.WatchCreate != 0,
|
||||
watchKind&protocol.WatchChange != 0,
|
||||
watchKind&protocol.WatchDelete != 0)
|
||||
|
||||
// Test match against some example paths
|
||||
testPaths := []string{
|
||||
"/Users/phil/dev/mcp-language-server/internal/watcher/watcher.go",
|
||||
"/Users/phil/dev/mcp-language-server/go.mod",
|
||||
}
|
||||
|
||||
for _, testPath := range testPaths {
|
||||
isMatch := w.matchesPattern(testPath, watcher.GlobPattern)
|
||||
log.Printf(" Test path '%s': %v", testPath, isMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find and open all existing files that match the newly registered patterns
|
||||
// TODO: not all language servers require this, but typescript does. Make this configurable
|
||||
go func() {
|
||||
startTime := time.Now()
|
||||
filesOpened := 0
|
||||
|
||||
err := filepath.WalkDir(w.workspacePath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip directories that should be excluded
|
||||
if d.IsDir() {
|
||||
log.Println(path)
|
||||
if path != w.workspacePath && shouldExcludeDir(path) {
|
||||
if debug {
|
||||
log.Printf("Skipping excluded directory!!: %s", path)
|
||||
}
|
||||
return filepath.SkipDir
|
||||
}
|
||||
} else {
|
||||
// Process files
|
||||
w.openMatchingFile(ctx, path)
|
||||
filesOpened++
|
||||
|
||||
// Add a small delay after every 100 files to prevent overwhelming the server
|
||||
if filesOpened%100 == 0 {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
elapsedTime := time.Since(startTime)
|
||||
if debug {
|
||||
log.Printf("Workspace scan complete: processed %d files in %.2f seconds", filesOpened, elapsedTime.Seconds())
|
||||
}
|
||||
|
||||
if err != nil && debug {
|
||||
log.Printf("Error scanning workspace for files to open: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// WatchWorkspace sets up file watching for a workspace
|
||||
func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath string) {
|
||||
w.workspacePath = workspacePath
|
||||
|
||||
// Register handler for file watcher registrations from the server
|
||||
lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
|
||||
w.AddRegistrations(ctx, id, watchers)
|
||||
})
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating watcher: %v", err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
// Watch the workspace recursively
|
||||
err = filepath.WalkDir(workspacePath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip excluded directories (except workspace root)
|
||||
if d.IsDir() && path != workspacePath {
|
||||
if shouldExcludeDir(path) {
|
||||
if debug {
|
||||
log.Printf("Skipping watching excluded directory: %s", path)
|
||||
}
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
|
||||
// Add directories to watcher
|
||||
if d.IsDir() {
|
||||
err = watcher.Add(path)
|
||||
if err != nil {
|
||||
log.Printf("Error watching path %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Error walking workspace: %v", err)
|
||||
}
|
||||
|
||||
// Event loop
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("file://%s", event.Name)
|
||||
|
||||
// Add new directories to the watcher
|
||||
if event.Op&fsnotify.Create != 0 {
|
||||
if info, err := os.Stat(event.Name); err == nil {
|
||||
if info.IsDir() {
|
||||
// Skip excluded directories
|
||||
if !shouldExcludeDir(event.Name) {
|
||||
if err := watcher.Add(event.Name); err != nil {
|
||||
log.Printf("Error watching new directory: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For newly created files
|
||||
if !shouldExcludeFile(event.Name) {
|
||||
w.openMatchingFile(ctx, event.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
if debug {
|
||||
matched, kind := w.isPathWatched(event.Name)
|
||||
log.Printf("Event: %s, Op: %s, Watched: %v, Kind: %d",
|
||||
event.Name, event.Op.String(), matched, kind)
|
||||
}
|
||||
|
||||
// Check if this path should be watched according to server registrations
|
||||
if watched, watchKind := w.isPathWatched(event.Name); watched {
|
||||
switch {
|
||||
case event.Op&fsnotify.Write != 0:
|
||||
if watchKind&protocol.WatchChange != 0 {
|
||||
w.debounceHandleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Changed))
|
||||
}
|
||||
case event.Op&fsnotify.Create != 0:
|
||||
// Already handled earlier in the event loop
|
||||
// Just send the notification if needed
|
||||
info, _ := os.Stat(event.Name)
|
||||
if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
|
||||
w.debounceHandleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Created))
|
||||
}
|
||||
case event.Op&fsnotify.Remove != 0:
|
||||
if watchKind&protocol.WatchDelete != 0 {
|
||||
w.handleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Deleted))
|
||||
}
|
||||
case event.Op&fsnotify.Rename != 0:
|
||||
// For renames, first delete
|
||||
if watchKind&protocol.WatchDelete != 0 {
|
||||
w.handleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Deleted))
|
||||
}
|
||||
|
||||
// Then check if the new file exists and create an event
|
||||
if info, err := os.Stat(event.Name); err == nil && !info.IsDir() {
|
||||
if watchKind&protocol.WatchCreate != 0 {
|
||||
w.debounceHandleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Created))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
log.Printf("Watcher error: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isPathWatched checks if a path should be watched based on server registrations
|
||||
func (w *WorkspaceWatcher) isPathWatched(path string) (bool, protocol.WatchKind) {
|
||||
w.registrationMu.RLock()
|
||||
defer w.registrationMu.RUnlock()
|
||||
|
||||
// If no explicit registrations, watch everything
|
||||
if len(w.registrations) == 0 {
|
||||
return true, protocol.WatchKind(protocol.WatchChange | protocol.WatchCreate | protocol.WatchDelete)
|
||||
}
|
||||
|
||||
// Check each registration
|
||||
for _, reg := range w.registrations {
|
||||
isMatch := w.matchesPattern(path, reg.GlobPattern)
|
||||
if isMatch {
|
||||
kind := protocol.WatchKind(protocol.WatchChange | protocol.WatchCreate | protocol.WatchDelete)
|
||||
if reg.Kind != nil {
|
||||
kind = *reg.Kind
|
||||
}
|
||||
return true, kind
|
||||
}
|
||||
}
|
||||
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// matchesGlob handles advanced glob patterns including ** and alternatives
|
||||
func matchesGlob(pattern, path string) bool {
|
||||
// Handle file extension patterns with braces like *.{go,mod,sum}
|
||||
if strings.Contains(pattern, "{") && strings.Contains(pattern, "}") {
|
||||
// Extract extensions from pattern like "*.{go,mod,sum}"
|
||||
parts := strings.SplitN(pattern, "{", 2)
|
||||
if len(parts) == 2 {
|
||||
prefix := parts[0]
|
||||
extPart := strings.SplitN(parts[1], "}", 2)
|
||||
if len(extPart) == 2 {
|
||||
extensions := strings.Split(extPart[0], ",")
|
||||
suffix := extPart[1]
|
||||
|
||||
// Check if the path matches any of the extensions
|
||||
for _, ext := range extensions {
|
||||
extPattern := prefix + ext + suffix
|
||||
isMatch := matchesSimpleGlob(extPattern, path)
|
||||
if isMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchesSimpleGlob(pattern, path)
|
||||
}
|
||||
|
||||
// matchesSimpleGlob handles glob patterns with ** wildcards
|
||||
func matchesSimpleGlob(pattern, path string) bool {
|
||||
// Handle special case for **/*.ext pattern (common in LSP)
|
||||
if strings.HasPrefix(pattern, "**/") {
|
||||
rest := strings.TrimPrefix(pattern, "**/")
|
||||
|
||||
// If the rest is a simple file extension pattern like *.go
|
||||
if strings.HasPrefix(rest, "*.") {
|
||||
ext := strings.TrimPrefix(rest, "*")
|
||||
isMatch := strings.HasSuffix(path, ext)
|
||||
return isMatch
|
||||
}
|
||||
|
||||
// Otherwise, try to check if the path ends with the rest part
|
||||
isMatch := strings.HasSuffix(path, rest)
|
||||
|
||||
// If it matches directly, great!
|
||||
if isMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, check if any path component matches
|
||||
pathComponents := strings.Split(path, "/")
|
||||
for i := 0; i < len(pathComponents); i++ {
|
||||
subPath := strings.Join(pathComponents[i:], "/")
|
||||
if strings.HasSuffix(subPath, rest) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle other ** wildcard pattern cases
|
||||
if strings.Contains(pattern, "**") {
|
||||
parts := strings.Split(pattern, "**")
|
||||
|
||||
// Validate the path starts with the first part
|
||||
if !strings.HasPrefix(path, parts[0]) && parts[0] != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// For patterns like "**/*.go", just check the suffix
|
||||
if len(parts) == 2 && parts[0] == "" {
|
||||
isMatch := strings.HasSuffix(path, parts[1])
|
||||
return isMatch
|
||||
}
|
||||
|
||||
// For other patterns, handle middle part
|
||||
remaining := strings.TrimPrefix(path, parts[0])
|
||||
if len(parts) == 2 {
|
||||
isMatch := strings.HasSuffix(remaining, parts[1])
|
||||
return isMatch
|
||||
}
|
||||
}
|
||||
|
||||
// Handle simple * wildcard for file extension patterns (*.go, *.sum, etc)
|
||||
if strings.HasPrefix(pattern, "*.") {
|
||||
ext := strings.TrimPrefix(pattern, "*")
|
||||
isMatch := strings.HasSuffix(path, ext)
|
||||
return isMatch
|
||||
}
|
||||
|
||||
// Fall back to simple matching for simpler patterns
|
||||
matched, err := filepath.Match(pattern, path)
|
||||
if err != nil {
|
||||
log.Printf("Error matching pattern %s: %v", pattern, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
// matchesPattern checks if a path matches the glob pattern
|
||||
func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
|
||||
patternInfo, err := pattern.AsPattern()
|
||||
if err != nil {
|
||||
log.Printf("Error parsing pattern: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
basePath := patternInfo.GetBasePath()
|
||||
patternText := patternInfo.GetPattern()
|
||||
|
||||
path = filepath.ToSlash(path)
|
||||
|
||||
// For simple patterns without base path
|
||||
if basePath == "" {
|
||||
// Check if the pattern matches the full path or just the file extension
|
||||
fullPathMatch := matchesGlob(patternText, path)
|
||||
baseNameMatch := matchesGlob(patternText, filepath.Base(path))
|
||||
|
||||
return fullPathMatch || baseNameMatch
|
||||
}
|
||||
|
||||
// For relative patterns
|
||||
basePath = strings.TrimPrefix(basePath, "file://")
|
||||
basePath = filepath.ToSlash(basePath)
|
||||
|
||||
// Make path relative to basePath for matching
|
||||
relPath, err := filepath.Rel(basePath, path)
|
||||
if err != nil {
|
||||
log.Printf("Error getting relative path for %s: %v", path, err)
|
||||
return false
|
||||
}
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
|
||||
isMatch := matchesGlob(patternText, relPath)
|
||||
|
||||
return isMatch
|
||||
}
|
||||
|
||||
// debounceHandleFileEvent handles file events with debouncing to reduce notifications
|
||||
func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
|
||||
w.debounceMu.Lock()
|
||||
defer w.debounceMu.Unlock()
|
||||
|
||||
// Create a unique key based on URI and change type
|
||||
key := fmt.Sprintf("%s:%d", uri, changeType)
|
||||
|
||||
// Cancel existing timer if any
|
||||
if timer, exists := w.debounceMap[key]; exists {
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
// Create new timer
|
||||
w.debounceMap[key] = time.AfterFunc(w.debounceTime, func() {
|
||||
w.handleFileEvent(ctx, uri, changeType)
|
||||
|
||||
// Cleanup timer after execution
|
||||
w.debounceMu.Lock()
|
||||
delete(w.debounceMap, key)
|
||||
w.debounceMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
// handleFileEvent sends file change notifications
|
||||
func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
|
||||
// If the file is open and it's a change event, use didChange notification
|
||||
filePath := uri[7:] // Remove "file://" prefix
|
||||
if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
|
||||
err := w.client.NotifyChange(ctx, filePath)
|
||||
if err != nil {
|
||||
log.Printf("Error notifying change: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Notify LSP server about the file event using didChangeWatchedFiles
|
||||
if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
|
||||
log.Printf("Error notifying LSP server about file event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// notifyFileEvent sends a didChangeWatchedFiles notification for a file event
|
||||
func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
|
||||
if debug {
|
||||
log.Printf("Notifying file event: %s (type: %d)", uri, changeType)
|
||||
}
|
||||
|
||||
params := protocol.DidChangeWatchedFilesParams{
|
||||
Changes: []protocol.FileEvent{
|
||||
{
|
||||
URI: protocol.DocumentUri(uri),
|
||||
Type: changeType,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return w.client.DidChangeWatchedFiles(ctx, params)
|
||||
}
|
||||
|
||||
// Common patterns for directories and files to exclude
|
||||
// TODO: make configurable
|
||||
var (
|
||||
excludedDirNames = map[string]bool{
|
||||
".git": true,
|
||||
"node_modules": true,
|
||||
"dist": true,
|
||||
"build": true,
|
||||
"out": true,
|
||||
"bin": true,
|
||||
".idea": true,
|
||||
".vscode": true,
|
||||
".cache": true,
|
||||
"coverage": true,
|
||||
"target": true, // Rust build output
|
||||
"vendor": true, // Go vendor directory
|
||||
}
|
||||
|
||||
excludedFileExtensions = map[string]bool{
|
||||
".swp": true,
|
||||
".swo": true,
|
||||
".tmp": true,
|
||||
".temp": true,
|
||||
".bak": true,
|
||||
".log": true,
|
||||
".o": true, // Object files
|
||||
".so": true, // Shared libraries
|
||||
".dylib": true, // macOS shared libraries
|
||||
".dll": true, // Windows shared libraries
|
||||
".a": true, // Static libraries
|
||||
".exe": true, // Windows executables
|
||||
".lock": true, // Lock files
|
||||
}
|
||||
|
||||
// Large binary files that shouldn't be opened
|
||||
largeBinaryExtensions = map[string]bool{
|
||||
".png": true,
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".gif": true,
|
||||
".bmp": true,
|
||||
".ico": true,
|
||||
".zip": true,
|
||||
".tar": true,
|
||||
".gz": true,
|
||||
".rar": true,
|
||||
".7z": true,
|
||||
".pdf": true,
|
||||
".mp3": true,
|
||||
".mp4": true,
|
||||
".mov": true,
|
||||
".wav": true,
|
||||
".wasm": true,
|
||||
}
|
||||
|
||||
// Maximum file size to open (5MB)
|
||||
maxFileSize int64 = 5 * 1024 * 1024
|
||||
)
|
||||
|
||||
// shouldExcludeDir returns true if the directory should be excluded from watching/opening
|
||||
func shouldExcludeDir(dirPath string) bool {
|
||||
dirName := filepath.Base(dirPath)
|
||||
|
||||
// Skip dot directories
|
||||
if strings.HasPrefix(dirName, ".") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip common excluded directories
|
||||
if excludedDirNames[dirName] {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldExcludeFile returns true if the file should be excluded from opening
|
||||
func shouldExcludeFile(filePath string) bool {
|
||||
fileName := filepath.Base(filePath)
|
||||
|
||||
// Skip dot files
|
||||
if strings.HasPrefix(fileName, ".") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check file extension
|
||||
ext := strings.ToLower(filepath.Ext(filePath))
|
||||
if excludedFileExtensions[ext] || largeBinaryExtensions[ext] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip temporary files
|
||||
if strings.HasSuffix(filePath, "~") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check file size
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
// If we can't stat the file, skip it
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip large files
|
||||
if info.Size() > maxFileSize {
|
||||
if debug {
|
||||
log.Printf("Skipping large file: %s (%.2f MB)", filePath, float64(info.Size())/(1024*1024))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// openMatchingFile opens a file if it matches any of the registered patterns
|
||||
func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
|
||||
// Skip directories
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip excluded files
|
||||
if shouldExcludeFile(path) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this path should be watched according to server registrations
|
||||
if watched, _ := w.isPathWatched(path); watched {
|
||||
// Don't need to check if it's already open - the client.OpenFile handles that
|
||||
if err := w.client.OpenFile(ctx, path); err != nil && debug {
|
||||
log.Printf("Error opening file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
244
internal/message/content.go
Normal file
244
internal/message/content.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
package message
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
type MessageRole string
|
||||
|
||||
const (
|
||||
Assistant MessageRole = "assistant"
|
||||
User MessageRole = "user"
|
||||
System MessageRole = "system"
|
||||
Tool MessageRole = "tool"
|
||||
)
|
||||
|
||||
type ContentPart interface {
|
||||
isPart()
|
||||
}
|
||||
|
||||
type ReasoningContent struct {
|
||||
Thinking string `json:"thinking"`
|
||||
}
|
||||
|
||||
func (tc ReasoningContent) String() string {
|
||||
return tc.Thinking
|
||||
}
|
||||
func (ReasoningContent) isPart() {}
|
||||
|
||||
type TextContent struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (tc TextContent) String() string {
|
||||
return tc.Text
|
||||
}
|
||||
|
||||
func (TextContent) isPart() {}
|
||||
|
||||
type ImageURLContent struct {
|
||||
URL string `json:"url"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
func (iuc ImageURLContent) String() string {
|
||||
return iuc.URL
|
||||
}
|
||||
|
||||
func (ImageURLContent) isPart() {}
|
||||
|
||||
type BinaryContent struct {
|
||||
MIMEType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (bc BinaryContent) String() string {
|
||||
base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
|
||||
return "data:" + bc.MIMEType + ";base64," + base64Encoded
|
||||
}
|
||||
|
||||
func (BinaryContent) isPart() {}
|
||||
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input string `json:"input"`
|
||||
Type string `json:"type"`
|
||||
Finished bool `json:"finished"`
|
||||
}
|
||||
|
||||
func (ToolCall) isPart() {}
|
||||
|
||||
type ToolResult struct {
|
||||
ToolCallID string `json:"tool_call_id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
IsError bool `json:"is_error"`
|
||||
}
|
||||
|
||||
func (ToolResult) isPart() {}
|
||||
|
||||
type Finish struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (Finish) isPart() {}
|
||||
|
||||
type Message struct {
|
||||
ID string
|
||||
Role MessageRole
|
||||
SessionID string
|
||||
Parts []ContentPart
|
||||
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
func (m *Message) Content() TextContent {
|
||||
for _, part := range m.Parts {
|
||||
if c, ok := part.(TextContent); ok {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return TextContent{}
|
||||
}
|
||||
|
||||
func (m *Message) ReasoningContent() ReasoningContent {
|
||||
for _, part := range m.Parts {
|
||||
if c, ok := part.(ReasoningContent); ok {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ReasoningContent{}
|
||||
}
|
||||
|
||||
func (m *Message) ImageURLContent() []ImageURLContent {
|
||||
imageURLContents := make([]ImageURLContent, 0)
|
||||
for _, part := range m.Parts {
|
||||
if c, ok := part.(ImageURLContent); ok {
|
||||
imageURLContents = append(imageURLContents, c)
|
||||
}
|
||||
}
|
||||
return imageURLContents
|
||||
}
|
||||
|
||||
func (m *Message) BinaryContent() []BinaryContent {
|
||||
binaryContents := make([]BinaryContent, 0)
|
||||
for _, part := range m.Parts {
|
||||
if c, ok := part.(BinaryContent); ok {
|
||||
binaryContents = append(binaryContents, c)
|
||||
}
|
||||
}
|
||||
return binaryContents
|
||||
}
|
||||
|
||||
func (m *Message) ToolCalls() []ToolCall {
|
||||
toolCalls := make([]ToolCall, 0)
|
||||
for _, part := range m.Parts {
|
||||
if c, ok := part.(ToolCall); ok {
|
||||
toolCalls = append(toolCalls, c)
|
||||
}
|
||||
}
|
||||
return toolCalls
|
||||
}
|
||||
|
||||
func (m *Message) ToolResults() []ToolResult {
|
||||
toolResults := make([]ToolResult, 0)
|
||||
for _, part := range m.Parts {
|
||||
if c, ok := part.(ToolResult); ok {
|
||||
toolResults = append(toolResults, c)
|
||||
}
|
||||
}
|
||||
return toolResults
|
||||
}
|
||||
|
||||
func (m *Message) IsFinished() bool {
|
||||
for _, part := range m.Parts {
|
||||
if _, ok := part.(Finish); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Message) FinishReason() string {
|
||||
for _, part := range m.Parts {
|
||||
if c, ok := part.(Finish); ok {
|
||||
return c.Reason
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Message) IsThinking() bool {
|
||||
if m.ReasoningContent().Thinking != "" && m.Content().Text == "" && !m.IsFinished() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Message) AppendContent(delta string) {
|
||||
found := false
|
||||
for i, part := range m.Parts {
|
||||
if c, ok := part.(TextContent); ok {
|
||||
m.Parts[i] = TextContent{Text: c.Text + delta}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
m.Parts = append(m.Parts, TextContent{Text: delta})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) AppendReasoningContent(delta string) {
|
||||
found := false
|
||||
for i, part := range m.Parts {
|
||||
if c, ok := part.(ReasoningContent); ok {
|
||||
m.Parts[i] = ReasoningContent{Thinking: c.Thinking + delta}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
m.Parts = append(m.Parts, ReasoningContent{Thinking: delta})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) AddToolCall(tc ToolCall) {
|
||||
for i, part := range m.Parts {
|
||||
if c, ok := part.(ToolCall); ok {
|
||||
if c.ID == tc.ID {
|
||||
m.Parts[i] = tc
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
m.Parts = append(m.Parts, tc)
|
||||
}
|
||||
|
||||
func (m *Message) SetToolCalls(tc []ToolCall) {
|
||||
for _, toolCall := range tc {
|
||||
m.Parts = append(m.Parts, toolCall)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) AddToolResult(tr ToolResult) {
|
||||
m.Parts = append(m.Parts, tr)
|
||||
}
|
||||
|
||||
func (m *Message) SetToolResults(tr []ToolResult) {
|
||||
for _, toolResult := range tr {
|
||||
m.Parts = append(m.Parts, toolResult)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) AddFinish(reason string) {
|
||||
m.Parts = append(m.Parts, Finish{Reason: reason})
|
||||
}
|
||||
|
||||
func (m *Message) AddImageURL(url, detail string) {
|
||||
m.Parts = append(m.Parts, ImageURLContent{URL: url, Detail: detail})
|
||||
}
|
||||
|
||||
func (m *Message) AddBinary(mimeType string, data []byte) {
|
||||
m.Parts = append(m.Parts, BinaryContent{MIMEType: mimeType, Data: data})
|
||||
}
|
|
@ -2,59 +2,17 @@ package message
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kujtimiihoxha/termai/internal/db"
|
||||
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||
)
|
||||
|
||||
type MessageRole string
|
||||
|
||||
const (
|
||||
Assistant MessageRole = "assistant"
|
||||
User MessageRole = "user"
|
||||
System MessageRole = "system"
|
||||
Tool MessageRole = "tool"
|
||||
)
|
||||
|
||||
type ToolResult struct {
|
||||
ToolCallID string
|
||||
Content string
|
||||
IsError bool
|
||||
// TODO: support for images
|
||||
}
|
||||
|
||||
type ToolCall struct {
|
||||
ID string
|
||||
Name string
|
||||
Input string
|
||||
Type string
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
ID string
|
||||
SessionID string
|
||||
|
||||
// NEW
|
||||
Role MessageRole
|
||||
Content string
|
||||
Thinking string
|
||||
|
||||
Finished bool
|
||||
|
||||
ToolResults []ToolResult
|
||||
ToolCalls []ToolCall
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
type CreateMessageParams struct {
|
||||
Role MessageRole
|
||||
Content string
|
||||
ToolCalls []ToolCall
|
||||
ToolResults []ToolResult
|
||||
Role MessageRole
|
||||
Parts []ContentPart
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
|
@ -73,6 +31,14 @@ type service struct {
|
|||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, q db.Querier) Service {
|
||||
return &service{
|
||||
Broker: pubsub.NewBroker[Message](),
|
||||
q: q,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Delete(id string) error {
|
||||
message, err := s.Get(id)
|
||||
if err != nil {
|
||||
|
@ -87,22 +53,21 @@ func (s *service) Delete(id string) error {
|
|||
}
|
||||
|
||||
func (s *service) Create(sessionID string, params CreateMessageParams) (Message, error) {
|
||||
toolCallsStr, err := json.Marshal(params.ToolCalls)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
toolResultsStr, err := json.Marshal(params.ToolResults)
|
||||
if params.Role != Assistant {
|
||||
params.Parts = append(params.Parts, Finish{
|
||||
Reason: "stop",
|
||||
})
|
||||
}
|
||||
partsJSON, err := marshallParts(params.Parts)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
|
||||
dbMessage, err := s.q.CreateMessage(s.ctx, db.CreateMessageParams{
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
Role: string(params.Role),
|
||||
Finished: params.Role != Assistant,
|
||||
Content: params.Content,
|
||||
ToolCalls: sql.NullString{String: string(toolCallsStr), Valid: true},
|
||||
ToolResults: sql.NullString{String: string(toolResultsStr), Valid: true},
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
Role: string(params.Role),
|
||||
Parts: string(partsJSON),
|
||||
})
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
|
@ -132,21 +97,13 @@ func (s *service) DeleteSessionMessages(sessionID string) error {
|
|||
}
|
||||
|
||||
func (s *service) Update(message Message) error {
|
||||
toolCallsStr, err := json.Marshal(message.ToolCalls)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
toolResultsStr, err := json.Marshal(message.ToolResults)
|
||||
parts, err := marshallParts(message.Parts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.q.UpdateMessage(s.ctx, db.UpdateMessageParams{
|
||||
ID: message.ID,
|
||||
Content: message.Content,
|
||||
Thinking: message.Thinking,
|
||||
Finished: message.Finished,
|
||||
ToolCalls: sql.NullString{String: string(toolCallsStr), Valid: true},
|
||||
ToolResults: sql.NullString{String: string(toolResultsStr), Valid: true},
|
||||
ID: message.ID,
|
||||
Parts: string(parts),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -179,40 +136,136 @@ func (s *service) List(sessionID string) ([]Message, error) {
|
|||
}
|
||||
|
||||
func (s *service) fromDBItem(item db.Message) (Message, error) {
|
||||
toolCalls := make([]ToolCall, 0)
|
||||
if item.ToolCalls.Valid {
|
||||
err := json.Unmarshal([]byte(item.ToolCalls.String), &toolCalls)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
parts, err := unmarshallParts([]byte(item.Parts))
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
|
||||
toolResults := make([]ToolResult, 0)
|
||||
if item.ToolResults.Valid {
|
||||
err := json.Unmarshal([]byte(item.ToolResults.String), &toolResults)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return Message{
|
||||
ID: item.ID,
|
||||
SessionID: item.SessionID,
|
||||
Role: MessageRole(item.Role),
|
||||
Content: item.Content,
|
||||
Thinking: item.Thinking,
|
||||
Finished: item.Finished,
|
||||
ToolCalls: toolCalls,
|
||||
ToolResults: toolResults,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
ID: item.ID,
|
||||
SessionID: item.SessionID,
|
||||
Role: MessageRole(item.Role),
|
||||
Parts: parts,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, q db.Querier) Service {
|
||||
return &service{
|
||||
Broker: pubsub.NewBroker[Message](),
|
||||
q: q,
|
||||
ctx: ctx,
|
||||
}
|
||||
type partType string
|
||||
|
||||
const (
|
||||
reasoningType partType = "reasoning"
|
||||
textType partType = "text"
|
||||
imageURLType partType = "image_url"
|
||||
binaryType partType = "binary"
|
||||
toolCallType partType = "tool_call"
|
||||
toolResultType partType = "tool_result"
|
||||
finishType partType = "finish"
|
||||
)
|
||||
|
||||
type partWrapper struct {
|
||||
Type partType `json:"type"`
|
||||
Data ContentPart `json:"data"`
|
||||
}
|
||||
|
||||
func marshallParts(parts []ContentPart) ([]byte, error) {
|
||||
wrappedParts := make([]partWrapper, len(parts))
|
||||
|
||||
for i, part := range parts {
|
||||
var typ partType
|
||||
|
||||
switch part.(type) {
|
||||
case ReasoningContent:
|
||||
typ = reasoningType
|
||||
case TextContent:
|
||||
typ = textType
|
||||
case ImageURLContent:
|
||||
typ = imageURLType
|
||||
case BinaryContent:
|
||||
typ = binaryType
|
||||
case ToolCall:
|
||||
typ = toolCallType
|
||||
case ToolResult:
|
||||
typ = toolResultType
|
||||
case Finish:
|
||||
typ = finishType
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown part type: %T", part)
|
||||
}
|
||||
|
||||
wrappedParts[i] = partWrapper{
|
||||
Type: typ,
|
||||
Data: part,
|
||||
}
|
||||
}
|
||||
return json.Marshal(wrappedParts)
|
||||
}
|
||||
|
||||
func unmarshallParts(data []byte) ([]ContentPart, error) {
|
||||
temp := []json.RawMessage{}
|
||||
|
||||
if err := json.Unmarshal(data, &temp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parts := make([]ContentPart, 0)
|
||||
|
||||
for _, rawPart := range temp {
|
||||
var wrapper struct {
|
||||
Type partType `json:"type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(rawPart, &wrapper); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch wrapper.Type {
|
||||
case reasoningType:
|
||||
part := ReasoningContent{}
|
||||
if err := json.Unmarshal(wrapper.Data, &part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, part)
|
||||
case textType:
|
||||
part := TextContent{}
|
||||
if err := json.Unmarshal(wrapper.Data, &part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, part)
|
||||
case imageURLType:
|
||||
part := ImageURLContent{}
|
||||
if err := json.Unmarshal(wrapper.Data, &part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case binaryType:
|
||||
part := BinaryContent{}
|
||||
if err := json.Unmarshal(wrapper.Data, &part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, part)
|
||||
case toolCallType:
|
||||
part := ToolCall{}
|
||||
if err := json.Unmarshal(wrapper.Data, &part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, part)
|
||||
case toolResultType:
|
||||
part := ToolResult{}
|
||||
if err := json.Unmarshal(wrapper.Data, &part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, part)
|
||||
case finishType:
|
||||
part := Finish{}
|
||||
if err := json.Unmarshal(wrapper.Data, &part); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, part)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown part type: %s", wrapper.Type)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return parts, nil
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/termai/internal/app"
|
||||
"github.com/kujtimiihoxha/termai/internal/llm/agent"
|
||||
"github.com/kujtimiihoxha/termai/internal/lsp/protocol"
|
||||
"github.com/kujtimiihoxha/termai/internal/message"
|
||||
"github.com/kujtimiihoxha/termai/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/termai/internal/session"
|
||||
|
@ -39,6 +41,7 @@ type messagesCmp struct {
|
|||
height int
|
||||
focused bool
|
||||
cachedView string
|
||||
timeLoaded time.Time
|
||||
}
|
||||
|
||||
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
@ -51,7 +54,8 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.viewport.GotoBottom()
|
||||
}
|
||||
for _, v := range m.messages {
|
||||
for _, c := range v.ToolCalls {
|
||||
for _, c := range v.ToolCalls() {
|
||||
// the message is being added to the session of a tool called
|
||||
if c.ID == msg.Payload.SessionID {
|
||||
m.renderView()
|
||||
m.viewport.GotoBottom()
|
||||
|
@ -130,12 +134,11 @@ func hasUnfinishedMessages(messages []message.Message) bool {
|
|||
return false
|
||||
}
|
||||
for _, msg := range messages {
|
||||
if !msg.Finished {
|
||||
if !msg.IsFinished() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
lastMessage := messages[len(messages)-1]
|
||||
return lastMessage.Role != message.Assistant
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *messagesCmp) renderMessageWithToolCall(content string, tools []message.ToolCall, futureMessages []message.Message) string {
|
||||
|
@ -205,7 +208,7 @@ func (m *messagesCmp) renderMessageWithToolCall(content string, tools []message.
|
|||
findToolResult := func(toolCallID string, messages []message.Message) *message.ToolResult {
|
||||
for _, msg := range messages {
|
||||
if msg.Role == message.Tool {
|
||||
for _, result := range msg.ToolResults {
|
||||
for _, result := range msg.ToolResults() {
|
||||
if result.ToolCallID == toolCallID {
|
||||
return &result
|
||||
}
|
||||
|
@ -257,7 +260,7 @@ func (m *messagesCmp) renderMessageWithToolCall(content string, tools []message.
|
|||
taskSessionMessages, _ := m.app.Messages.List(toolCall.ID)
|
||||
for _, msg := range taskSessionMessages {
|
||||
if msg.Role == message.Assistant {
|
||||
for _, toolCall := range msg.ToolCalls {
|
||||
for _, toolCall := range msg.ToolCalls() {
|
||||
toolHeader := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(styles.Blue).
|
||||
|
@ -304,11 +307,11 @@ func (m *messagesCmp) renderMessageWithToolCall(content string, tools []message.
|
|||
}
|
||||
|
||||
for _, msg := range futureMessages {
|
||||
if msg.Content != "" {
|
||||
if msg.Content().String() != "" {
|
||||
break
|
||||
}
|
||||
|
||||
for _, toolCall := range msg.ToolCalls {
|
||||
for _, toolCall := range msg.ToolCalls() {
|
||||
toolOutput := renderTool(toolCall)
|
||||
allParts = append(allParts, " "+strings.ReplaceAll(toolOutput, "\n", "\n "))
|
||||
|
||||
|
@ -339,10 +342,10 @@ func (m *messagesCmp) renderView() {
|
|||
|
||||
prevMessageWasUser := false
|
||||
for inx, msg := range m.messages {
|
||||
content := msg.Content
|
||||
content := msg.Content().String()
|
||||
if content != "" || prevMessageWasUser {
|
||||
if msg.Thinking != "" && content == "" {
|
||||
content = msg.Thinking
|
||||
if msg.ReasoningContent().String() != "" && content == "" {
|
||||
content = msg.ReasoningContent().String()
|
||||
} else if content == "" {
|
||||
content = "..."
|
||||
}
|
||||
|
@ -367,14 +370,14 @@ func (m *messagesCmp) renderView() {
|
|||
EmbeddedText: borderText(msg.Role, currentMessage),
|
||||
},
|
||||
)
|
||||
if len(msg.ToolCalls) > 0 {
|
||||
content = m.renderMessageWithToolCall(content, msg.ToolCalls, m.messages[inx+1:])
|
||||
if len(msg.ToolCalls()) > 0 {
|
||||
content = m.renderMessageWithToolCall(content, msg.ToolCalls(), m.messages[inx+1:])
|
||||
}
|
||||
stringMessages = append(stringMessages, content)
|
||||
currentMessage++
|
||||
displayedMsgCount++
|
||||
}
|
||||
if msg.Role == message.User && msg.Content != "" {
|
||||
if msg.Role == message.User && msg.Content().String() != "" {
|
||||
prevMessageWasUser = true
|
||||
} else {
|
||||
prevMessageWasUser = false
|
||||
|
@ -398,6 +401,57 @@ func (m *messagesCmp) Blur() tea.Cmd {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (m *messagesCmp) projectDiagnostics() string {
|
||||
errorDiagnostics := []protocol.Diagnostic{}
|
||||
warnDiagnostics := []protocol.Diagnostic{}
|
||||
hintDiagnostics := []protocol.Diagnostic{}
|
||||
infoDiagnostics := []protocol.Diagnostic{}
|
||||
for _, client := range m.app.LSPClients {
|
||||
for _, d := range client.GetDiagnostics() {
|
||||
for _, diag := range d {
|
||||
switch diag.Severity {
|
||||
case protocol.SeverityError:
|
||||
errorDiagnostics = append(errorDiagnostics, diag)
|
||||
case protocol.SeverityWarning:
|
||||
warnDiagnostics = append(warnDiagnostics, diag)
|
||||
case protocol.SeverityHint:
|
||||
hintDiagnostics = append(hintDiagnostics, diag)
|
||||
case protocol.SeverityInformation:
|
||||
infoDiagnostics = append(infoDiagnostics, diag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errorDiagnostics) == 0 && len(warnDiagnostics) == 0 && len(hintDiagnostics) == 0 && len(infoDiagnostics) == 0 {
|
||||
if time.Since(m.timeLoaded) < time.Second*10 {
|
||||
return "Loading diagnostics..."
|
||||
}
|
||||
return "No diagnostics"
|
||||
}
|
||||
|
||||
diagnostics := []string{}
|
||||
|
||||
if len(errorDiagnostics) > 0 {
|
||||
errStr := lipgloss.NewStyle().Foreground(styles.Error).Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
|
||||
diagnostics = append(diagnostics, errStr)
|
||||
}
|
||||
if len(warnDiagnostics) > 0 {
|
||||
warnStr := lipgloss.NewStyle().Foreground(styles.Warning).Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
|
||||
diagnostics = append(diagnostics, warnStr)
|
||||
}
|
||||
if len(hintDiagnostics) > 0 {
|
||||
hintStr := lipgloss.NewStyle().Foreground(styles.Text).Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
|
||||
diagnostics = append(diagnostics, hintStr)
|
||||
}
|
||||
if len(infoDiagnostics) > 0 {
|
||||
infoStr := lipgloss.NewStyle().Foreground(styles.Peach).Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
|
||||
diagnostics = append(diagnostics, infoStr)
|
||||
}
|
||||
|
||||
return strings.Join(diagnostics, " ")
|
||||
}
|
||||
|
||||
func (m *messagesCmp) BorderText() map[layout.BorderPosition]string {
|
||||
title := m.session.Title
|
||||
titleWidth := m.width / 2
|
||||
|
@ -409,7 +463,7 @@ func (m *messagesCmp) BorderText() map[layout.BorderPosition]string {
|
|||
}
|
||||
borderTest := map[layout.BorderPosition]string{
|
||||
layout.TopLeftBorder: title,
|
||||
layout.BottomRightBorder: formatTokensAndCost(m.session.CompletionTokens+m.session.PromptTokens, m.session.Cost),
|
||||
layout.BottomRightBorder: m.projectDiagnostics(),
|
||||
}
|
||||
if hasUnfinishedMessages(m.messages) {
|
||||
borderTest[layout.BottomLeftBorder] = lipgloss.NewStyle().Foreground(styles.Peach).Render("Thinking...")
|
||||
|
@ -442,6 +496,7 @@ func (m *messagesCmp) SetSize(width int, height int) {
|
|||
}
|
||||
|
||||
func (m *messagesCmp) Init() tea.Cmd {
|
||||
m.timeLoaded = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -245,4 +245,3 @@ func NewSessionsCmp(app *app.App) SessionsCmp {
|
|||
focused: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,11 @@ const (
|
|||
UserIcon string = ""
|
||||
|
||||
CheckIcon string = "✓"
|
||||
ErrorIcon string = "✗"
|
||||
ErrorIcon string = ""
|
||||
WarningIcon string = ""
|
||||
InfoIcon string = ""
|
||||
HintIcon string = ""
|
||||
SpinnerIcon string = "..."
|
||||
|
||||
SleepIcon string = ""
|
||||
BugIcon string = ""
|
||||
SleepIcon string = ""
|
||||
)
|
||||
|
|
6
main.go
6
main.go
|
@ -1,6 +1,3 @@
|
|||
/*
|
||||
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -11,7 +8,8 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
// Create a log file and make that the log output
|
||||
// Create a log file and make that the log output DEBUG
|
||||
// TODO: remove this on release
|
||||
logfile, err := os.OpenFile("debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue