@@ -20,11 +20,15 @@ import (
2020 "regexp"
2121 "strconv"
2222 "strings"
23+ "sync"
24+ "time"
2325
2426 "github.com/ajitpratap0/GoSQLX/pkg/errors"
2527 "github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
28+ "github.com/ajitpratap0/GoSQLX/pkg/models"
2629 "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
2730 "github.com/ajitpratap0/GoSQLX/pkg/sql/parser"
31+ "github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer"
2832)
2933
3034// Handler processes LSP requests and notifications.
@@ -107,8 +111,10 @@ import (
107111// - Immutable keyword and snippet data structures
108112// - No shared mutable state between requests
109113type Handler struct {
110- server * Server
111- keywords * keywords.Keywords
114+ server * Server
115+ keywords * keywords.Keywords
116+ debounceMu sync.Mutex
117+ debounceTimers map [string ]* time.Timer
112118}
113119
114120// NewHandler creates a new LSP request handler.
@@ -125,8 +131,9 @@ type Handler struct {
125131// Returns a fully initialized Handler ready to process LSP requests.
126132func NewHandler (server * Server ) * Handler {
127133 return & Handler {
128- server : server ,
129- keywords : keywords .New (keywords .DialectGeneric , true ),
134+ server : server ,
135+ keywords : keywords .New (keywords .DialectGeneric , true ),
136+ debounceTimers : make (map [string ]* time.Timer ),
130137 }
131138}
132139
@@ -149,6 +156,12 @@ func (h *Handler) HandleRequest(method string, params json.RawMessage) (interfac
149156 return h .handleSignatureHelp (params )
150157 case "textDocument/codeAction" :
151158 return h .handleCodeAction (params )
159+ case "textDocument/semanticTokens/full" :
160+ var semParams SemanticTokensParams
161+ if err := json .Unmarshal (params , & semParams ); err != nil {
162+ return nil , err
163+ }
164+ return h .handleSemanticTokensFull (semParams )
152165 default :
153166 return nil , fmt .Errorf ("method not found: %s" , method )
154167 }
@@ -204,6 +217,16 @@ func (h *Handler) handleInitialize(params json.RawMessage) (*InitializeResult, e
204217 CodeActionProvider : & CodeActionOptions {
205218 CodeActionKinds : []CodeActionKind {CodeActionQuickFix },
206219 },
220+ SemanticTokensProvider : & SemanticTokensOptions {
221+ Legend : SemanticTokensLegend {
222+ TokenTypes : []string {
223+ "keyword" , "identifier" , "number" , "string" , "operator" , "comment" ,
224+ },
225+ TokenModifiers : []string {},
226+ },
227+ Full : true ,
228+ Range : false ,
229+ },
207230 },
208231 ServerInfo : & ServerInfo {
209232 Name : "gosqlx-lsp" ,
@@ -285,21 +308,38 @@ func (h *Handler) handleDidChange(params json.RawMessage) {
285308 p .ContentChanges ,
286309 )
287310
288- // Get updated content and validate
289- if content , ok := h .server .Documents ().GetContent (p .TextDocument .URI ); ok {
290- // Check document size limit after update
291- if len (content ) > h .server .MaxDocumentSizeBytes () {
292- h .server .Logger ().Printf ("Document too large after change: %d bytes (max: %d)" , len (content ), h .server .MaxDocumentSizeBytes ())
293- // Clear diagnostics but don't validate
294- h .server .SendNotification ("textDocument/publishDiagnostics" , PublishDiagnosticsParams {
295- URI : p .TextDocument .URI ,
296- Version : p .TextDocument .Version ,
297- Diagnostics : []Diagnostic {},
298- })
299- return
300- }
301- h .validateDocument (p .TextDocument .URI , content , p .TextDocument .Version )
311+ // Get updated content for size check and debounced validation
312+ content , ok := h .server .Documents ().GetContent (p .TextDocument .URI )
313+ if ! ok {
314+ return
302315 }
316+
317+ // Check document size limit after update
318+ if len (content ) > h .server .MaxDocumentSizeBytes () {
319+ h .server .Logger ().Printf ("Document too large after change: %d bytes (max: %d)" , len (content ), h .server .MaxDocumentSizeBytes ())
320+ // Clear diagnostics but don't validate
321+ h .server .SendNotification ("textDocument/publishDiagnostics" , PublishDiagnosticsParams {
322+ URI : p .TextDocument .URI ,
323+ Version : p .TextDocument .Version ,
324+ Diagnostics : []Diagnostic {},
325+ })
326+ return
327+ }
328+
329+ // Debounce diagnostics — cancel existing timer and schedule new one
330+ uri := p .TextDocument .URI
331+ version := p .TextDocument .Version
332+ h .debounceMu .Lock ()
333+ if t , ok := h .debounceTimers [uri ]; ok {
334+ t .Stop ()
335+ }
336+ h .debounceTimers [uri ] = time .AfterFunc (300 * time .Millisecond , func () {
337+ h .debounceMu .Lock ()
338+ delete (h .debounceTimers , uri )
339+ h .debounceMu .Unlock ()
340+ h .validateDocument (uri , content , version )
341+ })
342+ h .debounceMu .Unlock ()
303343}
304344
305345// handleDidClose handles document close notifications
@@ -312,6 +352,15 @@ func (h *Handler) handleDidClose(params json.RawMessage) {
312352 }
313353
314354 h .server .Logger ().Printf ("Document closed: %s" , p .TextDocument .URI )
355+
356+ // Cancel any pending debounce timer for this document
357+ h .debounceMu .Lock ()
358+ if t , ok := h .debounceTimers [p .TextDocument .URI ]; ok {
359+ t .Stop ()
360+ delete (h .debounceTimers , p .TextDocument .URI )
361+ }
362+ h .debounceMu .Unlock ()
363+
315364 h .server .Documents ().Close (p .TextDocument .URI )
316365
317366 // Clear diagnostics for closed document
@@ -1649,3 +1698,121 @@ func truncateForLog(data json.RawMessage) string {
16491698 }
16501699 return s
16511700}
1701+
1702+ // handleSemanticTokensFull handles textDocument/semanticTokens/full requests.
1703+ // It tokenizes the document and returns LSP-encoded semantic token data.
1704+ func (h * Handler ) handleSemanticTokensFull (params SemanticTokensParams ) (* SemanticTokens , error ) {
1705+ doc , ok := h .server .Documents ().Get (params .TextDocument .URI )
1706+ if ! ok {
1707+ return & SemanticTokens {Data : []uint32 {}}, nil
1708+ }
1709+
1710+ tkz := tokenizer .GetTokenizer ()
1711+ defer tokenizer .PutTokenizer (tkz )
1712+ tokens , err := tkz .Tokenize ([]byte (doc .Content ))
1713+ if err != nil {
1714+ return & SemanticTokens {Data : []uint32 {}}, nil
1715+ }
1716+
1717+ return & SemanticTokens {Data : encodeSemanticTokens (tokens )}, nil
1718+ }
1719+
1720+ // encodeSemanticTokens converts token positions to LSP semantic token encoding.
1721+ // Each token is encoded as 5 uint32 values:
1722+ //
1723+ // [deltaLine, deltaStartChar, length, tokenType, tokenModifiers]
1724+ func encodeSemanticTokens (tokens []models.TokenWithSpan ) []uint32 {
1725+ data := make ([]uint32 , 0 , len (tokens )* 5 )
1726+ prevLine := uint32 (0 )
1727+ prevStartChar := uint32 (0 )
1728+
1729+ for _ , tok := range tokens {
1730+ tokenType := classifyToken (tok )
1731+ if tokenType < 0 {
1732+ continue // skip tokens we don't classify
1733+ }
1734+
1735+ // Location is 1-based; convert to 0-based for LSP
1736+ line := uint32 (tok .Start .Line - 1 )
1737+ startChar := uint32 (tok .Start .Column - 1 )
1738+ length := uint32 (tok .End .Column - tok .Start .Column )
1739+ if length == 0 {
1740+ // Fallback: use text length
1741+ length = uint32 (len (tok .Token .Value ))
1742+ }
1743+
1744+ deltaLine := line - prevLine
1745+ deltaStartChar := startChar
1746+ if deltaLine == 0 {
1747+ deltaStartChar = startChar - prevStartChar
1748+ }
1749+
1750+ data = append (data , deltaLine , deltaStartChar , length , uint32 (tokenType ), 0 )
1751+ prevLine = line
1752+ prevStartChar = startChar
1753+ }
1754+ return data
1755+ }
1756+
1757+ // Token type indices matching the legend declared in handleInitialize:
1758+ //
1759+ // "keyword", "identifier", "number", "string", "operator", "comment"
1760+ const (
1761+ semTokKeyword = 0
1762+ semTokIdentifier = 1
1763+ semTokNumber = 2
1764+ semTokString = 3
1765+ semTokOperator = 4
1766+ semTokComment = 5
1767+ )
1768+
1769+ // classifyToken maps a TokenWithSpan to a semantic token type index.
1770+ // Returns -1 for tokens that should not be included in the semantic token stream.
1771+ func classifyToken (tok models.TokenWithSpan ) int {
1772+ switch tok .Token .Type {
1773+ case models .TokenTypeKeyword ,
1774+ models .TokenTypeSelect , models .TokenTypeFrom , models .TokenTypeWhere ,
1775+ models .TokenTypeInsert , models .TokenTypeUpdate , models .TokenTypeDelete ,
1776+ models .TokenTypeCreate , models .TokenTypeDrop , models .TokenTypeAlter ,
1777+ models .TokenTypeTable , models .TokenTypeIndex ,
1778+ models .TokenTypeJoin , models .TokenTypeLeft , models .TokenTypeRight ,
1779+ models .TokenTypeInner , models .TokenTypeOuter ,
1780+ models .TokenTypeOn , models .TokenTypeAs ,
1781+ models .TokenTypeAnd , models .TokenTypeOr , models .TokenTypeNot ,
1782+ models .TokenTypeIn , models .TokenTypeIs , models .TokenTypeNull ,
1783+ models .TokenTypeLike ,
1784+ models .TokenTypeOrder , models .TokenTypeGroup , models .TokenTypeBy ,
1785+ models .TokenTypeHaving , models .TokenTypeLimit , models .TokenTypeOffset ,
1786+ models .TokenTypeWith , models .TokenTypeUnion ,
1787+ models .TokenTypeIntersect , models .TokenTypeExcept ,
1788+ models .TokenTypeDistinct ,
1789+ models .TokenTypeCase , models .TokenTypeWhen , models .TokenTypeThen ,
1790+ models .TokenTypeElse , models .TokenTypeEnd :
1791+ return semTokKeyword
1792+ case models .TokenTypeWord , models .TokenTypeIdentifier :
1793+ return semTokIdentifier
1794+ case models .TokenTypeNumber :
1795+ return semTokNumber
1796+ case models .TokenTypeString ,
1797+ models .TokenTypeSingleQuotedString , models .TokenTypeDoubleQuotedString ,
1798+ models .TokenTypeDollarQuotedString , models .TokenTypeEscapedStringLiteral ,
1799+ models .TokenTypeNationalStringLiteral , models .TokenTypeUnicodeStringLiteral ,
1800+ models .TokenTypeHexStringLiteral :
1801+ return semTokString
1802+ case models .TokenTypeWhitespace :
1803+ // Whitespace tokens that are comments
1804+ if tok .Token .Word == nil {
1805+ return - 1
1806+ }
1807+ return semTokComment
1808+ case models .TokenTypeEq , models .TokenTypeNeq ,
1809+ models .TokenTypeLt , models .TokenTypeGt ,
1810+ models .TokenTypeLtEq , models .TokenTypeGtEq ,
1811+ models .TokenTypePlus , models .TokenTypeMinus ,
1812+ models .TokenTypeMul , models .TokenTypeDiv ,
1813+ models .TokenTypeMod :
1814+ return semTokOperator
1815+ default :
1816+ return - 1 // don't classify
1817+ }
1818+ }
0 commit comments