├── .gitignore ├── test ├── format_test.go ├── types_test.go ├── transport_test.go ├── completion_test.go ├── uri_test.go ├── lifecycle_test.go ├── parser_test.go └── incremental_test.go ├── util ├── files.go └── handle.go ├── go.mod ├── server ├── diagnostics.go ├── completion.go ├── formatting.go ├── synchronization.go ├── incremental.go ├── config.go ├── lifecycle.go ├── compiler.go ├── files.go ├── server.go ├── goto_methods.go ├── workspace.go └── symbols.go ├── flake.lock ├── flake.nix ├── logging └── log.go ├── main.go ├── transport ├── transport_types.go └── transport.go ├── README.md ├── go.sum └── parser └── parser.go /.gitignore: -------------------------------------------------------------------------------- 1 | faustlsp* 2 | build.sh 3 | log.txt -------------------------------------------------------------------------------- /test/format_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/carn181/faustlsp/server" 7 | ) 8 | 9 | func TestFormat(t *testing.T) { 10 | out, err := server.Format([]byte("process=a with {f=2;};"), " ") 11 | t.Log(string(out), err) 12 | } 13 | -------------------------------------------------------------------------------- /util/files.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | 9 | func IsValidPath(path Path) bool{ 10 | _, err := os.Stat(path) 11 | if err != nil { 12 | if os.IsNotExist(err){ 13 | return false 14 | } 15 | fmt.Println(err) 16 | return false 17 | } else { 18 | return true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/types_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/carn181/faustlsp/transport" 8 | ) 9 | 10 | func TestResponseType(t *testing.T) { 11 | r1 := transport.ResponseMessage{ 12 | Message: transport.Message{Jsonrpc: "2.0"}, 13 | ID: 1, 14 | Result: []byte(""), 15 | Error: nil, 16 | } 17 | msg, _ := json.Marshal(r1) 18 | t.Log(string(msg)) 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/carn181/faustlsp 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.9.0 7 | github.com/khiner/tree-sitter-faust v0.0.0-20250701002309-122dd1019192 8 | github.com/otiai10/copy v1.14.1 9 | github.com/tree-sitter/go-tree-sitter v0.25.0 10 | ) 11 | 12 | require ( 13 | github.com/mattn/go-pointer v0.0.1 // indirect 14 | github.com/otiai10/mint v1.6.3 // indirect 15 | golang.org/x/sync v0.8.0 // indirect 16 | golang.org/x/sys v0.24.0 // indirect 17 | ) 18 | 19 | replace github.com/fsnotify/fsnotify v1.9.0 => github.com/carn181/fsnotify v0.0.0-20250612182652-935ca6b92412 20 | -------------------------------------------------------------------------------- /server/diagnostics.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/carn181/faustlsp/logging" 7 | "github.com/carn181/faustlsp/transport" 8 | ) 9 | 10 | func (s *Server) GenerateDiagnostics() { 11 | s.diagChan = make(chan transport.PublishDiagnosticsParams) 12 | 13 | for { 14 | logging.Logger.Info("Waiting for diagnostic\n") 15 | select { 16 | case diag := <-s.diagChan: 17 | content, _ := json.Marshal(diag) 18 | logging.Logger.Info("Writing Diagnostic", "content", string(content)) 19 | s.Transport.WriteNotif("textDocument/publishDiagnostics", content) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1748026580, 6 | "narHash": "sha256-rWtXrcIzU5wm/C8F9LWvUfBGu5U5E7cFzPYT1pHIJaQ=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "11cb3517b3af6af300dd6c055aeda73c9bf52c48", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "25.05", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | # flake.nix 2 | { 3 | inputs = { 4 | nixpkgs.url = "github:NixOS/nixpkgs/25.05"; # Or a specific release branch 5 | # You may need to add other inputs for specific Go-related tools 6 | }; 7 | 8 | outputs = 9 | { self, nixpkgs, ... }@inputs: 10 | let 11 | system = "x86_64-linux"; # Replace with your system architecture 12 | pkgs = import nixpkgs { 13 | inherit system; 14 | }; 15 | in 16 | { 17 | devShells.${system}.default = pkgs.mkShell { 18 | 19 | buildInputs = with pkgs; [ 20 | go 21 | gopls 22 | ]; 23 | 24 | # Set environment variables if needed (e.g., GOPROXY) 25 | shellHook = '' 26 | export PATH=$PATH:''${GOPATH:-~/.local/share/go}/bin 27 | ''; 28 | }; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /test/transport_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "github.com/carn181/faustlsp/transport" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestSocket(test *testing.T) { 11 | expectedMsg := []byte("Content-Length: 4\r\n\r\nHey!") 12 | client := func() { 13 | var t transport.Transport 14 | t.Init(transport.Client, transport.Socket) 15 | 16 | err := t.Write([]byte("Hey!")) 17 | if err != nil { 18 | test.Fatal(err) 19 | } 20 | 21 | t.Close() 22 | } 23 | 24 | server := func() { 25 | var t transport.Transport 26 | 27 | t.Init(transport.Server, transport.Socket) 28 | 29 | msg, err := t.Read() 30 | if err != nil { 31 | fmt.Println(err) 32 | test.Fatal(err) 33 | } 34 | 35 | bytes.Equal(msg, expectedMsg) 36 | 37 | if !bytes.Equal(msg, expectedMsg) { 38 | test.Fatalf("Got different message: %s\n", string(msg)) 39 | } 40 | 41 | t.Close() 42 | } 43 | 44 | go func() { server() }() 45 | client() 46 | 47 | } 48 | -------------------------------------------------------------------------------- /logging/log.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | ) 9 | 10 | // Logger is the global logger instance. 11 | var Logger *slog.Logger 12 | 13 | // Init initializes the logger with a file output. 14 | func Init() { 15 | // TODO: Add option to take log file path from user 16 | 17 | // os.TempDir gives temporary directory of any platform 18 | faustTempDir := filepath.Join(os.TempDir(), "faustlsp") 19 | os.Mkdir(faustTempDir, 0750) 20 | 21 | currTime := time.Now().Format("15-04-05") 22 | logFile := "log-" + currTime + ".json" 23 | logFilePath := filepath.Join(faustTempDir, logFile) 24 | 25 | f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_RDWR, 0755) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | // Initialize the logger to write to the file, without flags or prefixes. 31 | // Logger = log.New(f, "faust-lsp: ", log.Ltime) 32 | Logger = slog.New(slog.NewJSONHandler(f, &slog.HandlerOptions{ 33 | AddSource: true, 34 | })) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/carn181/faustlsp/logging" 11 | "github.com/carn181/faustlsp/server" 12 | "github.com/carn181/faustlsp/transport" 13 | ) 14 | 15 | func main() { 16 | logging.Init() 17 | 18 | logging.Logger.Info("Initialized") 19 | 20 | // Background Context for cancelling 21 | ctx, cancel := context.WithCancel(context.Background()) 22 | 23 | var s server.Server 24 | 25 | // Default Transport method is stdin 26 | s.Init(transport.Stdin) 27 | 28 | // Handle Signals 29 | sigs := make(chan os.Signal, 1) 30 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 31 | 32 | go func() { 33 | // Cancel when a signal is received 34 | <-sigs 35 | cancel() 36 | fmt.Println("Got Interrupt ") 37 | logging.Logger.Info("Got Interrupt") 38 | }() 39 | 40 | // Start running server 41 | err := s.Run(ctx) 42 | logging.Logger.Info("Ended") 43 | 44 | if err != nil { 45 | os.Exit(1) 46 | } else { 47 | os.Exit(0) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /transport/transport_types.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "encoding/json" 4 | 5 | type URI string 6 | type DocumentURI string 7 | 8 | type Message struct { 9 | Jsonrpc string `json:"jsonrpc"` 10 | } 11 | 12 | type RPCMessage struct { 13 | Jsonrpc string `json:"jsonrpc"` 14 | Method string `json:"method"` 15 | } 16 | 17 | type RequestMessage struct { 18 | Message 19 | ID any `json:"id"` 20 | Method string `json:"method"` 21 | Params json.RawMessage `json:"params,omitempty"` 22 | } 23 | 24 | type ResponseMessage struct { 25 | Message 26 | ID any `json:"id"` 27 | Result json.RawMessage `json:"result,omitempty"` 28 | Error *ResponseError `json:"error,omitempty"` 29 | } 30 | 31 | type ResponseError struct { 32 | Code int `json:"code"` 33 | Message string `json:"message"` 34 | Data json.RawMessage `json:"data,omitempty"` 35 | } 36 | 37 | type NotificationMessage struct { 38 | Message 39 | Method string `json:"method"` 40 | Params json.RawMessage `json:"params,omitempty"` 41 | } 42 | -------------------------------------------------------------------------------- /test/completion_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/carn181/faustlsp/logging" 7 | "github.com/carn181/faustlsp/server" 8 | "github.com/carn181/faustlsp/transport" 9 | ) 10 | 11 | func TestFindCompletionReplaceRange(t *testing.T) { 12 | logging.Init() 13 | 14 | tests := []struct { 15 | name string 16 | text string 17 | position transport.Position 18 | encoding string 19 | want transport.Range 20 | }{ 21 | { 22 | name: "Simple prefix", 23 | text: `import("stdfaust.lib"); 24 | foo = 1;`, 25 | position: transport.Position{Line: 1, Character: 2}, 26 | encoding: "utf-8", 27 | want: transport.Range{ 28 | Start: transport.Position{Line: 1, Character: 0}, 29 | End: transport.Position{Line: 1, Character: 2}, 30 | }, 31 | }, 32 | } 33 | 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := server.FindCompletionReplaceRange(tt.position, tt.text, tt.encoding); got != tt.want { 37 | t.Errorf("FindCompletionReplaceRange() = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/uri_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/carn181/faustlsp/util" 9 | ) 10 | 11 | func TestUri2path(t *testing.T) { 12 | 13 | if runtime.GOOS != "windows" { 14 | path, _ := util.URI2path("file:///home/user/a.dsp") 15 | if !(path == "/home/user/a.dsp") { 16 | t.Fatalf("Invalid Unix Path %s\n", path) 17 | } 18 | } else { 19 | path, _ := util.URI2path("file:///C:/users/user/file.txt") 20 | if !(path == "C:\\users\\user\\file.txt") { 21 | t.Fatalf("Invalid Windows Path %s\n", path) 22 | } 23 | } 24 | } 25 | 26 | func TestPath2Uri(t *testing.T) { 27 | if runtime.GOOS == "windows" { 28 | path := "C:\\user\\a.dsp" 29 | uri := util.Path2URI(path) 30 | expected_uri := "file:///C:/user/a.dsp" 31 | if !(uri == expected_uri) { 32 | t.Fatalf("Invalid URI %s\n", uri) 33 | } 34 | } else { 35 | path := "/home/user/a.dsp" 36 | uri := util.Path2URI(path) 37 | expected_uri := "file:///home/user/a.dsp" 38 | if !(uri == expected_uri) { 39 | t.Fatalf("Invalid URI %s\n", uri) 40 | } 41 | } 42 | } 43 | 44 | func TestIsWindowsPath(t *testing.T) { 45 | paths := []string{"/home/user/a.dsp", "C:\\Program\\a"} 46 | for _, path := range paths { 47 | fmt.Print(path) 48 | fmt.Printf(" Is Windows: %t\n", util.IsWindowsDrivePath(path)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /util/handle.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/url" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | type Path = string 12 | type URI = string 13 | 14 | type Handle struct { 15 | URI URI 16 | Path Path 17 | } 18 | 19 | func FromPath(path string) Handle { 20 | return Handle{Path2URI(path), path} 21 | } 22 | 23 | func FromURI(uri string) (Handle, error) { 24 | path, err := URI2path(uri) 25 | return Handle{uri, path}, err 26 | } 27 | 28 | // Converting functions 29 | 30 | func URI2path(uri string) (string, error) { 31 | url, err := url.Parse(uri) 32 | if err != nil { 33 | return "", err 34 | } 35 | // url.Path 36 | if IsWindowsDriveURIPath(url.Path) { 37 | url.Path = strings.ToUpper(string(url.Path[1])) + url.Path[2:] 38 | } 39 | return filepath.FromSlash(url.Path), nil 40 | } 41 | 42 | func Path2URI(path string) URI { 43 | scheme := "file://" 44 | if runtime.GOOS == "windows" { 45 | path = "/" + strings.Replace(path, "\\", "/", -1) 46 | } 47 | return scheme + path 48 | } 49 | 50 | func IsWindowsDriveURIPath(uri string) bool { 51 | if len(uri) < 4 { 52 | return false 53 | } 54 | return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' 55 | } 56 | 57 | func IsWindowsDrivePath(path string) bool { 58 | if len(path) < 3 { 59 | return false 60 | } 61 | return unicode.IsLetter(rune(path[0])) && path[1] == ':' 62 | } 63 | -------------------------------------------------------------------------------- /test/lifecycle_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/carn181/faustlsp/logging" 8 | "github.com/carn181/faustlsp/server" 9 | "github.com/carn181/faustlsp/transport" 10 | 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestExitWithoutError(t *testing.T) { 16 | logging.Init() 17 | logging.Logger.Info("Starting") 18 | var s server.Server 19 | 20 | runserver := func() error { 21 | s.Init(transport.Socket) 22 | err := s.Run(context.Background()) 23 | s.Transport.Close() 24 | return err 25 | } 26 | 27 | ctr := 0 28 | 29 | go func() { 30 | var tr transport.Transport 31 | tr.Init(transport.Client, transport.Socket) 32 | msg, _ := json.Marshal(transport.ParamInitialize{ 33 | XInitializeParams: transport.XInitializeParams{ 34 | RootPath: "", 35 | }, 36 | }) 37 | tr.WriteRequest(ctr, "initialize", msg) 38 | tr.Read() 39 | tr.WriteRequest(ctr+1, "shutdown", msg) 40 | tr.WriteNotif("exit", msg) 41 | time.Sleep(1 * time.Second) 42 | tr.Close() 43 | 44 | }() 45 | err := runserver() 46 | if err != nil { 47 | t.Errorf("Exit was not graceful, when it should've been") 48 | } 49 | } 50 | 51 | func TestExitWithError(t *testing.T) { 52 | logging.Init() 53 | logging.Logger.Info("Starting") 54 | 55 | var s server.Server 56 | ctx, cancel := context.WithCancel(context.Background()) 57 | runserver := func() error { 58 | s.Init(transport.Socket) 59 | err := s.Run(ctx) 60 | return err 61 | } 62 | 63 | ctr := 0 64 | 65 | go func() { 66 | var tr transport.Transport 67 | tr.Init(transport.Client, transport.Socket) 68 | msg, _ := json.Marshal(transport.ParamInitialize{ 69 | XInitializeParams: transport.XInitializeParams{ 70 | RootPath: "", 71 | }, 72 | }) 73 | tr.WriteRequest(ctr, "initialize", msg) 74 | tr.Read() 75 | tr.WriteNotif("exit", msg) 76 | time.Sleep(1 * time.Second) 77 | tr.Close() 78 | cancel() 79 | 80 | }() 81 | err := runserver() 82 | if err.Error() != "Exiting Ungracefully" { 83 | t.Errorf("Exit should not have been graceful") 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # faustlsp 2 | 3 | A LSP Server for the Faust programming language. 4 | 5 | # Installation 6 | 7 | To install, run 8 | ```sh 9 | go get github.com/carn181/faustlsp@latest 10 | ``` 11 | 12 | This will install a `faustlsp` executable in `$HOME/go/bin` by default. 13 | 14 | 15 | Alternatively, you can clone this repository, build and install faustlsp. 16 | 17 | ```sh 18 | git clone https://github.com/carn181/faustlsp 19 | cd faustlsp 20 | go build 21 | go install 22 | ``` 23 | 24 | For code formatting, install [faustfmt](https://github.com/carn181/faustfmt) following install instructions in the project's README. 25 | 26 | # Usage 27 | 28 | ## VS Code 29 | 30 | [vscode-faust](https://github.com/carn181/vscode-faust) is a VS Code extension for Faust that works with faustlsp. Follow installation steps in the README.md 31 | 32 | ## Neovim 33 | 34 | Sample nvim-lspconfig configuration which requires a .faustcfg.json in the faust project root directory: 35 | ```lua 36 | vim.lsp.config('faustlsp', { 37 | cmd = { 'faustlsp' }, 38 | filetypes = {'faust'}, 39 | workspace_required = true, 40 | root_markers = { '.faustcfg.json' } 41 | }) 42 | ``` 43 | 44 | ## Emacs 45 | 46 | Sample lsp-mode server config 47 | ```lisp 48 | (lsp-register-client 49 | (make-lsp-client 50 | :new-connection (lsp-stdio-connection "faustlsp") 51 | :activation-fn (lsp-activate-on "faust") 52 | :server-id 'faustlsp 53 | )) 54 | ``` 55 | 56 | 57 | # Features 58 | 59 | - [x] Document Synchronization 60 | - [x] Diagnostics 61 | - [x] Syntax Errors 62 | - [x] Compiler Errors (can disable in .faustcfg.json as they look ugly due to compiler limitations) 63 | - [x] Hover Documentation 64 | - [x] Code Completion 65 | - [x] Document Symbols 66 | - [x] Formatting (using [faustfmt](https://github.com/carn181/faustfmt)) 67 | - [x] Goto Definition 68 | - [ ] Find References 69 | 70 | # Configuration 71 | 72 | You can configure the LSP server and it give it information about the project using a `.faustcfg.json` file defined in a project's root directory. 73 | Configuration Options: 74 | ```js 75 | { 76 | "command": "faust", // Faust Compiler Executable to use 77 | "process_name": "process", // Process Name passed as -pn to compiler 78 | "process_files": ["a.dsp"], // Files that have top-level processes defined 79 | "compiler_diagnostics": true // Show Compiler Errors 80 | } 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /test/parser_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "log/slog" 5 | "slices" 6 | "testing" 7 | 8 | "github.com/carn181/faustlsp/logging" 9 | "github.com/carn181/faustlsp/parser" 10 | "github.com/carn181/faustlsp/server" 11 | "github.com/carn181/faustlsp/transport" 12 | "github.com/carn181/faustlsp/util" 13 | ) 14 | 15 | func testParseImports(t *testing.T) { 16 | parser.Init() 17 | code := []byte(` 18 | import("a.lib"); 19 | import("s.dsp"); 20 | import("c.dsp"); 21 | `) 22 | tree := parser.ParseTree(code) 23 | rslts := parser.GetImports(code, tree) 24 | expected := []string{"a.lib", "s.dsp", "c.dsp"} 25 | if !slices.Equal(rslts, expected) { 26 | t.Error(parser.GetImports(code, tree)) 27 | } 28 | } 29 | 30 | func testParseASTNode(t *testing.T) { 31 | logging.Logger = slog.Default() 32 | parser.Init() 33 | code := ` 34 | 35 | import("test.dsp"); 36 | d = library("another.dsp"); 37 | 38 | a = 1; 39 | b = 2; 40 | 41 | c = i with { i = 2; j = 2;}; 42 | 43 | d = par(i, 4, c); 44 | 45 | g = case{(x:y) => y:x; (x) => x;} 46 | ` 47 | tree := parser.ParseTree([]byte(code)) 48 | defer tree.Close() 49 | 50 | root := tree.RootNode() 51 | 52 | s := server.Server{} 53 | s.Workspace = server.Workspace{} 54 | s.Workspace.Root = "./test-project" 55 | s.Workspace.Config = server.FaustProjectConfig{ 56 | Command: "faustlsp", 57 | } 58 | 59 | file := server.File{ 60 | Content: []byte(code), 61 | Handle: util.FromPath("test.dsp"), 62 | } 63 | s.Workspace.ParseASTNode(root, &file, nil, nil, nil, nil) 64 | } 65 | 66 | func TestRangeContains(t *testing.T) { 67 | tests := []struct { 68 | name string 69 | parent transport.Range 70 | child transport.Range 71 | want bool 72 | }{ 73 | { 74 | name: "Child has greater char range than parent", 75 | parent: transport.Range{Start: transport.Position{Line: 0, Character: 0}, End: transport.Position{Line: 2, Character: 0}}, 76 | child: transport.Range{Start: transport.Position{Line: 1, Character: 0}, End: transport.Position{Line: 1, Character: 17}}, 77 | want: true, 78 | }, 79 | { 80 | name: "Child has greater char range than parent", 81 | parent: transport.Range{Start: transport.Position{Line: 1, Character: 2}, End: transport.Position{Line: 1, Character: 13}}, 82 | child: transport.Range{Start: transport.Position{Line: 6, Character: 18}, End: transport.Position{Line: 6, Character: 19}}, 83 | want: false, 84 | }, 85 | } 86 | 87 | for _, tt := range tests { 88 | t.Run(tt.name, func(t *testing.T) { 89 | got := server.RangeContains(tt.parent, tt.child) 90 | if got != tt.want { 91 | t.Errorf("RangeContains() = %v, want %v", got, tt.want) 92 | } 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /server/completion.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "unicode" 7 | 8 | "github.com/carn181/faustlsp/logging" 9 | "github.com/carn181/faustlsp/transport" 10 | "github.com/carn181/faustlsp/util" 11 | ) 12 | 13 | func Completion(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 14 | logging.Logger.Info("Got Completion Request", "request", string(par)) 15 | 16 | var params transport.CompletionParams 17 | json.Unmarshal(par, ¶ms) 18 | 19 | handle, err := util.FromURI(string(params.TextDocument.URI)) 20 | if err != nil { 21 | return []byte("null"), err 22 | } 23 | results := GetPossibleSymbols(params.Position, handle.Path, &s.Store, string(s.Files.encoding)) 24 | 25 | replaceRange := transport.Range{} 26 | f, ok := s.Files.Get(handle) 27 | if ok { 28 | f.mu.RLock() 29 | replaceRange = FindCompletionReplaceRange(params.Position, string(f.Content), string(s.Files.encoding)) 30 | logging.Logger.Info("Replace Range", "range", replaceRange) 31 | f.mu.RUnlock() 32 | } 33 | var items = []transport.CompletionItem{} 34 | plainText := transport.PlainTextTextFormat 35 | for _, sym := range results { 36 | items = append(items, transport.CompletionItem{ 37 | Label: sym.name, 38 | Kind: transport.VariableCompletion, 39 | // InsertText: sym.name, 40 | InsertTextFormat: &plainText, 41 | TextEdit: transport.TextEdit{ 42 | NewText: sym.name, 43 | Range: replaceRange, 44 | }, 45 | 46 | // Documentation: &transport.Or_CompletionItem_documentation{ 47 | // Value: transport.MarkupContent{ 48 | // Kind: "plaintext", 49 | // Value: sym.docs.Full, 50 | // }, 51 | // }, 52 | // Detail: sym.docs.Usage, 53 | }) 54 | } 55 | 56 | logging.Logger.Info("Completion results", "results", items) 57 | 58 | resp, err := json.Marshal(items) 59 | if err != nil { 60 | return []byte("null"), err 61 | } 62 | return resp, nil 63 | } 64 | 65 | func FindCompletionReplaceRange(pos transport.Position, content, encoding string) transport.Range { 66 | 67 | offset, err := PositionToOffset(pos, content, encoding) 68 | if err != nil { 69 | return transport.Range{} 70 | } 71 | start, end := offset, offset 72 | for { 73 | logging.Logger.Info("Finding start", "start", start, "char", string(content[start])) 74 | if start <= 0 { 75 | break 76 | } 77 | if !unicode.IsLetter(rune(content[start-1])) && !unicode.IsDigit(rune(content[start-1])) { 78 | break 79 | } 80 | start-- 81 | } 82 | startPos, err := OffsetToPosition(start, content, encoding) 83 | endPos, err := OffsetToPosition(end, content, encoding) 84 | return transport.Range{ 85 | Start: startPos, 86 | End: endPos, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /server/formatting.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "os/exec" 10 | "strings" 11 | 12 | "github.com/carn181/faustlsp/logging" 13 | "github.com/carn181/faustlsp/transport" 14 | "github.com/carn181/faustlsp/util" 15 | ) 16 | 17 | func Format(content []byte, indent string) ([]byte, error) { 18 | // TODO: Allow to take faustExec and customQueryFile from config file 19 | faustExec := "faustfmt" 20 | 21 | // Check if formatter exists in path 22 | _, err := exec.LookPath(faustExec) 23 | if err != nil { 24 | return []byte{}, errors.New("Couldn't find " + faustExec + " in PATH") 25 | } 26 | 27 | // Setup faustfmt command with input 28 | var errs strings.Builder 29 | var output bytes.Buffer 30 | cmd := exec.Command(faustExec, "-i", indent) 31 | cmd.Stdin = bytes.NewBuffer(content) 32 | cmd.Stderr = &errs 33 | cmd.Stdout = &output 34 | 35 | // Run faustfmt command 36 | err = cmd.Run() 37 | if err != nil { 38 | return []byte{}, fmt.Errorf("faustfmt error: %s, Stderr: %s", err, errs.String()) 39 | } 40 | 41 | return output.Bytes(), nil 42 | } 43 | 44 | func GetIndent(par transport.DocumentFormattingParams) string { 45 | if par.Options.InsertSpaces { 46 | s := "" 47 | for range par.Options.TabSize { 48 | s += " " 49 | } 50 | return s 51 | } else { 52 | return "\t" 53 | } 54 | } 55 | 56 | func Formatting(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 57 | var params transport.DocumentFormattingParams 58 | json.Unmarshal(par, ¶ms) 59 | 60 | logging.Logger.Info("Formatting request", "params", string(par)) 61 | path, err := util.URI2path(string(params.TextDocument.URI)) 62 | if err != nil { 63 | logging.Logger.Error("Uri2path error", "error", err) 64 | } 65 | 66 | f, ok := s.Files.GetFromPath(path) 67 | content := f.Content 68 | var output []byte 69 | if ok { 70 | output, err = Format(content, GetIndent(params)) 71 | if err != nil { 72 | logging.Logger.Error("Format error", "error", err) 73 | } 74 | } 75 | logging.Logger.Info("Got this for formatting", "output", string(output)) 76 | 77 | endPos := transport.Position{Line: 0, Character: 0} 78 | if ok { 79 | endPos, err = getDocumentEndPosition(string(content), string(s.Files.encoding)) 80 | if err != nil { 81 | logging.Logger.Error("OffsetToPosition error", "error", err) 82 | endPos = transport.Position{Line: 0, Character: 0} 83 | } 84 | } 85 | 86 | edit := transport.TextEdit{ 87 | Range: transport.Range{ 88 | Start: transport.Position{Line: 0, Character: 0}, 89 | End: endPos, 90 | }, 91 | NewText: string(output), 92 | } 93 | resultBytes, err := json.Marshal([]transport.TextEdit{edit}) 94 | 95 | return resultBytes, err 96 | } 97 | -------------------------------------------------------------------------------- /server/synchronization.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/carn181/faustlsp/logging" 8 | "github.com/carn181/faustlsp/transport" 9 | "github.com/carn181/faustlsp/util" 10 | ) 11 | 12 | type TDChangeType int 13 | 14 | const ( 15 | TDOpen = iota 16 | TDChange 17 | TDClose 18 | ) 19 | 20 | type TDEvent struct { 21 | Type TDChangeType 22 | Path util.Path 23 | } 24 | 25 | func TextDocumentOpen(ctx context.Context, s *Server, par json.RawMessage) error { 26 | var params transport.DidOpenTextDocumentParams 27 | json.Unmarshal(par, ¶ms) 28 | 29 | fileURI := params.TextDocument.URI 30 | 31 | // Open File 32 | s.Workspace.EditorOpenFile(util.URI(fileURI), &s.Files) 33 | 34 | logging.Logger.Info("Opening File", "uri", string(fileURI)) 35 | f, ok := s.Files.GetFromURI(util.URI(fileURI)) 36 | 37 | if !ok { 38 | s.Files.AddFromURI(util.URI(fileURI), []byte{}) 39 | f, _ = s.Files.GetFromURI(util.URI(fileURI)) 40 | } 41 | 42 | f.mu.RLock() 43 | logging.Logger.Info("Current File", "content", f.Content) 44 | 45 | s.Workspace.TDEvents <- TDEvent{Type: TDOpen, Path: f.Handle.Path} 46 | f.mu.RUnlock() 47 | 48 | // go s.Workspace.AnalyzeFile(f, &s.Store) 49 | go s.Workspace.DiagnoseFile(f.Handle.Path, s) 50 | 51 | return nil 52 | } 53 | 54 | func TextDocumentChangeFull(ctx context.Context, s *Server, par json.RawMessage) error { 55 | var params transport.DidChangeTextDocumentParams 56 | json.Unmarshal(par, ¶ms) 57 | 58 | fileURI := params.TextDocument.URI 59 | 60 | path, err := util.URI2path(string(fileURI)) 61 | if err != nil { 62 | return err 63 | } 64 | for _, change := range params.ContentChanges { 65 | s.Files.ModifyFull(path, change.Text) 66 | } 67 | s.Workspace.TDEvents <- TDEvent{Type: TDChange, Path: path} 68 | 69 | logging.Logger.Info("Modified File", "fileURI", string(fileURI)) 70 | return nil 71 | } 72 | 73 | func TextDocumentChangeIncremental(ctx context.Context, s *Server, par json.RawMessage) error { 74 | var params transport.DidChangeTextDocumentParams 75 | json.Unmarshal(par, ¶ms) 76 | logging.Logger.Info("TextDocumentChangeIncremental", "params", string(par)) 77 | fileURI := params.TextDocument.URI 78 | 79 | path, err := util.URI2path(string(fileURI)) 80 | if err != nil { 81 | return err 82 | } 83 | for _, change := range params.ContentChanges { 84 | s.Files.ModifyIncremental(path, *change.Range, change.Text) 85 | } 86 | 87 | s.Workspace.TDEvents <- TDEvent{Type: TDChange, Path: path} 88 | 89 | return nil 90 | } 91 | 92 | func TextDocumentClose(ctx context.Context, s *Server, par json.RawMessage) error { 93 | var params transport.DidCloseTextDocumentParams 94 | json.Unmarshal(par, ¶ms) 95 | 96 | fileURI := params.TextDocument.URI 97 | 98 | s.Files.CloseFromURI(util.Path(params.TextDocument.URI)) 99 | 100 | path, err := util.URI2path(string(fileURI)) 101 | logging.Logger.Error("Got error when getting path from URI", "error", err) 102 | s.Workspace.TDEvents <- TDEvent{Type: TDClose, Path: path} 103 | 104 | logging.Logger.Info("Closed File", "uri", string(fileURI)) 105 | // logging.Logger.Printf("Current Files: %s\n", s.Files) 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /server/incremental.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "unicode/utf8" 6 | 7 | "github.com/carn181/faustlsp/transport" 8 | ) 9 | 10 | func ApplyIncrementalChange(r transport.Range, newContent string, content string, encoding string) string { 11 | start, _ := PositionToOffset(r.Start, content, encoding) 12 | end, _ := PositionToOffset(r.End, content, encoding) 13 | // logging.Logger.Printf("Start: %d, End: %d\n", start, end) 14 | return content[:start] + newContent + content[end:] 15 | } 16 | 17 | func PositionToOffset(pos transport.Position, s string, encoding string) (uint, error) { 18 | if len(s) == 0 { 19 | return 0, nil 20 | } 21 | indices := GetLineIndices(s) 22 | if pos.Line > uint32(len(indices)) { 23 | return 0, fmt.Errorf("invalid Line Number") 24 | } else if pos.Line == uint32(len(indices)) { 25 | return uint(len(s)), nil 26 | } 27 | currChar := indices[pos.Line] 28 | for i := 0; i < int(pos.Character); i++ { 29 | if int(currChar) >= len(s) { 30 | break // Prevent reading past end of string 31 | } 32 | r, w := utf8.DecodeRuneInString(s[currChar:]) 33 | if w == 0 { 34 | break // Prevent infinite loop if decoding fails 35 | } 36 | currChar += uint(w) 37 | if encoding == "utf-16" { 38 | if r >= 0x10000 { 39 | i++ 40 | if i == int(pos.Character) { 41 | break 42 | } 43 | } 44 | } 45 | } 46 | return currChar, nil 47 | } 48 | 49 | func OffsetToPosition(offset uint, s string, encoding string) (transport.Position, error) { 50 | if len(s) == 0 || offset == 0 { 51 | return transport.Position{Line: 0, Character: 0}, nil 52 | } 53 | line := uint32(0) 54 | char := uint32(0) 55 | str := []byte(s) 56 | 57 | for i := uint(0); i < offset && i < uint(len(str)); { 58 | r, w := utf8.DecodeRune(str[i:]) 59 | if w == 0 { 60 | break // Prevent infinite loop if decoding fails 61 | } 62 | if r == '\n' { 63 | line++ 64 | char = 0 65 | } else { 66 | char++ 67 | if r >= 0x10000 && encoding == "utf-16" { 68 | char++ 69 | } 70 | } 71 | i += uint(w) 72 | } 73 | 74 | return transport.Position{Line: line, Character: char}, nil 75 | } 76 | 77 | func GetLineIndices(s string) []uint { 78 | // logging.Logger.Printf("Got %s\n", s) 79 | lines := []uint{0} 80 | i := 0 81 | for w := 0; i < len(s); i += w { 82 | runeValue, width := utf8.DecodeRuneInString(s[i:]) 83 | if runeValue == '\n' { 84 | lines = append(lines, uint(i)+1) 85 | } 86 | w = width 87 | } 88 | return lines 89 | } 90 | 91 | func getDocumentEndOffset(s string, encoding string) uint { 92 | switch encoding { 93 | case "utf-8": 94 | return uint(len(s)) 95 | case "utf-16": 96 | offset := uint(0) 97 | for _, r := range s { 98 | if r >= 0x10000 { 99 | offset += 2 100 | } else { 101 | offset += 1 102 | } 103 | } 104 | return offset 105 | case "utf-32": 106 | // Each rune is one code unit in utf-32 107 | return uint(len([]rune(s))) 108 | default: 109 | // Fallback to utf-8 110 | return uint(len(s)) 111 | } 112 | } 113 | 114 | func getDocumentEndPosition(s string, encoding string) (transport.Position, error) { 115 | offset := getDocumentEndOffset(s, encoding) 116 | pos, err := OffsetToPosition(offset, s, encoding) 117 | return pos, err 118 | } 119 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "path/filepath" 6 | 7 | "github.com/carn181/faustlsp/logging" 8 | "github.com/carn181/faustlsp/transport" 9 | "github.com/carn181/faustlsp/util" 10 | ) 11 | 12 | type FaustProjectConfig struct { 13 | Command string `json:"command,omitempty"` 14 | Type string `json:"type"` // Actually make this enum between Process or Library eventually 15 | ProcessName string `json:"process_name,omitempty"` 16 | ProcessFiles []util.Path `json:"process_files,omitempty"` 17 | IncludeDir []util.Path `json:"include,omitempty"` 18 | CompilerDiagnostics bool `json:"compiler_diagnostics,omitempty"` 19 | } 20 | 21 | func (w *Workspace) Rel2Abs(relPath string) util.Path { 22 | return filepath.Join(w.Root, relPath) 23 | } 24 | 25 | func (w *Workspace) cleanDiagnostics(s *Server) { 26 | for _, path := range w.Files { 27 | f, _ := s.Files.GetFromPath(path) 28 | f.mu.RLock() 29 | path := f.Handle.Path 30 | f.mu.RUnlock() 31 | if IsFaustFile(path) { 32 | w.DiagnoseFile(path, s) 33 | } 34 | } 35 | } 36 | 37 | func (w *Workspace) sendCompilerDiagnostics(s *Server) { 38 | for _, filePath := range w.Config.ProcessFiles { 39 | path := filepath.Join(w.Root, filePath) 40 | f, ok := s.Files.GetFromPath(path) 41 | 42 | if ok { 43 | f.mu.RLock() 44 | tempPath := w.TempDirPath(f.Handle.Path) 45 | logging.Logger.Info("Generating Compiler Diagnostics", "temp_path", tempPath) 46 | f.mu.RUnlock() 47 | if !f.hasSyntaxErrors { 48 | var diagnosticErrors = []transport.Diagnostic{} 49 | uri := util.Path2URI(path) 50 | logging.Logger.Info("Generating Compiler Diagnostics", "temp_path", tempPath) 51 | diagnosticError := getCompilerDiagnostics(tempPath, w.Root, w.Config) 52 | if diagnosticError.Message != "" { 53 | diagnosticErrors = []transport.Diagnostic{diagnosticError} 54 | } 55 | d := transport.PublishDiagnosticsParams{ 56 | URI: transport.DocumentURI(uri), 57 | Diagnostics: diagnosticErrors, 58 | } 59 | s.diagChan <- d 60 | } 61 | } 62 | } 63 | } 64 | 65 | func (c *FaustProjectConfig) UnmarshalJSON(content []byte) error { 66 | type Config FaustProjectConfig 67 | var cfg = Config{ 68 | Command: "faust", 69 | ProcessName: "process", 70 | CompilerDiagnostics: true, 71 | } 72 | if err := json.Unmarshal(content, &cfg); err != nil { 73 | logging.Logger.Error("Failed to unmarshal FaustProjectConfig", "error", err) 74 | return err 75 | } 76 | *c = FaustProjectConfig(cfg) 77 | return nil 78 | } 79 | 80 | func (w *Workspace) parseConfig(content []byte) (FaustProjectConfig, error) { 81 | var config FaustProjectConfig 82 | err := json.Unmarshal(content, &config) 83 | if err != nil { 84 | logging.Logger.Error("Invalid Project Config file", "error", err) 85 | return FaustProjectConfig{}, err 86 | } 87 | // If no process files provided, all .dsp files become process 88 | if len(config.ProcessFiles) == 0 { 89 | config.ProcessFiles = w.getFaustDSPRelativePaths() 90 | } 91 | return config, nil 92 | } 93 | 94 | func (w *Workspace) defaultConfig() FaustProjectConfig { 95 | logging.Logger.Info("Using default config file") 96 | var config = FaustProjectConfig{ 97 | Command: "faust", 98 | Type: "process", 99 | ProcessFiles: w.getFaustDSPRelativePaths(), 100 | CompilerDiagnostics: true, 101 | } 102 | return config 103 | } 104 | 105 | func (w *Workspace) getFaustDSPRelativePaths() []util.Path { 106 | var filePaths = []util.Path{} 107 | for _, file := range w.Files { 108 | if IsDSPFile(file) { 109 | filePaths = append(filePaths, file) 110 | } 111 | } 112 | return filePaths 113 | } 114 | -------------------------------------------------------------------------------- /server/lifecycle.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/json" 7 | "os" 8 | 9 | "github.com/carn181/faustlsp/logging" 10 | "github.com/carn181/faustlsp/transport" 11 | "github.com/carn181/faustlsp/util" 12 | ) 13 | 14 | // Initialize Handler 15 | func Initialize(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 16 | // TODO: Error Handling 17 | 18 | s.Status = Initializing 19 | var params transport.InitializeParams 20 | json.Unmarshal(par, ¶ms) 21 | logging.Logger.Info("Got Initialize Parameters from Client", "params", par) 22 | 23 | // TODO: Choose ServerCapabilities based on ClientCapabilities 24 | // Server Capabilities 25 | 26 | // Don't select UTF-8, select UTF-32 and UTF-16 only 27 | var positionEncoding transport.PositionEncodingKind 28 | if params.Capabilities.General.PositionEncodings[0] == "utf-16" { 29 | positionEncoding = transport.UTF16 30 | } else if params.Capabilities.General.PositionEncodings[0] == "utf-32" { 31 | positionEncoding = transport.UTF32 32 | } else { 33 | positionEncoding = transport.UTF16 34 | } 35 | var result transport.InitializeResult = transport.InitializeResult{ 36 | Capabilities: transport.ServerCapabilities{ 37 | // TODO: Implement Incremental Changes for better synchronization 38 | DocumentSymbolProvider: &transport.Or_ServerCapabilities_documentSymbolProvider{Value: true}, 39 | PositionEncoding: &positionEncoding, 40 | TextDocumentSync: transport.Incremental, 41 | Workspace: &transport.WorkspaceOptions{ 42 | WorkspaceFolders: &transport.WorkspaceFolders5Gn{ 43 | Supported: true, 44 | ChangeNotifications: "ws", 45 | }, 46 | }, 47 | DocumentFormattingProvider: &transport.Or_ServerCapabilities_documentFormattingProvider{Value: true}, 48 | DefinitionProvider: &transport.Or_ServerCapabilities_definitionProvider{Value: true}, 49 | HoverProvider: &transport.Or_ServerCapabilities_hoverProvider{Value: true}, 50 | CompletionProvider: &transport.CompletionOptions{ 51 | TriggerCharacters: []string{"."}, 52 | }, 53 | }, 54 | ServerInfo: &transport.ServerInfo{Name: "faust-lsp", Version: "0.0.1"}, 55 | } 56 | s.Capabilities = result.Capabilities 57 | 58 | rootPath, _ := util.URI2path(string(params.RootURI)) 59 | logging.Logger.Info("Got workspace", "workspace", rootPath) 60 | s.Workspace.Root = rootPath 61 | 62 | resultBytes, err := json.Marshal(result) 63 | if err != nil { 64 | return []byte{}, nil 65 | } 66 | return resultBytes, err 67 | } 68 | 69 | // Initialized Handler 70 | func Initialized(ctx context.Context, s *Server, par json.RawMessage) error { 71 | 72 | s.Status = Running 73 | go s.GenerateDiagnostics() 74 | s.Files.Init(ctx, *s.Capabilities.PositionEncoding) 75 | s.Store.Files = &s.Files 76 | s.Store.Dependencies = NewDependencyGraph() 77 | s.Store.Cache = make(map[[sha256.Size]byte]*Scope) 78 | s.Workspace.Init(ctx, s) 79 | logging.Logger.Info("Handling Initialized with diagnostics") 80 | logging.Logger.Info("Started Diagnostic Handler") 81 | // Send WorkspaceFolders Request 82 | // TODO: Do this only if server-client agreed on workspacefolders 83 | // err := s.Transport.WriteRequest(s.reqIdCtr,"workspace/workspaceFolders", []byte{}) 84 | // if err != nil { 85 | // logging.Logger.Fatal(err) 86 | // } 87 | // s.reqIdCtr+=1 88 | return nil 89 | } 90 | 91 | // Shutdown Handler 92 | func ShutdownEnd(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 93 | s.Status = Shutdown 94 | // Some Clients end the server right after sending shutdown like emacs lsp-mode 95 | // Remove Temp Dir just in case 96 | os.RemoveAll(s.tempDir) 97 | 98 | content, err := json.Marshal([]byte("")) 99 | return content, err 100 | } 101 | 102 | // Exit Handler 103 | func ExitEnd(ctx context.Context, s *Server, par json.RawMessage) error { 104 | if s.Status == Shutdown { 105 | s.Status = Exit 106 | } else { 107 | s.Status = ExitError 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /server/compiler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "os/exec" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/carn181/faustlsp/logging" 10 | "github.com/carn181/faustlsp/transport" 11 | ) 12 | 13 | type FaustError struct { 14 | File string 15 | Line int 16 | Message string 17 | } 18 | 19 | type FaustErrorReportingType uint 20 | 21 | const ( 22 | FileError = iota 23 | Error 24 | NullError 25 | ) 26 | 27 | var FaustErrorName = map[FaustErrorReportingType]string{ 28 | FileError: "File Error", 29 | Error: "Error", 30 | NullError: "Unrecognized Error", 31 | } 32 | 33 | func (fe FaustErrorReportingType) String() string { 34 | return FaustErrorName[fe] 35 | } 36 | 37 | func getFaustErrorReportingType(s string) FaustErrorReportingType { 38 | if len(s) < 5 { 39 | return NullError 40 | } 41 | errorstr := s[:5] 42 | if errorstr == "ERROR" || errorstr == "Error" { 43 | return Error 44 | } 45 | return FileError 46 | } 47 | 48 | // TODO: When handling initialize, send diagnostics capability based on whether PATH has faust or some other compiler path provided by project configuration 49 | func getCompilerDiagnostics(path string, dirPath string, cfg FaustProjectConfig) transport.Diagnostic { 50 | cmd := exec.Command(cfg.Command, path, "-pn", cfg.ProcessName) 51 | if dirPath != "" { 52 | cmd.Dir = dirPath 53 | } 54 | var errors strings.Builder 55 | cmd.Stderr = &errors 56 | err := cmd.Run() 57 | faustErrors := errors.String() 58 | logging.Logger.Info("Return code of faust compiler", "error", err) 59 | if err == nil { 60 | return transport.Diagnostic{} 61 | } 62 | 63 | errorType := getFaustErrorReportingType(faustErrors) 64 | logging.Logger.Info("Got error from compiler", "path", path, "type", errorType, "output", faustErrors) 65 | 66 | switch errorType { 67 | case FileError: 68 | error := parseFileError(errors.String()) 69 | logging.Logger.Info("FileError", "error", error) 70 | if error.Line > 0 { 71 | error.Line -= 1 72 | } 73 | if error.Line == -1 { 74 | error.Line = 0 75 | } 76 | return transport.Diagnostic{ 77 | Range: transport.Range{ 78 | Start: transport.Position{ 79 | // Lines must be zero-indexed 80 | Line: uint32(error.Line), 81 | Character: 0, 82 | }, 83 | End: transport.Position{ 84 | Line: uint32(error.Line), 85 | // TODO: Actually calculate end of line 86 | Character: 2147483647, 87 | }, 88 | }, 89 | Message: error.Message, 90 | Severity: transport.DiagnosticSeverity(transport.Error), 91 | Source: "faust", 92 | } 93 | case Error: 94 | error := parseError(errors.String()) 95 | logging.Logger.Info("Error", "error", error) 96 | return transport.Diagnostic{ 97 | Range: transport.Range{}, 98 | Message: error.Message, 99 | Severity: transport.DiagnosticSeverity(transport.Error), 100 | Source: "faust", 101 | } 102 | case NullError: 103 | logging.Logger.Info("Unrecognized Error") 104 | return transport.Diagnostic{} 105 | default: 106 | return transport.Diagnostic{} 107 | } 108 | } 109 | 110 | func parseFileError(s string) FaustError { 111 | 112 | // Previous 113 | // re := regexp.MustCompile(`(?s)(.+):\s*([-\d]+)\s:\sERROR\s:\s(.*)`) 114 | // Problem: Couldn't handle a.dsp:8 ERROR : redefinition of symbols are not allowed : process due to missing colon after the line number 115 | re := regexp.MustCompile(`(?s)(.+):\s*([-\d]+)[\s:]*\sERROR\s:\s(.*)`) 116 | captures := re.FindStringSubmatch(s) 117 | if len(captures) < 4 { 118 | logging.Logger.Error("Compiler Output Regex error: Expected 4 values in parseFileError", "captures", captures) 119 | } 120 | line, _ := strconv.Atoi(captures[2]) 121 | return FaustError{File: captures[1], Line: line, Message: captures[3]} 122 | } 123 | 124 | func parseError(s string) FaustError { 125 | re := regexp.MustCompile(`(?s)ERROR\s:\s(.*)`) 126 | captures := re.FindStringSubmatch(s) 127 | if len(captures) < 2 { 128 | logging.Logger.Error("Compiler Output Regex error: Expected 2 values in parseError", "captures", captures) 129 | } 130 | return FaustError{Message: captures[1]} 131 | } 132 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/carn181/fsnotify v0.0.0-20250612182652-935ca6b92412 h1:TiwX7yYt063HeLRClSrIkql3OYXl5efPGQwxQi1oK+g= 2 | github.com/carn181/fsnotify v0.0.0-20250612182652-935ca6b92412/go.mod h1:LyOAO9e2FjZ61JNmsn+7dI4jg0+yhHPg6+cGTVqSxqU= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/khiner/tree-sitter-faust v0.0.0-20250701002309-122dd1019192 h1:OhO0SzXfxs0Pd3wk3EJNSLBQ7WZiNe/GVc+WmZlK874= 6 | github.com/khiner/tree-sitter-faust v0.0.0-20250701002309-122dd1019192/go.mod h1:u7eaf+8hwLapBvCSzDa6seDS84XGHi/74SGTFMi+VRg= 7 | github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 8 | github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 9 | github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= 10 | github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= 11 | github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= 12 | github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 16 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | github.com/tree-sitter/go-tree-sitter v0.25.0 h1:sx6kcg8raRFCvc9BnXglke6axya12krCJF5xJ2sftRU= 18 | github.com/tree-sitter/go-tree-sitter v0.25.0/go.mod h1:r77ig7BikoZhHrrsjAnv8RqGti5rtSyvDHPzgTPsUuU= 19 | github.com/tree-sitter/tree-sitter-c v0.23.4 h1:nBPH3FV07DzAD7p0GfNvXM+Y7pNIoPenQWBpvM++t4c= 20 | github.com/tree-sitter/tree-sitter-c v0.23.4/go.mod h1:MkI5dOiIpeN94LNjeCp8ljXN/953JCwAby4bClMr6bw= 21 | github.com/tree-sitter/tree-sitter-cpp v0.23.4 h1:LaWZsiqQKvR65yHgKmnaqA+uz6tlDJTJFCyFIeZU/8w= 22 | github.com/tree-sitter/tree-sitter-cpp v0.23.4/go.mod h1:doqNW64BriC7WBCQ1klf0KmJpdEvfxyXtoEybnBo6v8= 23 | github.com/tree-sitter/tree-sitter-embedded-template v0.23.2 h1:nFkkH6Sbe56EXLmZBqHHcamTpmz3TId97I16EnGy4rg= 24 | github.com/tree-sitter/tree-sitter-embedded-template v0.23.2/go.mod h1:HNPOhN0qF3hWluYLdxWs5WbzP/iE4aaRVPMsdxuzIaQ= 25 | github.com/tree-sitter/tree-sitter-go v0.23.4 h1:yt5KMGnTHS+86pJmLIAZMWxukr8W7Ae1STPvQUuNROA= 26 | github.com/tree-sitter/tree-sitter-go v0.23.4/go.mod h1:Jrx8QqYN0v7npv1fJRH1AznddllYiCMUChtVjxPK040= 27 | github.com/tree-sitter/tree-sitter-html v0.23.2 h1:1UYDV+Yd05GGRhVnTcbP58GkKLSHHZwVaN+lBZV11Lc= 28 | github.com/tree-sitter/tree-sitter-html v0.23.2/go.mod h1:gpUv/dG3Xl/eebqgeYeFMt+JLOY9cgFinb/Nw08a9og= 29 | github.com/tree-sitter/tree-sitter-java v0.23.5 h1:J9YeMGMwXYlKSP3K4Us8CitC6hjtMjqpeOf2GGo6tig= 30 | github.com/tree-sitter/tree-sitter-java v0.23.5/go.mod h1:NRKlI8+EznxA7t1Yt3xtraPk1Wzqh3GAIC46wxvc320= 31 | github.com/tree-sitter/tree-sitter-javascript v0.23.1 h1:1fWupaRC0ArlHJ/QJzsfQ3Ibyopw7ZfQK4xXc40Zveo= 32 | github.com/tree-sitter/tree-sitter-javascript v0.23.1/go.mod h1:lmGD1EJdCA+v0S1u2fFgepMg/opzSg/4pgFym2FPGAs= 33 | github.com/tree-sitter/tree-sitter-json v0.24.8 h1:tV5rMkihgtiOe14a9LHfDY5kzTl5GNUYe6carZBn0fQ= 34 | github.com/tree-sitter/tree-sitter-json v0.24.8/go.mod h1:F351KK0KGvCaYbZ5zxwx/gWWvZhIDl0eMtn+1r+gQbo= 35 | github.com/tree-sitter/tree-sitter-php v0.23.11 h1:iHewsLNDmznh8kgGyfWfujsZxIz1YGbSd2ZTEM0ZiP8= 36 | github.com/tree-sitter/tree-sitter-php v0.23.11/go.mod h1:T/kbfi+UcCywQfUNAJnGTN/fMSUjnwPXA8k4yoIks74= 37 | github.com/tree-sitter/tree-sitter-python v0.23.6 h1:qHnWFR5WhtMQpxBZRwiaU5Hk/29vGju6CVtmvu5Haas= 38 | github.com/tree-sitter/tree-sitter-python v0.23.6/go.mod h1:cpdthSy/Yoa28aJFBscFHlGiU+cnSiSh1kuDVtI8YeM= 39 | github.com/tree-sitter/tree-sitter-ruby v0.23.1 h1:T/NKHUA+iVbHM440hFx+lzVOzS4dV6z8Qw8ai+72bYo= 40 | github.com/tree-sitter/tree-sitter-ruby v0.23.1/go.mod h1:kUS4kCCQloFcdX6sdpr8p6r2rogbM6ZjTox5ZOQy8cA= 41 | github.com/tree-sitter/tree-sitter-rust v0.23.2 h1:6AtoooCW5GqNrRpfnvl0iUhxTAZEovEmLKDbyHlfw90= 42 | github.com/tree-sitter/tree-sitter-rust v0.23.2/go.mod h1:hfeGWic9BAfgTrc7Xf6FaOAguCFJRo3RBbs7QJ6D7MI= 43 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 44 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 45 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 46 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 47 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 48 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | -------------------------------------------------------------------------------- /transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "io" 9 | "net" 10 | "os" 11 | "strconv" 12 | 13 | "github.com/carn181/faustlsp/logging" 14 | ) 15 | 16 | type TransportMethod int 17 | 18 | const ( 19 | Stdin = iota 20 | Socket 21 | ) 22 | 23 | // Useful for socket dialling or listening based on client and server 24 | type TransportType int 25 | 26 | const ( 27 | Client = iota 28 | Server 29 | ) 30 | 31 | // Transport structure to handle reading from streams 32 | type Transport struct { 33 | Type TransportType // client or server 34 | Method TransportMethod // type of stream 35 | Scanner *bufio.Scanner // reader (scanner) 36 | conn net.Conn // connection to close for client 37 | ln net.Listener // listener to close for server 38 | Writer io.Writer // writer 39 | Closed bool 40 | } 41 | 42 | func (t *Transport) Init(ttype TransportType, method TransportMethod) { 43 | t.Method = method 44 | t.Type = ttype 45 | var r io.Reader 46 | 47 | switch t.Method { 48 | // Communicate with client through stdin 49 | case Stdin: 50 | r = os.Stdin 51 | t.Writer = os.Stdout 52 | 53 | // Communicate with client through tcp socket 54 | // Default port at 5007 55 | // TODO: take port from cmd arguments 56 | case Socket: 57 | var conn net.Conn 58 | var err error 59 | switch t.Type { 60 | case Server: 61 | t.ln, err = net.Listen("tcp", ":5007") 62 | if err != nil { 63 | logging.Logger.Error("Connection error", "error", err) 64 | } 65 | conn, err = t.ln.Accept() 66 | if err != nil { 67 | logging.Logger.Error("Connection error", "error", err) 68 | } 69 | case Client: 70 | var err error 71 | conn, err = net.Dial("tcp", "localhost:5007") 72 | t.conn = conn 73 | if err != nil { 74 | logging.Logger.Error("Connection error", "error", err) 75 | } 76 | } 77 | r = conn 78 | t.Writer = conn 79 | } 80 | 81 | // TODO: Find dynamic buffer for handling large files 82 | const maxBufferSize = 1024 * 1024 * 10 // 10 MB 83 | buf := make([]byte, maxBufferSize) 84 | scanner := bufio.NewScanner(r) 85 | scanner.Buffer(buf, maxBufferSize) 86 | scanner.Split(split) 87 | t.Scanner = scanner 88 | } 89 | 90 | // Reads one JSON RPC message from the stream 91 | func (t *Transport) Read() ([]byte, error) { 92 | hasError := !t.Scanner.Scan() 93 | if hasError { 94 | if t.Scanner.Err() == nil { 95 | t.Closed = true 96 | } 97 | } 98 | 99 | rawMessage := t.Scanner.Bytes() 100 | err := t.Scanner.Err() 101 | if err != nil { 102 | return rawMessage, err 103 | } 104 | 105 | _, content, _ := bytes.Cut(rawMessage, []byte{'\r', '\n', '\r', '\n'}) 106 | return content, nil 107 | } 108 | 109 | // Writes JSON RPC message 110 | func (t *Transport) Write(msg []byte) error { 111 | header := []byte("Content-Length: " + strconv.Itoa(len(msg)) + "\r\n\r\n") 112 | _, err := t.Writer.Write(append(header, msg...)) 113 | return err 114 | } 115 | 116 | // Writes JSON RPC Notif Message 117 | func (t *Transport) WriteNotif(method string, params json.RawMessage) error { 118 | msg, err := json.Marshal( 119 | NotificationMessage{ 120 | Message: Message{Jsonrpc: "2.0"}, 121 | Method: method, 122 | Params: params, 123 | }) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | err = t.Write(msg) 129 | return err 130 | } 131 | 132 | // Writes JSON RPC Request Message 133 | func (t *Transport) WriteRequest(id any, method string, params json.RawMessage) error { 134 | msg, err := json.Marshal( 135 | RequestMessage{ 136 | Message: Message{Jsonrpc: "2.0"}, 137 | ID: id, 138 | Method: method, 139 | Params: params, 140 | }) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | logging.Logger.Info("Writing " + string(msg)) 146 | err = t.Write(msg) 147 | return err 148 | } 149 | 150 | // Writes JSON RPC Response Message 151 | func (t *Transport) WriteResponse(id any, response json.RawMessage, responseError *ResponseError) error { 152 | msg, err := json.Marshal( 153 | ResponseMessage{ 154 | Message: Message{Jsonrpc: "2.0"}, 155 | ID: id, 156 | Result: response, 157 | Error: responseError, 158 | }) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | logging.Logger.Info("Writing " + string(msg)) 164 | err = t.Write(msg) 165 | return err 166 | } 167 | 168 | func (t *Transport) Close() { 169 | if t.Method == Socket { 170 | if t.Type == Client { 171 | t.conn.Close() 172 | } else { 173 | t.ln.Close() 174 | } 175 | } 176 | } 177 | 178 | // Split function for scanner to parse a JSON RPC message 179 | func split(data []byte, _ bool) (advance int, token []byte, err error) { 180 | header, content, found := bytes.Cut(data, []byte{'\r', '\n', '\r', '\n'}) 181 | if !found { 182 | return 0, nil, nil 183 | } 184 | 185 | // Content-Length: 186 | if len(header) < len("Content-Length: ") { 187 | return 0, nil, errors.New("invalid Header: " + string(header)) 188 | } 189 | contentLengthBytes := header[len("Content-Length: "):] 190 | contentLength, err := strconv.Atoi(string(contentLengthBytes)) 191 | if err != nil { 192 | return 0, nil, errors.New("invalid Content Length") 193 | } 194 | 195 | if len(content) < contentLength { 196 | return 0, nil, nil 197 | } 198 | 199 | totalLength := len(header) + 4 + contentLength 200 | return totalLength, data[:totalLength], nil 201 | } 202 | 203 | func GetMethod(content []byte) (string, error) { 204 | var msg RPCMessage 205 | 206 | err := json.Unmarshal(content, &msg) 207 | return msg.Method, err 208 | } 209 | -------------------------------------------------------------------------------- /server/files.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "fmt" 7 | "log/slog" 8 | "os" 9 | 10 | "sync" 11 | 12 | "github.com/carn181/faustlsp/logging" 13 | "github.com/carn181/faustlsp/parser" 14 | "github.com/carn181/faustlsp/transport" 15 | "github.com/carn181/faustlsp/util" 16 | ) 17 | 18 | type File struct { 19 | // Ensure thread-safety for modifications 20 | mu sync.RWMutex 21 | Handle util.Handle 22 | 23 | // A file's Syntax Tree Scope. Contains all symbols that are accessible in it. 24 | // Parent of this scope will be nil 25 | Scope *Scope 26 | 27 | // File Content 28 | Content []byte 29 | 30 | // Hash for each file. Used for caching scopes. 31 | Hash [sha256.Size]byte 32 | 33 | // TODO: Shift away from using this in diagnostics checking step 34 | hasSyntaxErrors bool 35 | } 36 | 37 | func (f *File) LogValue() slog.Value { 38 | // Create a map with all file attributes 39 | fileAttrs := map[string]any{ 40 | "Handle": f.Handle, 41 | "Hash": f.Hash, 42 | "Scope": f.Scope, 43 | } 44 | return slog.AnyValue(fileAttrs) 45 | } 46 | 47 | func (f *File) DocumentSymbols() []transport.DocumentSymbol { 48 | f.mu.RLock() 49 | defer f.mu.RUnlock() 50 | 51 | t := parser.ParseTree(f.Content) 52 | defer t.Close() 53 | return parser.DocumentSymbols(t, f.Content) 54 | // return []transport.DocumentSymbol{} 55 | } 56 | 57 | func (f *File) TSDiagnostics() transport.PublishDiagnosticsParams { 58 | logging.Logger.Info("Waiting for lock", "file", f.Handle.Path) 59 | f.mu.Lock() 60 | 61 | logging.Logger.Info("Got lock", "file", f.Handle.Path) 62 | t := parser.ParseTree(f.Content) 63 | 64 | errors := parser.TSDiagnostics(f.Content, t) 65 | if len(errors) == 0 { 66 | f.hasSyntaxErrors = false 67 | } else { 68 | f.hasSyntaxErrors = true 69 | } 70 | d := transport.PublishDiagnosticsParams{ 71 | URI: transport.DocumentURI(f.Handle.URI), 72 | Diagnostics: errors, 73 | } 74 | f.mu.Unlock() 75 | return d 76 | } 77 | 78 | type Files struct { 79 | // Absolute Paths Only 80 | fs map[util.Handle]*File 81 | mu sync.Mutex 82 | encoding transport.PositionEncodingKind // Position Encoding for applying incremental changes. UTF-16 and UTF-32 supported 83 | } 84 | 85 | func (files *Files) Init(context context.Context, encoding transport.PositionEncodingKind) { 86 | files.fs = make(map[util.Handle]*File) 87 | files.encoding = encoding 88 | } 89 | 90 | func (files *Files) OpenFromURI(uri util.URI) { 91 | handle, err := util.FromURI(uri) 92 | if err != nil { 93 | logging.Logger.Error("Invalid URI", "uri", uri, "error", err) 94 | } 95 | files.Open(handle) 96 | } 97 | 98 | func (files *Files) OpenFromPath(path util.Path) { 99 | handle := util.FromPath(path) 100 | files.Open(handle) 101 | } 102 | 103 | func (files *Files) Open(handle util.Handle) { 104 | _, ok := files.Get(handle) 105 | // If File already in store, ignore 106 | if ok { 107 | logging.Logger.Info("File already in store", "handle.Path", handle.Path) 108 | return 109 | } 110 | logging.Logger.Info("Reading contents of file", "handle.Path", handle.Path) 111 | 112 | content, err := os.ReadFile(handle.Path) 113 | 114 | if err != nil { 115 | if os.IsNotExist(err) { 116 | logging.Logger.Error("Invalid Path", "error", err) 117 | return 118 | } 119 | } 120 | 121 | var file = File{ 122 | Handle: handle, 123 | Content: content, 124 | Hash: sha256.Sum256(content), 125 | } 126 | 127 | files.mu.Lock() 128 | files.fs[handle] = &file 129 | files.mu.Unlock() 130 | } 131 | 132 | func (files *Files) AddFromURI(uri util.URI, content []byte) { 133 | handle, err := util.FromURI(uri) 134 | if err != nil { 135 | return 136 | } 137 | files.Add(handle, content) 138 | } 139 | 140 | func (files *Files) Add(handle util.Handle, content []byte) { 141 | var file = File{ 142 | Handle: handle, Content: content, Hash: sha256.Sum256(content), 143 | } 144 | files.mu.Lock() 145 | files.fs[handle] = &file 146 | files.mu.Unlock() 147 | } 148 | 149 | func (files *Files) Get(handle util.Handle) (*File, bool) { 150 | files.mu.Lock() 151 | file, ok := files.fs[handle] 152 | files.mu.Unlock() 153 | return file, ok 154 | } 155 | 156 | func (files *Files) GetFromPath(path util.Path) (*File, bool) { 157 | handle := util.FromPath(path) 158 | file, ok := files.Get(handle) 159 | return file, ok 160 | } 161 | 162 | func (files *Files) GetFromURI(uri util.URI) (*File, bool) { 163 | handle, err := util.FromURI(uri) 164 | if err != nil { 165 | return nil, false 166 | } 167 | file, ok := files.Get(handle) 168 | return file, ok 169 | } 170 | 171 | func (files *Files) TSDiagnostics(path util.Path) transport.PublishDiagnosticsParams { 172 | d := transport.PublishDiagnosticsParams{} 173 | 174 | file, ok := files.GetFromPath(path) 175 | files.mu.Lock() 176 | if ok { 177 | d = file.TSDiagnostics() 178 | 179 | } 180 | files.mu.Unlock() 181 | return d 182 | } 183 | 184 | func (files *Files) ModifyFull(path util.Path, content string) { 185 | 186 | f, ok := files.GetFromPath(path) 187 | if !ok { 188 | logging.Logger.Error("file to modify not in file store", "path", path) 189 | files.mu.Unlock() 190 | return 191 | } 192 | 193 | files.mu.Lock() 194 | f.mu.Lock() 195 | f.Content = []byte(content) 196 | f.Hash = sha256.Sum256(f.Content) 197 | f.mu.Unlock() 198 | 199 | files.mu.Unlock() 200 | } 201 | 202 | func (files *Files) ModifyIncremental(path util.Path, changeRange transport.Range, content string) { 203 | logging.Logger.Info("Applying Incremental Change", "path", path) 204 | 205 | f, ok := files.GetFromPath(path) 206 | if !ok { 207 | logging.Logger.Error("file to modify not in file store", "path", path) 208 | files.mu.Unlock() 209 | return 210 | } 211 | result := ApplyIncrementalChange(changeRange, content, string(f.Content), string(files.encoding)) 212 | // logging.Logger.Info("Before/After Incremental Change", "before", string(f.Content), "after", result) 213 | logging.Logger.Info("Incremental Change Parameters ", "range", changeRange, "content", content) 214 | logging.Logger.Info("Before/After Incremental Change", "before", string(f.Content), "after", result) 215 | 216 | files.mu.Lock() 217 | f.mu.Lock() 218 | f.Content = []byte(result) 219 | f.Hash = sha256.Sum256(f.Content) 220 | f.mu.Unlock() 221 | 222 | files.mu.Unlock() 223 | } 224 | 225 | func (files *Files) CloseFromURI(uri util.URI) { 226 | handle, err := util.FromURI(uri) 227 | if err != nil { 228 | logging.Logger.Error("CloseFromURI error", "error", err) 229 | return 230 | } 231 | files.Close(handle) 232 | } 233 | 234 | func (files *Files) CloseFromPath(path util.Path) { 235 | handle := util.FromPath(path) 236 | files.Close(handle) 237 | } 238 | 239 | func (files *Files) Close(handle util.Handle) { 240 | files.mu.Lock() 241 | f, ok := files.fs[handle] 242 | if !ok { 243 | logging.Logger.Error("file to close not in file store", "handle", handle) 244 | files.mu.Unlock() 245 | return 246 | } 247 | f.mu.Lock() 248 | f.mu.Unlock() 249 | files.mu.Unlock() 250 | } 251 | 252 | func (files *Files) RemoveFromPath(path util.Path) { 253 | handle := util.FromPath(path) 254 | files.mu.Lock() 255 | delete(files.fs, handle) 256 | files.mu.Unlock() 257 | } 258 | 259 | func (files *Files) RemoveFromURI(uri util.URI) { 260 | handle, _ := util.FromURI(uri) 261 | files.mu.Lock() 262 | delete(files.fs, handle) 263 | files.mu.Unlock() 264 | } 265 | 266 | func (files *Files) Remove(handle util.Handle) { 267 | files.mu.Lock() 268 | delete(files.fs, handle) 269 | files.mu.Unlock() 270 | } 271 | 272 | func (files *Files) String() string { 273 | str := "" 274 | for handle := range files.fs { 275 | if IsFaustFile(handle.Path) { 276 | str += fmt.Sprintf("Files has %s\n", handle) 277 | } 278 | } 279 | return str 280 | } 281 | 282 | func (files *Files) LogValue() slog.Value { 283 | fs := make([]any, 0, len(files.fs)) 284 | files.mu.Lock() 285 | defer files.mu.Unlock() 286 | 287 | for handle, file := range files.fs { 288 | if IsFaustFile(handle.Path) { 289 | // Use each file's LogValue method to get its proper representation 290 | fileValue := file.LogValue() 291 | fs = append(fs, fileValue.Any()) 292 | } 293 | } 294 | return slog.AnyValue(fs) 295 | } 296 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "sync" 12 | 13 | "github.com/carn181/faustlsp/logging" 14 | "github.com/carn181/faustlsp/parser" 15 | "github.com/carn181/faustlsp/transport" 16 | "github.com/carn181/faustlsp/util" 17 | ) 18 | 19 | // TODO: Have a type for request ID 20 | 21 | type ServerState int 22 | 23 | const ( 24 | Created = iota 25 | Initializing 26 | Running 27 | Shutdown 28 | Exit 29 | ExitError 30 | ) 31 | 32 | // Main Server Struct 33 | type Server struct { 34 | // TODO: workspaceFolders, diagnosticsBundle, mutex 35 | // TODO: request id counter so that we can send our own requests 36 | // Capabalities 37 | Capabilities transport.ServerCapabilities 38 | 39 | // Workspace and Files are different because in future should allow having multiple workspaces while having one main File Store, but both have to be synchronized on each document Change 40 | Workspace Workspace 41 | Files Files 42 | Store Store 43 | 44 | Status ServerState 45 | mu sync.Mutex 46 | 47 | // Allows to add other transportation methods in the future 48 | // possible values: stdin | socket 49 | Transport transport.Transport 50 | 51 | // Request Id Counter for new requ ests 52 | reqIdCtr int 53 | 54 | // Temporary Directory where we replicate workspace for diagnostics 55 | tempDir util.Path 56 | 57 | // Diagnostic Channel 58 | diagChan chan transport.PublishDiagnosticsParams 59 | } 60 | 61 | // Initialize Server 62 | func (s *Server) Init(transp transport.TransportMethod) { 63 | s.Status = Created 64 | s.Transport.Init(transport.Server, transp) 65 | parser.Init() 66 | 67 | // Create Temporary Directory 68 | faustTemp := filepath.Join(os.TempDir(), "faustlsp") // No need to create $TEMPDIR/faustlsp as logging should create it 69 | temp_dir, err := os.MkdirTemp(faustTemp, "faustlsp-") 70 | if err != nil { 71 | logging.Logger.Error("Couldn't create temp dir", "error", err) 72 | return 73 | } else { 74 | logging.Logger.Info("Created Temp Directory", "path", temp_dir) 75 | } 76 | s.tempDir = temp_dir 77 | } 78 | 79 | // Might be pointless ? 80 | // Wanted a way to handle both cancel and ending gracefully from the loop go routine while handling or logging possible errors 81 | func (s *Server) Run(ctx context.Context) error { 82 | var returnError error 83 | end := make(chan error, 1) 84 | go s.Loop(ctx, end) 85 | select { 86 | case err := <-end: 87 | if err != nil { 88 | errormsg := "Ending because of error (" + err.Error() + ")" 89 | logging.Logger.Info(errormsg) 90 | fmt.Println(errormsg) 91 | returnError = errors.New(err.Error()) 92 | } else { 93 | logging.Logger.Info("LSP Successfully Exited") 94 | } 95 | case <-ctx.Done(): 96 | logging.Logger.Info("Canceling Main Loop") 97 | } 98 | 99 | // TODO: Have a proper cleanup function here 100 | parser.Close() 101 | os.RemoveAll(s.tempDir) 102 | return returnError 103 | } 104 | 105 | // The central LSP server loop 106 | func (s *Server) Loop(ctx context.Context, end chan<- error) { 107 | var err error 108 | var msg []byte 109 | var method string 110 | 111 | // LSP Server Main Loop 112 | for s.Status != Exit && s.Status != ExitError && !s.Transport.Closed && err == nil { 113 | // If parent cancels, make sure to stop 114 | select { 115 | case <-ctx.Done(): 116 | break 117 | default: 118 | } 119 | 120 | // Read one JSON RPC Message 121 | logging.Logger.Debug("Reading") 122 | msg, err = s.Transport.Read() 123 | if err != nil { 124 | logging.Logger.Error("Scanning error", "error", err) 125 | } 126 | 127 | // Parse JSON RPC Message here and get method 128 | method, err = transport.GetMethod(msg) 129 | if len(method) == 0 { 130 | break 131 | } 132 | if err != nil { 133 | logging.Logger.Error("Parsing error", "error", err) 134 | break 135 | } 136 | 137 | logging.Logger.Debug("Got Method: " + method) 138 | 139 | // Validate Message (error if the client shouldn't be sending that method) 140 | err = s.ValidateMethod(method) 141 | if err != nil { 142 | break 143 | } 144 | 145 | // Dispatch to Method Handler 146 | 147 | // Handle important lifecycle messages non-concurrently 148 | switch method { 149 | case "exit", "shutdown", "initialize", "initialized": 150 | s.HandleMethod(ctx, method, msg) 151 | default: 152 | go s.HandleMethod(ctx, method, msg) 153 | } 154 | } 155 | if s.Status == ExitError { 156 | err = errors.New("exiting ungracefully") 157 | end <- err 158 | } else if s.Status == Exit { 159 | end <- nil 160 | return 161 | } 162 | if err == nil && s.Transport.Closed { 163 | err = errors.New("stream closed: got EOF") 164 | } else { 165 | s.Transport.Close() 166 | } 167 | end <- err 168 | } 169 | 170 | // Validates if current method is valid given current server State 171 | // TODO: Handle all server states 172 | func (s *Server) ValidateMethod(method string) error { 173 | switch s.Status { 174 | case Created: 175 | if method != "initialize" { 176 | return errors.New("Server not started, but received " + method) 177 | } 178 | case Shutdown: 179 | if method != "exit" { 180 | return errors.New("Can only exit" + method) 181 | } 182 | } 183 | return nil 184 | } 185 | 186 | // Main Handle Method 187 | func (s *Server) HandleMethod(ctx context.Context, method string, content []byte) { 188 | // TODO: Receive only content, no Header 189 | handler, ok := requestHandlers[method] 190 | if ok { 191 | var m transport.RequestMessage 192 | json.Unmarshal(content, &m) 193 | logging.Logger.Debug("Request ID", "type", reflect.TypeOf(m.ID), "value", m.ID) 194 | if reflect.TypeOf(m.ID).String() == "float64" { 195 | s.reqIdCtr = int(m.ID.(float64) + 1) 196 | } 197 | 198 | // Main handle method for request and get response 199 | resp, err := handler(ctx, s, m.Params) 200 | 201 | var responseError *transport.ResponseError 202 | if err != nil { 203 | responseError = &transport.ResponseError{ 204 | Code: int(transport.InternalError), 205 | Message: err.Error(), 206 | } 207 | } 208 | err = s.Transport.WriteResponse(m.ID, resp, responseError) 209 | if err != nil { 210 | logging.Logger.Warn(err.Error()) 211 | return 212 | } 213 | 214 | return 215 | } 216 | handler2, ok := notificationHandlers[method] 217 | if ok { 218 | var m transport.NotificationMessage 219 | json.Unmarshal(content, &m) 220 | 221 | // Send Request Message to appropriate Handler 222 | err := handler2(ctx, s, m.Params) 223 | if err != nil { 224 | logging.Logger.Warn(err.Error()) 225 | return 226 | } 227 | } 228 | return 229 | } 230 | 231 | // Map from method to method handler for request methods 232 | var requestHandlers = map[string]func(context.Context, *Server, json.RawMessage) (json.RawMessage, error){ 233 | "initialize": Initialize, 234 | "textDocument/documentSymbol": TextDocumentSymbol, 235 | "textDocument/formatting": Formatting, 236 | "textDocument/definition": GetDefinition, 237 | "textDocument/hover": Hover, 238 | "textDocument/completion": Completion, 239 | "shutdown": ShutdownEnd, 240 | } 241 | 242 | // Map from method to method handler for request methods 243 | var notificationHandlers = map[string]func(context.Context, *Server, json.RawMessage) error{ 244 | "initialized": Initialized, 245 | "textDocument/didOpen": TextDocumentOpen, 246 | "textDocument/didChange": TextDocumentChangeIncremental, 247 | "textDocument/didClose": TextDocumentClose, 248 | // The save action of textDocument/didSave should be handled by our watcher to our store, so no need to handle 249 | "exit": ExitEnd, 250 | } 251 | 252 | func TextDocumentSymbol(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 253 | var params transport.DocumentSymbolParams 254 | json.Unmarshal(par, ¶ms) 255 | 256 | fileURI := params.TextDocument.URI 257 | path, err := util.URI2path(string(fileURI)) 258 | if err != nil { 259 | return []byte{}, err 260 | } 261 | f, ok := s.Files.GetFromPath(path) 262 | if !ok { 263 | return []byte{}, fmt.Errorf("trying to get symbols from non-existent path: %s", path) 264 | } 265 | result := f.DocumentSymbols() 266 | 267 | resultBytes, err := json.Marshal(result) 268 | 269 | return resultBytes, err 270 | } 271 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | . "github.com/carn181/faustlsp/transport" 8 | "github.com/carn181/faustlsp/util" 9 | 10 | tree_sitter_faust "github.com/khiner/tree-sitter-faust/bindings/go" 11 | tree_sitter "github.com/tree-sitter/go-tree-sitter" 12 | ) 13 | 14 | // TODO: Improve DocumentSymbols function 15 | // TODO: Handle Incremental Changes to Trees 16 | 17 | type TSParser struct { 18 | language *tree_sitter.Language 19 | parser *tree_sitter.Parser 20 | treesToClose []*tree_sitter.Tree 21 | mu sync.Mutex 22 | } 23 | 24 | var tsParser TSParser 25 | 26 | func Init() { 27 | tsParser.language = tree_sitter.NewLanguage(tree_sitter_faust.Language()) 28 | tsParser.parser = tree_sitter.NewParser() 29 | tsParser.parser.SetLanguage(tsParser.language) 30 | } 31 | 32 | type TSQueryResult struct { 33 | // Here string is labels matched from your TS Query 34 | Results map[string][]tree_sitter.Node 35 | } 36 | 37 | func ParseTree(code []byte) *tree_sitter.Tree { 38 | // tsParser.parser = tree_sitter.NewParser() 39 | // tsParser.parser.SetLanguage(tsParser.language) 40 | tsParser.mu.Lock() 41 | tree := tsParser.parser.Parse(code, nil) 42 | // tsParser.parser.Close() 43 | tsParser.parser.Reset() 44 | tsParser.mu.Unlock() 45 | return tree 46 | } 47 | 48 | func TSDiagnostics(code []byte, tree *tree_sitter.Tree) []Diagnostic { 49 | errorQuery := "(ERROR) @error\n(MISSING) @missing" 50 | rslts := GetQueryMatches(errorQuery, code, tree) 51 | 52 | var diagnostics = []Diagnostic{} 53 | for _, errors := range rslts.Results { 54 | for _, node := range errors { 55 | // First named parent node from error 56 | prev := node.Parent() 57 | if prev == nil { 58 | break 59 | } 60 | 61 | if prev.GrammarName() == "ERROR" { 62 | continue 63 | } 64 | for { 65 | if !prev.IsNamed() { 66 | prev = prev.Parent() 67 | } else { 68 | break 69 | } 70 | } 71 | start := node.StartPosition() 72 | end := node.EndPosition() 73 | 74 | var msg string 75 | if node.Kind() != "ERROR" { 76 | msg = fmt.Sprintf("Missing '%s' at %d:%d\n", node.GrammarName(), start.Row, start.Column) 77 | } else { 78 | msg = fmt.Sprintf("Syntax Error: Unexpected '%s' at %d:%d when parsing inside %s\n", node.Utf8Text(code), start.Row, start.Column, prev.GrammarName()) 79 | 80 | } 81 | 82 | d := Diagnostic{ 83 | Range: Range{ 84 | Start: Position{Line: uint32(start.Row), 85 | Character: uint32(start.Column)}, 86 | End: Position{Line: uint32(end.Row), 87 | Character: uint32(end.Column)}, 88 | }, 89 | Message: msg, 90 | Severity: DiagnosticSeverity(Error), 91 | Source: "tree-sitter", 92 | } 93 | diagnostics = append(diagnostics, d) 94 | } 95 | } 96 | return diagnostics 97 | } 98 | 99 | func DocumentSymbols(tree *tree_sitter.Tree, content []byte) []DocumentSymbol { 100 | cursor := tree.Walk() 101 | defer cursor.Close() 102 | 103 | program := DocumentSymbolsRecursive(tree.RootNode(), content) 104 | // fmt.Println(program.Children) 105 | return program.Children 106 | } 107 | 108 | func DocumentSymbolsRecursiveNoEnvironment(node *tree_sitter.Node, content []byte) DocumentSymbol { 109 | name := node.GrammarName() 110 | var s DocumentSymbol 111 | if name == "definition" || name == "function_definition" { 112 | ident := node.Child(0) 113 | s.Name = ident.Utf8Text(content) 114 | // istart := ident.StartPosition() 115 | // iend := ident.EndPosition() 116 | start := node.StartPosition() 117 | end := node.EndPosition() 118 | if name == "function_definition" { 119 | s.Kind = Function 120 | } else if name == "definition" { 121 | s.Kind = Variable 122 | } 123 | s.SelectionRange = Range{ 124 | Start: Position{Line: uint32(start.Row), Character: uint32(start.Column)}, 125 | End: Position{Line: uint32(end.Row), Character: uint32(end.Column)}, 126 | } 127 | s.Range = Range{ 128 | Start: Position{Line: uint32(start.Row), Character: uint32(start.Column)}, 129 | End: Position{Line: uint32(end.Row), Character: uint32(end.Column)}, 130 | } 131 | } 132 | 133 | if name == "definition" || name == "function_definition" || name == "environment" || name == "program" { 134 | for i := uint(0); i < node.ChildCount(); i++ { 135 | n := node.Child(i) 136 | node := DocumentSymbolsRecursive(n, content) 137 | if node.Name != "" { 138 | s.Children = append(s.Children, node) 139 | } 140 | } 141 | return s 142 | } else { 143 | return DocumentSymbol{} 144 | } 145 | 146 | } 147 | 148 | func DocumentSymbolsRecursive(node *tree_sitter.Node, content []byte) DocumentSymbol { 149 | name := node.GrammarName() 150 | var s DocumentSymbol 151 | if name == "definition" || name == "function_definition" { 152 | ident := node.Child(0) 153 | s.Name = ident.Utf8Text(content) 154 | if name == "function_definition" { 155 | s.Kind = Function 156 | } else if name == "definition" { 157 | // Every definition is essentially a function in Faust than a variable 158 | s.Kind = Function 159 | } 160 | // istart := ident.StartPosition() 161 | // iend := ident.EndPosition() 162 | start := node.StartPosition() 163 | end := node.EndPosition() 164 | s.SelectionRange = Range{ 165 | Start: Position{Line: uint32(start.Row), Character: uint32(start.Column)}, 166 | End: Position{Line: uint32(end.Row), Character: uint32(end.Column)}, 167 | } 168 | s.Range = Range{ 169 | Start: Position{Line: uint32(start.Row), Character: uint32(start.Column)}, 170 | End: Position{Line: uint32(end.Row), Character: uint32(end.Column)}, 171 | } 172 | } 173 | 174 | if name == "definition" || name == "function_definition" || name == "program" { 175 | // fmt.Printf("Got %s with %s\n",name,node.Utf8Text(content)) 176 | for i := uint(0); i < node.ChildCount(); i++ { 177 | n := node.Child(i) 178 | node := DocumentSymbolsRecursive(n, content) 179 | if node.Name == "environment" { 180 | s.Children = append(s.Children, node.Children...) 181 | } else if node.Name != "" { 182 | s.Children = append(s.Children, node) 183 | } 184 | } 185 | // fmt.Printf("children of %s is %v\n", node.GrammarName(), s.Children) 186 | return s 187 | } else if name == "with_environment" || name == "letrec_environment" { 188 | s.Name = "environment" 189 | // fmt.Printf("Got %s with %s\n",name,node.Utf8Text(content)) 190 | if node.ChildCount() >= 2 { 191 | node = node.Child(2) 192 | } else { 193 | return DocumentSymbol{} 194 | } 195 | // fmt.Printf("Got %s with %s\n",node.GrammarName(),node.Utf8Text(content)) 196 | for i := uint(0); i < node.ChildCount(); i++ { 197 | n := node.Child(i) 198 | node := DocumentSymbolsRecursive(n, content) 199 | if node.Name != "" { 200 | s.Children = append(s.Children, node) 201 | } 202 | } 203 | // fmt.Printf("children of %s is %v\n", node.GrammarName(), s.Children) 204 | return s 205 | } else { 206 | return DocumentSymbol{} 207 | } 208 | 209 | } 210 | 211 | func GetImports(code []byte, tree *tree_sitter.Tree) []util.Path { 212 | importQuery := ` 213 | (file_import filename: (string) @import) 214 | (definition (identifier) (library filename: (string) @import)) 215 | ` 216 | paths := []util.Path{} 217 | rslts := GetQueryMatches(importQuery, code, tree) 218 | for _, imports := range rslts.Results { 219 | for _, imp := range imports { 220 | p := imp.Utf8Text(code) 221 | cleanRelPath := p[1 : len(p)-1] 222 | paths = append(paths, cleanRelPath) 223 | } 224 | } 225 | return paths 226 | } 227 | 228 | func GetQueryMatches(queryStr string, code []byte, tree *tree_sitter.Tree) TSQueryResult { 229 | query, _ := tree_sitter.NewQuery(tsParser.language, queryStr) 230 | defer query.Close() 231 | 232 | cursor := tree_sitter.NewQueryCursor() 233 | defer cursor.Close() 234 | 235 | matches := cursor.Matches(query, tree.RootNode(), code) 236 | 237 | var result TSQueryResult 238 | result.Results = make(map[string][]tree_sitter.Node) 239 | for match := matches.Next(); match != nil; match = matches.Next() { 240 | for _, capture := range match.Captures { 241 | // fmt.Printf("Match %d, Capture %d (%s): %s\n", match.PatternIndex, capture.Index, query.CaptureNames()[capture.Index], capture.Node.Utf8Text(code)) 242 | 243 | // Add to result 244 | captureName := query.CaptureNames()[capture.Index] 245 | captures, _ := result.Results[captureName] 246 | node := capture.Node 247 | result.Results[captureName] = append(captures, node) 248 | } 249 | } 250 | 251 | return result 252 | } 253 | 254 | func GetQueryMatchesFromNode(queryStr string, code []byte, node *tree_sitter.Node) TSQueryResult { 255 | if node == nil { 256 | return TSQueryResult{} 257 | } 258 | query, _ := tree_sitter.NewQuery(tsParser.language, queryStr) 259 | defer query.Close() 260 | 261 | cursor := tree_sitter.NewQueryCursor() 262 | defer cursor.Close() 263 | 264 | matches := cursor.Matches(query, node, code) 265 | 266 | var result TSQueryResult 267 | result.Results = make(map[string][]tree_sitter.Node) 268 | for match := matches.Next(); match != nil; match = matches.Next() { 269 | for _, capture := range match.Captures { 270 | // fmt.Printf("Match %d, Capture %d (%s): %s\n", match.PatternIndex, capture.Index, query.CaptureNames()[capture.Index], capture.Node.Utf8Text(code)) 271 | 272 | // Add to result 273 | captureName := query.CaptureNames()[capture.Index] 274 | captures, _ := result.Results[captureName] 275 | node := capture.Node 276 | result.Results[captureName] = append(captures, node) 277 | } 278 | } 279 | 280 | return result 281 | } 282 | 283 | func Close() { 284 | // tsParser.parser.Close() 285 | } 286 | -------------------------------------------------------------------------------- /server/goto_methods.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/carn181/faustlsp/logging" 10 | "github.com/carn181/faustlsp/parser" 11 | "github.com/carn181/faustlsp/transport" 12 | "github.com/carn181/faustlsp/util" 13 | ) 14 | 15 | func GetDefinition(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 16 | // TODO: Work on this function 17 | var params transport.DefinitionParams 18 | json.Unmarshal(par, ¶ms) 19 | 20 | logging.Logger.Info("Goto Definition Request", "params", params) 21 | path, err := util.URI2path(string(params.TextDocument.URI)) 22 | if err != nil { 23 | logging.Logger.Error("Uri2path error", "error", err) 24 | return []byte{}, err 25 | } 26 | 27 | f, ok := s.Files.GetFromPath(path) 28 | if !ok { 29 | logging.Logger.Error("File should've been in server file store", "path", path) 30 | } 31 | 32 | offset, err := PositionToOffset(params.Position, string(f.Content), string(s.Files.encoding)) 33 | if err != nil { 34 | return []byte{}, err 35 | } 36 | 37 | ident, scope := FindSymbolScope(f.Content, f.Scope, offset) 38 | 39 | logging.Logger.Info("Got symbol at Location", "symbol", ident, "scope_exists", f.Scope != nil) 40 | 41 | if ident == "" { 42 | // Couldn't find symbol to lookup 43 | return []byte("null"), nil 44 | } 45 | 46 | var loc Location 47 | identSplit := strings.Split(ident, ".") 48 | 49 | if len(identSplit) > 1 { 50 | logging.Logger.Info("Resolving library symbol", "symbol", identSplit) 51 | for i := range len(identSplit) - 1 { 52 | libIdent := identSplit[i] 53 | 54 | // Resolve as Environment 55 | sym, err := FindEnvironmentIdent(libIdent, scope, &s.Store) 56 | logging.Logger.Info("Resolved environment", "env", libIdent, "sym", sym.Ident, "loc", sym.Loc) 57 | if err == nil { 58 | loc = sym.Loc 59 | scope = sym.Scope 60 | continue 61 | } 62 | 63 | // Resolve as Library if not resolved as environment 64 | file, err := FindLibraryIdent(libIdent, scope, &s.Store) 65 | if err != nil { 66 | break 67 | } 68 | logging.Logger.Info("Resolved library environment", "env", libIdent, "location", file) 69 | f, ok := s.Store.Files.GetFromPath(file) 70 | if ok { 71 | f.mu.RLock() 72 | logging.Logger.Info("Setting New Scope to", "path", file) 73 | scope = f.Scope 74 | f.mu.RUnlock() 75 | if scope == nil { 76 | break 77 | } 78 | } 79 | } 80 | } 81 | ident = identSplit[len(identSplit)-1] 82 | 83 | loc, err = FindDefinition(ident, scope, &s.Store) 84 | 85 | logging.Logger.Info("Got definition as", "location", loc, "error", err) 86 | if err == nil { 87 | fileLocation := transport.Location{ 88 | URI: transport.DocumentURI(util.Path2URI(loc.File)), 89 | Range: loc.Range, 90 | } 91 | result, err := json.Marshal(fileLocation) 92 | if err == nil { 93 | return result, nil 94 | } 95 | } 96 | 97 | return []byte("null"), nil 98 | } 99 | 100 | func Hover(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 101 | // TODO: Work on this function 102 | var params transport.HoverParams 103 | json.Unmarshal(par, ¶ms) 104 | 105 | logging.Logger.Info("Hover Request", "params", params) 106 | path, err := util.URI2path(string(params.TextDocument.URI)) 107 | if err != nil { 108 | logging.Logger.Error("Uri2path error", "error", err) 109 | return []byte{}, err 110 | } 111 | 112 | f, ok := s.Files.GetFromPath(path) 113 | if !ok { 114 | logging.Logger.Error("File should've been in server file store", "path", path) 115 | } 116 | 117 | offset, err := PositionToOffset(params.Position, string(f.Content), string(s.Files.encoding)) 118 | if err != nil { 119 | return []byte{}, err 120 | } 121 | 122 | ident, scope := FindSymbolScope(f.Content, f.Scope, offset) 123 | 124 | logging.Logger.Info("Got symbol at Location", "symbol", ident, "scope_exists", f.Scope != nil) 125 | 126 | if ident == "" { 127 | // Couldn't find symbol to lookup 128 | return []byte("null"), nil 129 | } 130 | 131 | identSplit := strings.Split(ident, ".") 132 | 133 | if len(identSplit) > 1 { 134 | logging.Logger.Info("Resolving library symbol", "symbol", identSplit) 135 | for i := range len(identSplit) - 1 { 136 | libIdent := identSplit[i] 137 | 138 | // Resolve as Environment 139 | sym, err := FindEnvironmentIdent(libIdent, scope, &s.Store) 140 | logging.Logger.Info("Resolved environment", "env", libIdent, "sym", sym.Ident, "loc", sym.Loc) 141 | if err == nil { 142 | scope = sym.Scope 143 | continue 144 | } 145 | 146 | // Resolve as Library if not resolved as environment 147 | file, err := FindLibraryIdent(libIdent, scope, &s.Store) 148 | if err != nil { 149 | break 150 | } 151 | logging.Logger.Info("Resolved library environment", "env", libIdent, "location", file) 152 | f, ok := s.Store.Files.GetFromPath(file) 153 | if ok { 154 | f.mu.RLock() 155 | logging.Logger.Info("Setting New Scope to", "path", file) 156 | scope = f.Scope 157 | f.mu.RUnlock() 158 | if scope == nil { 159 | break 160 | } 161 | } 162 | } 163 | } 164 | ident = identSplit[len(identSplit)-1] 165 | 166 | docs, err := FindDocs(ident, scope, &s.Store) 167 | 168 | logging.Logger.Info("Got docs as", "documentation", docs, "error", err) 169 | if err == nil { 170 | docsResp := transport.Hover{ 171 | Contents: transport.MarkupContent{ 172 | Kind: transport.Markdown, 173 | Value: docs, 174 | }, 175 | } 176 | result, err := json.Marshal(docsResp) 177 | if err == nil { 178 | return result, nil 179 | } 180 | } 181 | 182 | return []byte("null"), nil 183 | } 184 | 185 | func GetReferences(ctx context.Context, s *Server, par json.RawMessage) (json.RawMessage, error) { 186 | // TODO: Work on this function 187 | var params transport.DefinitionParams 188 | json.Unmarshal(par, ¶ms) 189 | 190 | logging.Logger.Info("Goto Definition Request", "params", params) 191 | path, err := util.URI2path(string(params.TextDocument.URI)) 192 | if err != nil { 193 | logging.Logger.Error("Uri2path error", "error", err) 194 | return []byte{}, err 195 | } 196 | 197 | f, ok := s.Files.GetFromPath(path) 198 | if !ok { 199 | logging.Logger.Error("File should've been in server file store", "path", path) 200 | } 201 | 202 | offset, err := PositionToOffset(params.Position, string(f.Content), string(s.Files.encoding)) 203 | if err != nil { 204 | return []byte{}, err 205 | } 206 | 207 | ident, scope := FindSymbolScope(f.Content, f.Scope, offset) 208 | 209 | logging.Logger.Info("Got symbol at Location", "symbol", ident, "scope", f.Scope == nil) 210 | 211 | if ident == "" { 212 | // Couldn't find symbol to lookup 213 | return []byte("null"), nil 214 | } 215 | 216 | var loc Location 217 | identSplit := strings.Split(ident, ".") 218 | if len(identSplit) > 1 { 219 | logging.Logger.Info("Resolving library symbol", "symbol", identSplit) 220 | for _, libIdent := range identSplit { 221 | // Resolve as Environment 222 | sym, err := FindEnvironmentIdent(ident, scope, &s.Store) 223 | logging.Logger.Info("Resolved environment", "env", libIdent, "sym", sym.Ident, "loc", sym.Loc) 224 | if err == nil { 225 | loc = sym.Loc 226 | scope = sym.Scope 227 | continue 228 | } 229 | 230 | // Resolve as Library if not resolved as environment 231 | file, err := FindLibraryIdent(libIdent, scope, &s.Store) 232 | if err != nil { 233 | break 234 | } 235 | logging.Logger.Info("Resolved library environment", "env", libIdent, "location", file) 236 | f, ok := s.Store.Files.GetFromPath(file) 237 | if ok { 238 | f.mu.RLock() 239 | logging.Logger.Info("Setting New Scope to", "path", file) 240 | scope = f.Scope 241 | f.mu.RUnlock() 242 | if scope == nil { 243 | break 244 | } 245 | } 246 | } 247 | } 248 | ident = identSplit[len(identSplit)-1] 249 | 250 | loc, err = FindDefinition(ident, scope, &s.Store) 251 | 252 | logging.Logger.Info("Got definition as", "location", loc, "error", err) 253 | if err == nil { 254 | // Find references using location 255 | // FindReferences(loc, store) (Location[], error) 256 | // Parse file tree for references (parse new tree and query pure identifiers) 257 | // Go through scopes and check their expressions for references if it contains same symbol definition and remove from this file tree 258 | // Do same for all importers till no other importers (avoid cycles too) 259 | // startFile := loc.File 260 | // importers := s.Store.Dependencies.GetImporters(startFile) 261 | 262 | fileLocation := transport.Location{ 263 | URI: transport.DocumentURI(util.Path2URI(loc.File)), 264 | Range: loc.Range, 265 | } 266 | result, err := json.Marshal(fileLocation) 267 | if err == nil { 268 | return result, nil 269 | } 270 | } 271 | 272 | return []byte("null"), nil 273 | } 274 | 275 | func RefQuery(ident string) string { 276 | return fmt.Sprintf(` 277 | ((identifier) @l 278 | (#eq? @l %s) 279 | )`, ident) 280 | } 281 | 282 | func GetRefsForFile(ident string, path util.Path, store *Store) []Location { 283 | f, ok := store.Files.GetFromPath(path) 284 | if !ok { 285 | return []Location{} 286 | } 287 | 288 | locations := []Location{} 289 | 290 | // Parse through Scope 291 | tree := parser.ParseTree(f.Content) 292 | defer tree.Close() 293 | results := parser.GetQueryMatches(RefQuery(ident), f.Content, tree) 294 | 295 | totalRefs := make(map[transport.Range]struct{}) 296 | for _, result := range results.Results { 297 | for _, refs := range result { 298 | totalRefs[ToRange(&refs)] = struct{}{} 299 | } 300 | } 301 | 302 | // CleanUpRefs(ident, , currentRefs map[transport.Range]struct{}, content []byte) 303 | 304 | return locations 305 | } 306 | 307 | func CleanUpRefs(ident string, symbol *Symbol, currentRefs map[transport.Range]struct{}, content []byte) { 308 | // 1) Check if definition of same identifier exists 309 | defined := false 310 | for _, child := range symbol.Scope.Symbols { 311 | if child.Ident == ident { 312 | defined = true 313 | } 314 | } 315 | 316 | if defined { 317 | results := parser.GetQueryMatchesFromNode(RefQuery(ident), content, symbol.Expr) 318 | for _, resultType := range results.Results { 319 | for _, result := range resultType { 320 | delete(currentRefs, ToRange(&result)) 321 | } 322 | } 323 | } 324 | 325 | for _, child := range symbol.Scope.Symbols { 326 | if child.Scope != nil { 327 | CleanUpRefs(ident, child, currentRefs, content) 328 | } 329 | } 330 | } 331 | 332 | // Parse current scope, add to found references list. 333 | // Iterate through child scope recursively, remove from references list if found in child scope and scope has definition of same reference 334 | -------------------------------------------------------------------------------- /server/workspace.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "io/fs" 6 | "log/slog" 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "slices" 11 | "sync" 12 | 13 | "github.com/carn181/faustlsp/logging" 14 | "github.com/carn181/faustlsp/util" 15 | 16 | "github.com/fsnotify/fsnotify" 17 | cp "github.com/otiai10/copy" 18 | ) 19 | 20 | const faustConfigFile = ".faustcfg.json" 21 | 22 | type WorkspaceFiles []util.Path 23 | 24 | func (w WorkspaceFiles) LogValue() slog.Value { 25 | files := make([]any, 0, len(w)) 26 | for _, key := range w { 27 | files = append(files, map[string]any{ 28 | "path": key, 29 | }) 30 | } 31 | 32 | return slog.AnyValue(files) 33 | } 34 | 35 | type Workspace struct { 36 | // Path to Root Directory of Workspace 37 | Root string 38 | Files WorkspaceFiles 39 | mu sync.Mutex 40 | TDEvents chan TDEvent 41 | Config FaustProjectConfig 42 | 43 | // Temporary directory where this workspace is replicated 44 | tempDir util.Path 45 | openedFiles map[util.Handle]struct{} 46 | } 47 | 48 | func IsFaustFile(path util.Path) bool { 49 | ext := filepath.Ext(path) 50 | return ext == ".dsp" || ext == ".lib" 51 | } 52 | 53 | func IsDSPFile(path util.Path) bool { 54 | ext := filepath.Ext(path) 55 | return ext == ".dsp" 56 | } 57 | 58 | func IsLibFile(path util.Path) bool { 59 | ext := filepath.Ext(path) 60 | return ext == ".lib" 61 | } 62 | 63 | func (workspace *Workspace) TempDirPath(filePath util.Path) util.Path { 64 | result := filepath.Join(workspace.tempDir, filePath) 65 | return result 66 | } 67 | 68 | func (workspace *Workspace) Init(ctx context.Context, s *Server) { 69 | // Open all files in workspace and add to File Store 70 | workspace.Files = []util.Path{} 71 | workspace.TDEvents = make(chan TDEvent) 72 | workspace.openedFiles = make(map[util.Handle]struct{}) 73 | workspace.tempDir = s.tempDir 74 | 75 | // Replicate Workspace in our Temp Dir by copying 76 | logging.Logger.Info("Current workspace root", "path", workspace.Root) 77 | 78 | tempWorkspacePath := filepath.Join(s.tempDir, workspace.Root) 79 | err := cp.Copy(workspace.Root, tempWorkspacePath) 80 | if err != nil { 81 | logging.Logger.Error("Copying file error", "error", err) 82 | } 83 | logging.Logger.Info("Replicating Workspace in ", "path", tempWorkspacePath) 84 | 85 | // Parse Config File 86 | workspace.loadConfigFiles(s) 87 | 88 | // Open the files in file store 89 | err = filepath.Walk(workspace.Root, func(path string, info os.FileInfo, err error) error { 90 | if err != nil { 91 | return err 92 | } 93 | if !info.IsDir() { 94 | f, ok := s.Files.GetFromPath(path) 95 | 96 | if !ok { 97 | // Path relative to workspace 98 | logging.Logger.Info("Opening file from workspace\n", "path", path) 99 | 100 | s.Files.OpenFromPath(path) 101 | 102 | workspace.addFile(path) 103 | 104 | f, ok = s.Files.GetFromPath(path) 105 | if ok { 106 | workspace.DiagnoseFile(path, s) 107 | } 108 | } 109 | // Test if goroutine speeds this up 110 | if ok { 111 | if IsFaustFile(f.Handle.Path) { 112 | go workspace.AnalyzeFile(f, &s.Store) 113 | } 114 | } 115 | } 116 | return nil 117 | }) 118 | 119 | logging.Logger.Info("Workspace Files", "files", workspace.Files) 120 | logging.Logger.Info("File Store", "files", &s.Files) 121 | 122 | go func() { workspace.StartTrackingChanges(ctx, s) }() 123 | logging.Logger.Info("Started workspace watcher\n") 124 | } 125 | 126 | func (workspace *Workspace) loadConfigFiles(s *Server) { 127 | configFilePath := filepath.Join(workspace.Root, faustConfigFile) 128 | f, ok := s.Files.GetFromPath(configFilePath) 129 | var cfg FaustProjectConfig 130 | var err error 131 | if ok { 132 | f.mu.RLock() 133 | cfg, err = workspace.parseConfig(f.Content) 134 | f.mu.RUnlock() 135 | if err != nil { 136 | cfg = workspace.defaultConfig() 137 | } 138 | } else { 139 | // Try opening file if not opened but it exists 140 | s.Files.OpenFromPath(configFilePath) 141 | f, ok := s.Files.GetFromPath(configFilePath) 142 | if ok { 143 | f.mu.RLock() 144 | cfg, err = workspace.parseConfig(f.Content) 145 | f.mu.RUnlock() 146 | if err != nil { 147 | cfg = workspace.defaultConfig() 148 | } 149 | } else { 150 | cfg = workspace.defaultConfig() 151 | } 152 | } 153 | workspace.Config = cfg 154 | logging.Logger.Info("Workspace Config", "config", cfg) 155 | } 156 | 157 | // Track and Replicate Changes to workspace 158 | // TODO: Refactor and simplify 159 | // TODO: Avoid repetition of getting relative paths 160 | func (workspace *Workspace) StartTrackingChanges(ctx context.Context, s *Server) { 161 | // 1) Open All Files in Path with absolute Path recursively, store in s.Files, give pointers to Workspace.Files 162 | // 2) Copy Directory to TempDir Workspace 163 | // 3) Start Watching Changes like util 164 | // 3*) If File open, get changes from filebuffer 165 | // 3**) Replicate in disk + replicate in memory all these changes in both Files and Workspace.files 166 | 167 | // Ideal Pipeline 168 | // File Paths -> Content{Get from disk, Get from text document changes} -> Replicate in Disk TempDir -> ParseSymbols/Get Diagnostics from TempDir and Memory 169 | watcher, err := fsnotify.NewWatcher() 170 | if err != nil { 171 | logging.Logger.Error("Error in starting watcher", "error", err) 172 | } 173 | 174 | // Recursively add directories to watchlist 175 | watcher.Add(workspace.Root) 176 | err = filepath.Walk(workspace.Root, func(path string, info os.FileInfo, err error) error { 177 | if err != nil { 178 | return err 179 | } 180 | if info.IsDir() { 181 | watcher.Add(path) 182 | logging.Logger.Info("Adding directory to watcher\n", path, workspace.Root) 183 | } 184 | return nil 185 | }) 186 | 187 | for { 188 | select { 189 | // Editor TextDocument Events 190 | // Assumes Method Handler has handled this event and has this file in Files Store 191 | case change := <-workspace.TDEvents: 192 | logging.Logger.Info("Handling TD Event", "event", change) 193 | workspace.HandleEditorEvent(change, s) 194 | // Disk Events 195 | case event, ok := <-watcher.Events: 196 | logging.Logger.Info("Handling Workspace Disk Event", "event", event) 197 | if !ok { 198 | return 199 | } 200 | workspace.HandleDiskEvent(event, s, watcher) 201 | // Watcher Errors 202 | case _, ok := <-watcher.Errors: 203 | if !ok { 204 | return 205 | } 206 | // Cancel from parent 207 | case <-ctx.Done(): 208 | watcher.Close() 209 | return 210 | } 211 | } 212 | } 213 | 214 | func (workspace *Workspace) HandleDiskEvent(event fsnotify.Event, s *Server, watcher *fsnotify.Watcher) { 215 | // Path of original file 216 | origPath, err := filepath.Localize(event.Name) 217 | 218 | if err != nil { 219 | if runtime.GOOS == "windows" { 220 | logging.Logger.Error("Localizing error", "error", err) 221 | } 222 | origPath = event.Name 223 | } 224 | 225 | // If file of this path is already opened by editor, ignore this HandleDiskEvent 226 | _, open := workspace.openedFiles[util.FromPath(origPath)] 227 | if open { 228 | return 229 | } 230 | 231 | // Path relative to workspace 232 | relPath := origPath[len(workspace.Root)+1:] 233 | 234 | // Reload config file if changed 235 | if filepath.Base(relPath) == faustConfigFile { 236 | workspace.loadConfigFiles(s) 237 | workspace.cleanDiagnostics(s) 238 | } 239 | 240 | // The equivalent of the workspace file path for the temporary directory 241 | // Should be of the form TEMP_DIR/WORKSPACE_ROOT_PATH/relPath 242 | tempDirFilePath := workspace.TempDirPath(origPath) 243 | logging.Logger.Info("Got disk event for file", "path", origPath, "temp", tempDirFilePath, "event", event) 244 | 245 | // OS CREATE Event 246 | if event.Has(fsnotify.Create) { 247 | // Check if this is a rename Create or a normal new file create. fsnotify sends a rename and create event on file renames and the create event has the RenamedFrom field 248 | if event.RenamedFrom == "" { 249 | // Normal New File 250 | // Ensure path exists to copy 251 | // Sometimes files get deleted by text editors before this goroutine can handle it 252 | fi, err := os.Stat(origPath) 253 | if err != nil { 254 | return 255 | } 256 | 257 | if fi.IsDir() { 258 | // If a directory is being created, mkdir instead of create 259 | os.MkdirAll(tempDirFilePath, fi.Mode().Perm()) 260 | // Add this new directory to watch as watcher does not recursively watch subdirectories 261 | watcher.Add(origPath) 262 | } else { 263 | // Add it our server tracking and workspace 264 | s.Files.OpenFromPath(origPath) 265 | 266 | // Create File 267 | f, err := os.Create(tempDirFilePath) 268 | if err != nil { 269 | logging.Logger.Error("Create File error", "error", err) 270 | } 271 | f.Chmod(fi.Mode()) 272 | f.Close() 273 | 274 | workspace.addFile(origPath) 275 | } 276 | } else { 277 | // Rename Create 278 | oldFileRelPath := event.RenamedFrom[len(workspace.Root)+1:] 279 | oldTempPath := filepath.Join(workspace.tempDir, workspace.Root, oldFileRelPath) 280 | 281 | if util.IsValidPath(tempDirFilePath) && util.IsValidPath(oldTempPath) { 282 | err := os.Rename(oldTempPath, tempDirFilePath) 283 | if err != nil { 284 | return 285 | } 286 | } 287 | 288 | fi, _ := os.Stat(origPath) 289 | if fi.IsDir() { 290 | // Add this new directory to watch as watcher does not recursively watch subdirectories 291 | watcher.Add(origPath) 292 | } 293 | } 294 | } 295 | 296 | // OS REMOVE Event 297 | if event.Has(fsnotify.Remove) { 298 | // Remove from File Store, Workspace and Temp Directory 299 | s.Files.RemoveFromPath(origPath) 300 | workspace.removeFile(origPath) 301 | os.Remove(tempDirFilePath) 302 | } 303 | 304 | // OS WRITE Event 305 | if event.Has(fsnotify.Write) { 306 | contents, _ := os.ReadFile(origPath) 307 | os.WriteFile(tempDirFilePath, contents, fs.FileMode(os.O_TRUNC)) 308 | s.Files.ModifyFull(origPath, string(contents)) 309 | workspace.DiagnoseFile(origPath, s) 310 | } 311 | } 312 | 313 | func (workspace *Workspace) HandleEditorEvent(change TDEvent, s *Server) { 314 | // Temporary Directory 315 | tempDir := s.tempDir 316 | 317 | // Path of File that this Event affected 318 | origFilePath := change.Path 319 | 320 | // Reload config file if changed 321 | if filepath.Base(origFilePath) == faustConfigFile { 322 | workspace.loadConfigFiles(s) 323 | workspace.cleanDiagnostics(s) 324 | } 325 | 326 | file, ok := s.Files.GetFromPath(origFilePath) 327 | if !ok { 328 | logging.Logger.Error("File should've been in File Store.", "path", origFilePath) 329 | } 330 | 331 | tempDirFilePath := filepath.Join(tempDir, origFilePath) // Construct the temporary file path 332 | switch change.Type { 333 | case TDOpen: 334 | // Ensure directory exists before creating file. This mirrors the workspace's directory structure in the temp directory. 335 | // TODO: Add this and sub-directories to watcher 336 | dirPath := filepath.Dir(tempDirFilePath) 337 | if _, err := os.Stat(dirPath); os.IsNotExist(err) { 338 | err := os.MkdirAll(dirPath, 0755) // Create the directory and all parent directories with permissions 0755 339 | if err != nil { 340 | logging.Logger.Error("failed to create directory", "error", err) 341 | break 342 | } 343 | } 344 | 345 | // Create File in Temporary Directory. This creates an empty file at the temp path. 346 | f, err := os.Create(tempDirFilePath) 347 | if err != nil { 348 | logging.Logger.Error("OS create error", "error", err) 349 | } 350 | file, ok := s.Store.Files.GetFromPath(origFilePath) 351 | if ok { 352 | file.mu.RLock() 353 | os.WriteFile(tempDirFilePath, file.Content, fs.FileMode(os.O_TRUNC)) 354 | file.mu.RUnlock() 355 | 356 | } 357 | f.Close() 358 | case TDChange: 359 | // Write File to Temporary Directory. Updates the temporary file with the latest content from the editor. 360 | logging.Logger.Info("Writing recent change to", "path", tempDirFilePath) 361 | os.WriteFile(tempDirFilePath, file.Content, fs.FileMode(os.O_TRUNC)) // Write the file content to the temp file, overwriting existing content 362 | content, _ := os.ReadFile(tempDirFilePath) 363 | logging.Logger.Info("Current state of file", "path", tempDirFilePath, "content", string(content)) 364 | go s.Workspace.AnalyzeFile(file, &s.Store) 365 | workspace.DiagnoseFile(origFilePath, s) 366 | 367 | case TDClose: 368 | // Sync file from disk on close if it exists and replicate it to temporary directory, else remove from Files Store 369 | if util.IsValidPath(origFilePath) { // Check if the file path is valid 370 | s.Files.OpenFromPath(origFilePath) // Reload the file from the specified path. 371 | 372 | file, ok := s.Files.GetFromPath(origFilePath) // Retrieve the file again (unnecessary, can use the previous `file`) 373 | if ok { 374 | os.WriteFile(tempDirFilePath, file.Content, os.FileMode(os.O_TRUNC)) // Write content to temporary file, replicating it from disk. 375 | } 376 | workspace.addFile(origFilePath) 377 | } else { 378 | s.Files.RemoveFromPath(origFilePath) // Remove the file from the file store if the path isn't valid 379 | } 380 | 381 | } 382 | } 383 | 384 | func (workspace *Workspace) EditorOpenFile(uri util.URI, files *Files) { 385 | files.OpenFromURI(uri) 386 | handle, _ := util.FromURI(uri) 387 | workspace.openedFiles[handle] = struct{}{} 388 | } 389 | 390 | func (workspace *Workspace) addFile(path util.Path) { 391 | workspace.mu.Lock() 392 | workspace.Files = append(workspace.Files, path) 393 | workspace.mu.Unlock() 394 | } 395 | 396 | func (w *Workspace) DiagnoseFile(path util.Path, s *Server) { 397 | if IsFaustFile(path) { 398 | logging.Logger.Info("Diagnosing File", "path", path) 399 | 400 | params := s.Files.TSDiagnostics(path) 401 | logging.Logger.Info("Got Diagnose File", "params", params) 402 | if params.URI != "" { 403 | s.diagChan <- params 404 | } 405 | if len(params.Diagnostics) == 0 { 406 | // Compiler Diagnostics if exists 407 | if w.Config.CompilerDiagnostics { 408 | logging.Logger.Info("Generating Compiler errors as no syntax errors") 409 | w.sendCompilerDiagnostics(s) 410 | } 411 | } 412 | } 413 | } 414 | 415 | func (workspace *Workspace) removeFile(path util.Path) { 416 | workspace.mu.Lock() 417 | for i, filePath := range workspace.Files { 418 | if filePath == path { 419 | workspace.Files = slices.Delete(workspace.Files, i, i) 420 | } 421 | } 422 | workspace.mu.Unlock() 423 | } 424 | -------------------------------------------------------------------------------- /test/incremental_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/carn181/faustlsp/server" 8 | "github.com/carn181/faustlsp/transport" 9 | ) 10 | 11 | func TestGetLines(t *testing.T) { 12 | testStr := "abcde" 13 | indices := server.GetLineIndices(testStr) 14 | for _, c := range indices { 15 | if c >= uint(len(indices)) { 16 | break 17 | } 18 | if c != 0 && testStr[c] != '\n' { 19 | t.Errorf("%c at %d not newline", testStr[c], c) 20 | } 21 | } 22 | // t.Error("") 23 | } 24 | 25 | func TestPositionToOffset(t *testing.T) { 26 | tests := []struct { 27 | name string 28 | text string 29 | pos transport.Position 30 | encoding string 31 | want uint 32 | wantErr bool 33 | }{ 34 | { 35 | name: "Empty string, position 0,0", 36 | text: "", 37 | pos: transport.Position{Line: 0, Character: 0}, 38 | encoding: "utf-16", 39 | want: 0, 40 | wantErr: false, 41 | }, 42 | { 43 | name: "Single line, position at end", 44 | text: "abc", 45 | pos: transport.Position{Line: 0, Character: 3}, 46 | encoding: "utf-16", 47 | want: 3, 48 | wantErr: false, 49 | }, 50 | { 51 | name: "Single line, position out of bounds", 52 | text: "abc", 53 | pos: transport.Position{Line: 0, Character: 10}, 54 | encoding: "utf-16", 55 | want: 3, 56 | wantErr: false, 57 | }, 58 | { 59 | name: "Multi-line, start of second line", 60 | text: "abc\ndef", 61 | pos: transport.Position{Line: 1, Character: 0}, 62 | encoding: "utf-16", 63 | want: 4, 64 | wantErr: false, 65 | }, 66 | { 67 | name: "Multi-line, end of second line", 68 | text: "abc\ndef", 69 | pos: transport.Position{Line: 1, Character: 3}, 70 | encoding: "utf-16", 71 | want: 7, 72 | wantErr: false, 73 | }, 74 | { 75 | name: "Position beyond last line", 76 | text: "abc\ndef", 77 | pos: transport.Position{Line: 2, Character: 0}, 78 | encoding: "utf-16", 79 | want: 7, 80 | wantErr: false, 81 | }, 82 | { 83 | name: "Unicode character, utf-16 encoding", 84 | text: "a😆b\nc", 85 | pos: transport.Position{Line: 0, Character: 3}, 86 | encoding: "utf-16", 87 | want: 5, // 'a' + '😆' (2 code units in utf-16, but offset is still 2 in Go string) 88 | wantErr: false, 89 | }, 90 | { 91 | name: "Unicode character, utf-32 encoding", 92 | text: "a😆b\nc", 93 | pos: transport.Position{Line: 0, Character: 2}, 94 | encoding: "utf-32", 95 | want: 5, 96 | wantErr: false, 97 | }, 98 | { 99 | name: "Position at start of file", 100 | text: "abc\ndef", 101 | pos: transport.Position{Line: 0, Character: 0}, 102 | encoding: "utf-16", 103 | want: 0, 104 | wantErr: false, 105 | }, 106 | { 107 | name: "Position at end of first line", 108 | text: "abc\ndef", 109 | pos: transport.Position{Line: 0, Character: 3}, 110 | encoding: "utf-16", 111 | want: 3, 112 | wantErr: false, 113 | }, 114 | { 115 | name: "Position at start of second line", 116 | text: "abc\ndef", 117 | pos: transport.Position{Line: 1, Character: 0}, 118 | encoding: "utf-16", 119 | want: 4, 120 | wantErr: false, 121 | }, 122 | { 123 | name: "Position at end of second line", 124 | text: "abc\ndef", 125 | pos: transport.Position{Line: 1, Character: 3}, 126 | encoding: "utf-16", 127 | want: 7, 128 | wantErr: false, 129 | }, 130 | { 131 | name: "Position beyond last line", 132 | text: "abc\ndef", 133 | pos: transport.Position{Line: 2, Character: 0}, 134 | encoding: "utf-16", 135 | want: 7, 136 | wantErr: false, 137 | }, 138 | { 139 | name: "Position beyond last character in line", 140 | text: "abc\ndef", 141 | pos: transport.Position{Line: 1, Character: 100}, 142 | encoding: "utf-16", 143 | want: 7, 144 | wantErr: false, 145 | }, 146 | { 147 | name: "Empty string, position 0,0", 148 | text: "", 149 | pos: transport.Position{Line: 0, Character: 0}, 150 | encoding: "utf-16", 151 | want: 0, 152 | wantErr: false, 153 | }, 154 | { 155 | name: "String with only newlines", 156 | text: "\n\n\n", 157 | pos: transport.Position{Line: 2, Character: 0}, 158 | encoding: "utf-16", 159 | want: 2, 160 | wantErr: false, 161 | }, 162 | { 163 | name: "Unicode emoji, position at emoji", 164 | text: "a💚c", 165 | pos: transport.Position{Line: 0, Character: 2}, 166 | encoding: "utf-16", 167 | want: 5, 168 | wantErr: false, 169 | }, 170 | { 171 | name: "Unicode emoji, position after emoji", 172 | text: "a💚c", 173 | pos: transport.Position{Line: 0, Character: 3}, 174 | encoding: "utf-16", 175 | want: 5, 176 | wantErr: false, 177 | }, 178 | { 179 | name: "Tabs and spaces", 180 | text: "a\tb c\n d", 181 | pos: transport.Position{Line: 1, Character: 2}, 182 | encoding: "utf-16", 183 | want: 8, 184 | wantErr: false, 185 | }, 186 | } 187 | 188 | for _, tt := range tests { 189 | t.Run(tt.name, func(t *testing.T) { 190 | got, err := server.PositionToOffset(tt.pos, tt.text, tt.encoding) 191 | if (err != nil) != tt.wantErr { 192 | t.Errorf("PositionToOffset() error = %v, wantErr %v", err, tt.wantErr) 193 | return 194 | } 195 | if got != tt.want { 196 | t.Errorf("PositionToOffset() = %v, want %v", got, tt.want) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestOffsetToPosition(t *testing.T) { 203 | tests := []struct { 204 | name string 205 | text string 206 | offset uint 207 | encoding string 208 | want transport.Position 209 | wantErr bool 210 | }{ 211 | { 212 | name: "Empty string, offset 0", 213 | text: "", 214 | offset: 0, 215 | encoding: "utf-16", 216 | want: transport.Position{Line: 0, Character: 0}, 217 | wantErr: false, 218 | }, 219 | { 220 | name: "Single line, offset at end", 221 | text: "abc", 222 | offset: 3, 223 | encoding: "utf-16", 224 | want: transport.Position{Line: 0, Character: 3}, 225 | wantErr: false, 226 | }, 227 | { 228 | name: "Single line, offset out of bounds", 229 | text: "abc", 230 | offset: 10, 231 | encoding: "utf-16", 232 | want: transport.Position{Line: 0, Character: 3}, 233 | wantErr: false, 234 | }, 235 | { 236 | name: "Multi-line, start of second line", 237 | text: "abc\ndef", 238 | offset: 4, 239 | encoding: "utf-16", 240 | want: transport.Position{Line: 1, Character: 0}, 241 | wantErr: false, 242 | }, 243 | { 244 | name: "Multi-line, end of second line", 245 | text: "abc\ndef", 246 | offset: 7, 247 | encoding: "utf-16", 248 | want: transport.Position{Line: 1, Character: 3}, 249 | wantErr: false, 250 | }, 251 | { 252 | name: "Offset beyond last character", 253 | text: "abc\ndef", 254 | offset: 100, 255 | encoding: "utf-16", 256 | want: transport.Position{Line: 1, Character: 3}, 257 | wantErr: false, 258 | }, 259 | { 260 | name: "Unicode character, utf-16 encoding", 261 | text: "a😆b\nc", 262 | offset: 5, 263 | encoding: "utf-16", 264 | want: transport.Position{Line: 0, Character: 3}, 265 | wantErr: false, 266 | }, 267 | { 268 | name: "Unicode emoji, position at emoji", 269 | text: "a💚c", 270 | offset: 5, 271 | encoding: "utf-16", 272 | want: transport.Position{Line: 0, Character: 3}, 273 | wantErr: false, 274 | }, 275 | { 276 | name: "Tabs and spaces", 277 | text: "a\tb c\n d", 278 | offset: 8, 279 | encoding: "utf-16", 280 | want: transport.Position{Line: 1, Character: 2}, 281 | wantErr: false, 282 | }, 283 | { 284 | name: "String with only newlines", 285 | text: "\n\n\n", 286 | offset: 2, 287 | encoding: "utf-16", 288 | want: transport.Position{Line: 2, Character: 0}, 289 | wantErr: false, 290 | }, 291 | { 292 | name: "Offset at start of file", 293 | text: "abc\ndef", 294 | offset: 0, 295 | encoding: "utf-16", 296 | want: transport.Position{Line: 0, Character: 0}, 297 | wantErr: false, 298 | }, 299 | { 300 | name: "Offset at newline character", 301 | text: "abc\ndef", 302 | offset: 3, 303 | encoding: "utf-16", 304 | want: transport.Position{Line: 0, Character: 3}, 305 | wantErr: false, 306 | }, 307 | { 308 | name: "Negative offset (invalid)", 309 | text: "abc\ndef", 310 | offset: ^uint(0), // max uint, simulates negative for error 311 | encoding: "utf-16", 312 | want: transport.Position{Line: 1, Character: 3}, 313 | wantErr: false, 314 | }, 315 | } 316 | 317 | for _, tt := range tests { 318 | t.Run(tt.name, func(t *testing.T) { 319 | got, err := server.OffsetToPosition(tt.offset, tt.text, tt.encoding) 320 | if (err != nil) != tt.wantErr { 321 | t.Errorf("OffsetToPosition() error = %v, wantErr %v", err, tt.wantErr) 322 | return 323 | } 324 | if got != tt.want { 325 | t.Errorf("OffsetToPosition() = %v, want %v", got, tt.want) 326 | } 327 | }) 328 | } 329 | } 330 | 331 | type IncrementalTest struct { 332 | text string 333 | pos transport.Position 334 | off uint 335 | } 336 | 337 | func testIncremental(t IncrementalTest) error { 338 | p, _ := server.OffsetToPosition(t.off, t.text, "utf-16") 339 | o, _ := server.PositionToOffset(t.pos, t.text, "utf-16") 340 | if p != t.pos { 341 | return fmt.Errorf("Expected: %v, Found: %v\n", t.pos, p) 342 | } 343 | if o != t.off { 344 | return fmt.Errorf("Expected: %v, Found: %v\n", t.off, o) 345 | } 346 | return nil 347 | } 348 | 349 | func TestApplyIncrementalChange(t *testing.T) { 350 | tests := []struct { 351 | name string 352 | original string 353 | changeRange transport.Range 354 | newText string 355 | encoding string 356 | want string 357 | }{ 358 | { 359 | name: "Replace middle of line", 360 | original: "abcdef", 361 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 2}, End: transport.Position{Line: 0, Character: 4}}, 362 | newText: "XY", 363 | encoding: "utf-16", 364 | want: "abXYef", 365 | }, 366 | { 367 | name: "Insert at start", 368 | original: "abcdef", 369 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 0}, End: transport.Position{Line: 0, Character: 0}}, 370 | newText: "123", 371 | encoding: "utf-16", 372 | want: "123abcdef", 373 | }, 374 | { 375 | name: "Insert at end", 376 | original: "abcdef", 377 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 6}, End: transport.Position{Line: 0, Character: 6}}, 378 | newText: "XYZ", 379 | encoding: "utf-16", 380 | want: "abcdefXYZ", 381 | }, 382 | { 383 | name: "Delete range", 384 | original: "abcdef", 385 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 2}, End: transport.Position{Line: 0, Character: 5}}, 386 | newText: "", 387 | encoding: "utf-16", 388 | want: "abf", 389 | }, 390 | { 391 | name: "Replace whole document", 392 | original: "abcdef", 393 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 0}, End: transport.Position{Line: 0, Character: 6}}, 394 | newText: "xyz", 395 | encoding: "utf-16", 396 | want: "xyz", 397 | }, 398 | { 399 | name: "Undo: revert to previous state", 400 | original: "abXYef", 401 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 2}, End: transport.Position{Line: 0, Character: 4}}, 402 | newText: "cd", 403 | encoding: "utf-16", 404 | want: "abcdef", 405 | }, 406 | { 407 | name: "Out of bounds range (end too large)", 408 | original: "abcdef", 409 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 4}, End: transport.Position{Line: 0, Character: 100}}, 410 | newText: "ZZ", 411 | encoding: "utf-16", 412 | want: "abcdZZ", 413 | }, 414 | { 415 | name: "Multi-line replace", 416 | original: "abc\ndef\nghi", 417 | changeRange: transport.Range{Start: transport.Position{Line: 1, Character: 0}, End: transport.Position{Line: 2, Character: 3}}, 418 | newText: "XYZ", 419 | encoding: "utf-16", 420 | want: "abc\nXYZ", 421 | }, 422 | { 423 | name: "Insert newline", 424 | original: "abc\ndef", 425 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 3}, End: transport.Position{Line: 0, Character: 3}}, 426 | newText: "\n", 427 | encoding: "utf-16", 428 | want: "abc\n\ndef", 429 | }, 430 | { 431 | name: "Undo: insert then remove", 432 | original: "abc\n\ndef", 433 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 3}, End: transport.Position{Line: 1, Character: 0}}, 434 | newText: "", 435 | encoding: "utf-16", 436 | want: "abc\ndef", 437 | }, 438 | { 439 | name: "Insert at empty document", 440 | original: "", 441 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 0}, End: transport.Position{Line: 0, Character: 0}}, 442 | newText: "hello", 443 | encoding: "utf-16", 444 | want: "hello", 445 | }, 446 | { 447 | name: "Replace with empty string (delete all)", 448 | original: "abcdef", 449 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 0}, End: transport.Position{Line: 0, Character: 6}}, 450 | newText: "", 451 | encoding: "utf-16", 452 | want: "", 453 | }, 454 | { 455 | name: "Insert at end of multi-line document", 456 | original: "abc\ndef\nghi", 457 | changeRange: transport.Range{Start: transport.Position{Line: 2, Character: 3}, End: transport.Position{Line: 2, Character: 3}}, 458 | newText: "XYZ", 459 | encoding: "utf-16", 460 | want: "abc\ndef\nghiXYZ", 461 | }, 462 | { 463 | name: "Replace across multiple lines with longer text", 464 | original: "abc\ndef\nghi", 465 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 1}, End: transport.Position{Line: 2, Character: 2}}, 466 | newText: "LONGREPLACEMENT", 467 | encoding: "utf-16", 468 | want: "aLONGREPLACEMENTi", 469 | }, 470 | { 471 | name: "Insert unicode emoji", 472 | original: "abc", 473 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 1}, End: transport.Position{Line: 0, Character: 1}}, 474 | newText: "💚", 475 | encoding: "utf-16", 476 | want: "a💚bc", 477 | }, 478 | { 479 | name: "Replace with only newlines", 480 | original: "abc\ndef", 481 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 0}, End: transport.Position{Line: 1, Character: 3}}, 482 | newText: "\n\n\n", 483 | encoding: "utf-16", 484 | want: "\n\n\n", 485 | }, 486 | { 487 | name: "Insert at very large character index", 488 | original: "abc", 489 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 1000}, End: transport.Position{Line: 0, Character: 1000}}, 490 | newText: "XYZ", 491 | encoding: "utf-16", 492 | want: "abcXYZ", 493 | }, 494 | { 495 | name: "Replace with multi-line text", 496 | original: "abc\ndef", 497 | changeRange: transport.Range{Start: transport.Position{Line: 0, Character: 1}, End: transport.Position{Line: 1, Character: 2}}, 498 | newText: "1\n2\n3", 499 | encoding: "utf-16", 500 | want: "a1\n2\n3f", 501 | }, 502 | } 503 | 504 | for _, tt := range tests { 505 | t.Run(tt.name, func(t *testing.T) { 506 | got := server.ApplyIncrementalChange(tt.changeRange, tt.newText, tt.original, tt.encoding) 507 | if got != tt.want { 508 | t.Errorf("ApplyIncrementalChange() = %q, want %q", got, tt.want) 509 | } 510 | }) 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /server/symbols.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "log/slog" 7 | "os/exec" 8 | "path/filepath" 9 | "slices" 10 | "strings" 11 | "sync" 12 | "time" 13 | "unicode" 14 | 15 | "github.com/carn181/faustlsp/logging" 16 | "github.com/carn181/faustlsp/parser" 17 | "github.com/carn181/faustlsp/transport" 18 | "github.com/carn181/faustlsp/util" 19 | tree_sitter "github.com/tree-sitter/go-tree-sitter" 20 | ) 21 | 22 | type SymbolKind int 23 | 24 | const ( 25 | // Identifier is a simple identifier with no scope or expression 26 | Identifier SymbolKind = iota 27 | // Definition is a simple variable or function along with expression 28 | Definition 29 | 30 | // Function has single scope for arguments along with expression 31 | Function 32 | 33 | // Pattern scope has multiple scopes and expressions for each rule. 34 | Case 35 | 36 | // Just like Function, but without identifier 37 | Rule 38 | 39 | // Iteration (par, seq, sum, prod) 40 | Iteration 41 | 42 | // with and letrec environments have scope as well as expression 43 | WithEnvironment 44 | LetRecEnvironment 45 | 46 | // Environment has a scope along with identifier it is assigned to 47 | Environment 48 | 49 | // Library has a file path along with identifier it is assigned to 50 | Library 51 | 52 | // Import simply has a file path 53 | Import 54 | ) 55 | 56 | var symbolKindStrings = map[SymbolKind]string{ 57 | Identifier: "Identifier", 58 | Iteration: "Iteration", 59 | Definition: "Definition", 60 | Function: "Function", 61 | Case: "Case", 62 | Rule: "Rule", 63 | WithEnvironment: "WithEnvironment", 64 | LetRecEnvironment: "LetRecEnvironment", 65 | Environment: "Environment", 66 | Library: "Library", 67 | Import: "Import", 68 | } 69 | 70 | func (k SymbolKind) String() string { 71 | s, ok := symbolKindStrings[k] 72 | if ok { 73 | return s 74 | } 75 | return "UnknownSymbolKind" 76 | } 77 | 78 | type Symbol struct { 79 | Kind SymbolKind 80 | Loc Location 81 | Ident string 82 | Scope *Scope 83 | 84 | // For Case's Rules 85 | Children []Symbol 86 | 87 | // Useful for populating reference map after parsing AST 88 | Expr *tree_sitter.Node 89 | // For with_environments and letrec_environments, useful for references and environment symbols 90 | Expression *Scope 91 | 92 | // File path to import scope from 93 | File util.Path 94 | 95 | // Documentation 96 | Docs Documentation 97 | } 98 | 99 | type Documentation struct { 100 | Full string 101 | Usage string 102 | } 103 | 104 | func ParseDocumentation(node *tree_sitter.Node, content []byte) Documentation { 105 | if node == nil { 106 | return Documentation{Full: "", Usage: ""} 107 | } 108 | 109 | docContent := []string{} 110 | curr := node 111 | 112 | // Traverse previous siblings until we find a non-comment node 113 | for { 114 | curr = curr.PrevSibling() 115 | if curr == nil { 116 | break 117 | } 118 | if curr.GrammarName() != "comment" { 119 | break 120 | } 121 | 122 | lineContent := curr.Utf8Text(content) 123 | lineContent = lineContent[len("//"):] 124 | // Double spaces for markdown 125 | docContent = slices.Insert(docContent, 0, lineContent) 126 | } 127 | 128 | usage := "" 129 | if len(docContent) > 1 { 130 | usage = docContent[1] 131 | } else if len(docContent) == 1 { 132 | usage = docContent[0] 133 | } 134 | 135 | doc := Documentation{ 136 | Full: strings.Join(docContent, " \n"), 137 | Usage: usage, 138 | } 139 | logging.Logger.Info("Parsed docs", "documentation", doc) 140 | return doc 141 | } 142 | 143 | func NewIdentifier(Loc Location, Ident string) Symbol { 144 | return Symbol{ 145 | Kind: Identifier, 146 | Loc: Loc, 147 | Ident: Ident, 148 | } 149 | } 150 | 151 | func NewDefinition(Loc Location, Ident string, Expr *tree_sitter.Node, Expression *Scope, Docs Documentation) Symbol { 152 | return Symbol{ 153 | Kind: Definition, 154 | Loc: Loc, 155 | Ident: Ident, 156 | Expr: Expr, 157 | Expression: Expression, 158 | Docs: Docs, 159 | } 160 | } 161 | 162 | func NewFunction(Loc Location, Ident string, Scope *Scope, Expr *tree_sitter.Node, Expression *Scope, Docs Documentation) Symbol { 163 | return Symbol{ 164 | Kind: Function, 165 | Loc: Loc, 166 | Ident: Ident, 167 | Scope: Scope, 168 | Expression: Expression, 169 | Docs: Docs, 170 | } 171 | } 172 | 173 | func NewCase(Loc Location, Children []Symbol) Symbol { 174 | return Symbol{ 175 | Kind: Case, 176 | Loc: Loc, 177 | // For Case Rules 178 | Children: Children, 179 | } 180 | } 181 | 182 | func NewRule(Loc Location, Scope *Scope, Expr *tree_sitter.Node) Symbol { 183 | return Symbol{ 184 | Kind: Rule, 185 | Loc: Loc, 186 | Scope: Scope, 187 | Expr: Expr, 188 | } 189 | } 190 | 191 | func NewIteration(Loc Location, Scope *Scope, Expr *tree_sitter.Node) Symbol { 192 | return Symbol{ 193 | Kind: Iteration, 194 | Loc: Loc, 195 | Scope: Scope, 196 | Expr: Expr, 197 | } 198 | } 199 | 200 | func NewWithEnvironment(Loc Location, Scope *Scope, Expr *tree_sitter.Node, Expression *Scope) Symbol { 201 | return Symbol{ 202 | Kind: WithEnvironment, 203 | Loc: Loc, 204 | Scope: Scope, 205 | Expr: Expr, 206 | Expression: Expression, 207 | } 208 | } 209 | 210 | func NewLetRecEnvironment(Loc Location, Scope *Scope, Expr *tree_sitter.Node, Expression *Scope) Symbol { 211 | return Symbol{ 212 | Kind: LetRecEnvironment, 213 | Loc: Loc, 214 | Scope: Scope, 215 | Expr: Expr, 216 | Expression: Expression, 217 | } 218 | } 219 | 220 | func NewEnvironment(Loc Location, Ident string, Scope *Scope) Symbol { 221 | return Symbol{ 222 | Kind: Environment, 223 | Ident: Ident, 224 | Loc: Loc, 225 | Scope: Scope, 226 | } 227 | } 228 | 229 | func NewLibrary(Loc Location, importedFile util.Path, Ident string) Symbol { 230 | return Symbol{ 231 | Kind: Library, 232 | Ident: Ident, 233 | Loc: Loc, 234 | File: importedFile, 235 | } 236 | } 237 | 238 | func NewImport(Loc Location, importedFile util.Path) Symbol { 239 | return Symbol{ 240 | Kind: Import, 241 | Loc: Loc, 242 | File: importedFile, 243 | } 244 | } 245 | 246 | type Location struct { 247 | File util.Path 248 | Range transport.Range 249 | } 250 | 251 | type Scope struct { 252 | Parent *Scope 253 | Symbols []*Symbol 254 | Children []*Scope 255 | Range transport.Range 256 | } 257 | 258 | func (s *Scope) LogValue() slog.Value { 259 | return slog.GroupValue( 260 | slog.Any("symbols", s.Symbols), 261 | slog.Any("range", s.Range), 262 | ) 263 | } 264 | 265 | func NewScope(parent *Scope, scopeRange transport.Range) *Scope { 266 | scope := Scope{ 267 | Parent: parent, 268 | Symbols: []*Symbol{}, 269 | Children: []*Scope{}, 270 | Range: scopeRange, 271 | } 272 | if parent != nil { 273 | parent.Children = append(parent.Children, &scope) 274 | } 275 | 276 | return &scope 277 | } 278 | 279 | func (scope *Scope) addSymbol(sym *Symbol) { 280 | scope.Symbols = append(scope.Symbols, sym) 281 | } 282 | 283 | // DependencyGraph manages the import relationships between files. 284 | type DependencyGraph struct { 285 | mu sync.RWMutex // Protects concurrent access 286 | 287 | // Adjacency list: maps an importer's Path to a set of Paths it imports. 288 | // We use map[string]struct{} for a set. 289 | imports map[string]map[string]struct{} 290 | 291 | // Reverse adjacency list: maps an imported Path to a set of Paths that import it. 292 | // This is crucial for efficiently finding "dependents" when a file changes. 293 | // If found and string != "", it is a library import (used for reference finding) 294 | importedBy map[string]map[string]string 295 | 296 | // Tracks files currently being analyzed/processed to detect cycles. 297 | // Maps file Path to true if it's currently in the analysis stack. 298 | processing map[string]bool 299 | } 300 | 301 | func NewDependencyGraph() DependencyGraph { 302 | return DependencyGraph{ 303 | imports: make(map[string]map[string]struct{}), 304 | importedBy: make(map[string]map[string]string), 305 | processing: make(map[string]bool), 306 | } 307 | } 308 | 309 | // AddDependency records that 'importerPath' imports 'importedPath'. 310 | func (dg *DependencyGraph) AddDependency(importerPath, importedPath util.Path) { 311 | dg.mu.Lock() 312 | defer dg.mu.Unlock() 313 | 314 | if _, ok := dg.imports[importerPath]; !ok { 315 | dg.imports[importerPath] = make(map[string]struct{}) 316 | } 317 | dg.imports[importerPath][importedPath] = struct{}{} 318 | 319 | if _, ok := dg.importedBy[importedPath]; !ok { 320 | dg.importedBy[importedPath] = make(map[string]string) 321 | } 322 | dg.importedBy[importedPath][importerPath] = "" 323 | } 324 | 325 | func (dg *DependencyGraph) AddLibraryDependency(importerPath, importedPath util.Path, library string) { 326 | dg.mu.Lock() 327 | defer dg.mu.Unlock() 328 | 329 | if _, ok := dg.imports[importerPath]; !ok { 330 | dg.imports[importerPath] = make(map[string]struct{}) 331 | } 332 | dg.imports[importerPath][importedPath] = struct{}{} 333 | 334 | if _, ok := dg.importedBy[importedPath]; !ok { 335 | dg.importedBy[importedPath] = make(map[string]string) 336 | } 337 | dg.importedBy[importedPath][importerPath] = library 338 | } 339 | 340 | // Call this before re-analyzing a file, as its imports might have changed. 341 | func (dg *DependencyGraph) RemoveDependenciesForFile(path util.Path) { 342 | dg.mu.Lock() 343 | defer dg.mu.Unlock() 344 | 345 | // Remove its outgoing dependencies 346 | if importedPaths, ok := dg.imports[path]; ok { 347 | for importedPath := range importedPaths { 348 | delete(dg.importedBy[importedPath], path) // Remove self from imported's list 349 | if len(dg.importedBy[importedPath]) == 0 { 350 | delete(dg.importedBy, importedPath) // Clean up empty sets 351 | } 352 | } 353 | delete(dg.imports, path) // Remove its own entry 354 | } 355 | 356 | // Remove any incoming dependencies (if another file was importing it) 357 | // This is effectively handled by the other file being re-analyzed or removed. 358 | // But good to clean up if the file itself is deleted. 359 | delete(dg.importedBy, path) // If this file was being imported 360 | } 361 | 362 | // GetImporters returns a list of URIs that import the given file. 363 | func (dg *DependencyGraph) GetImporters(path string) []string { 364 | dg.mu.RLock() 365 | defer dg.mu.RUnlock() 366 | 367 | importers := []string{} 368 | if s, ok := dg.importedBy[path]; ok { 369 | for importerPath := range s { 370 | importers = append(importers, importerPath) 371 | } 372 | } 373 | return importers 374 | } 375 | 376 | type SymbolKey struct { 377 | File util.Path 378 | Name string 379 | Line uint 380 | Char uint 381 | } 382 | 383 | type ReferenceMap struct { 384 | references map[SymbolKey][]Symbol 385 | } 386 | 387 | type Store struct { 388 | mu sync.Mutex 389 | Files *Files 390 | References ReferenceMap 391 | Dependencies DependencyGraph 392 | Cache map[[sha256.Size]byte]*Scope 393 | } 394 | 395 | // This needs workspace to be able to resolve the file path 396 | // Analyzes AST of a File and updates the store 397 | func (workspace *Workspace) AnalyzeFile(f *File, store *Store) { 398 | // 3) After 1) and 2) are done, resolve all symbols as references 399 | 400 | var visited = make(map[util.Path]struct{}) 401 | 402 | // Stack for files to parse after current file 403 | var fileChan = make(chan string) 404 | 405 | // Parse through file import tree asynchronously to speed up parsing times using a pipeline 406 | go func() { 407 | for { 408 | select { 409 | case currentFile := <-fileChan: 410 | logging.Logger.Info("Parsing file", "file", currentFile) 411 | f, ok := store.Files.GetFromPath(currentFile) 412 | //logging.Logger.Info("AST Traversal: Got library definition", "file", current, "ident", identName) 413 | if ok { 414 | go workspace.ParseFile(f, store, visited, fileChan) 415 | 416 | } else { 417 | store.Files.OpenFromPath(currentFile) 418 | f, ok := store.Files.GetFromPath(currentFile) 419 | if ok { 420 | go workspace.ParseFile(f, store, visited, fileChan) 421 | } 422 | 423 | } 424 | // Close file channel after 30 seconds 425 | // TODO: Find way to close channel when all files are done parsing 426 | case <-time.After(5 * time.Second): 427 | logging.Logger.Info("Closing file channel as nothing received for 5 seconds") 428 | close(fileChan) 429 | return 430 | } 431 | } 432 | }() 433 | 434 | logging.Logger.Info("Starting to analyze file", "path", f.Handle.Path) 435 | workspace.ParseFile(f, store, visited, fileChan) 436 | 437 | logging.Logger.Info("AST Parsing completed for file", "file", f.Handle.Path) 438 | // logging.Logger.Info("Dependency Graph", "graph", store.Dependencies.imports) 439 | } 440 | 441 | func (workspace *Workspace) ParseFile(f *File, store *Store, visited map[util.Path]struct{}, fileChan chan string) { 442 | // If file is already visited, skip it 443 | if _, ok := visited[f.Handle.Path]; !ok { 444 | f.mu.Lock() 445 | // Check if file content of this type is already parsed 446 | scope, ok := store.Cache[f.Hash] 447 | if ok { 448 | logging.Logger.Info("File already parsed, using cached scope", "file", f.Handle.Path) 449 | f.Scope = scope 450 | f.mu.Unlock() 451 | } else { 452 | 453 | tree := parser.ParseTree(f.Content) 454 | root := tree.RootNode() 455 | scope := NewScope(nil, ToRange(root)) 456 | visited[f.Handle.Path] = struct{}{} 457 | workspace.ParseASTNode(root, f, scope, store, visited, fileChan) 458 | f.Scope = scope 459 | store.Cache[f.Hash] = scope 460 | f.mu.Unlock() 461 | 462 | // tree.Close() 463 | logging.Logger.Info("Parsed file", "path", f.Handle.Path) 464 | } 465 | } else { 466 | logging.Logger.Info("Skipping file as it is already visited", "file", f.Handle.Path) 467 | } 468 | 469 | } 470 | 471 | func (workspace *Workspace) ParseASTNode(node *tree_sitter.Node, currentFile *File, scope *Scope, store *Store, visited map[util.Path]struct{}, fileChan chan string) { 472 | // Parse Symbols recursively. Map from tree_sitter.Node -> a Symbol type 473 | if node == nil { 474 | logging.Logger.Error("AST Parsing Traversal Error: Node is nil", "node", node) 475 | return 476 | } 477 | 478 | name := node.GrammarName() 479 | 480 | switch name { 481 | case "definition": 482 | logging.Logger.Info("AST Traversal: Got definition") 483 | 484 | value := node.ChildByFieldName("value") 485 | ident := node.ChildByFieldName("variable") 486 | if value == nil { 487 | logging.Logger.Info("AST Traversal: Got definition without value. Ignoring.") 488 | return 489 | } 490 | 491 | valueGrammarName := value.GrammarName() 492 | identName := ident.Utf8Text(currentFile.Content) 493 | 494 | if valueGrammarName == "library" { 495 | logging.Logger.Info("AST Traversal: Got library") 496 | 497 | fileName := value.ChildByFieldName("filename") 498 | if fileName == nil { 499 | logging.Logger.Error("AST Traversal: Library definition without filename", "node", node) 500 | return 501 | } 502 | 503 | libraryFilePath := stripQuotes(fileName.Utf8Text(currentFile.Content)) 504 | resolvedPath, _ := workspace.ResolveFilePath(libraryFilePath, workspace.Root) 505 | 506 | logging.Logger.Info("AST Traversal: Got library definition", "file", resolvedPath, "ident", identName) 507 | fileChan <- resolvedPath 508 | 509 | logging.Logger.Info("AST Traversal: Got library definition", "file", resolvedPath, "ident", identName) 510 | store.Dependencies.RemoveDependenciesForFile(currentFile.Handle.Path) 511 | store.Dependencies.AddLibraryDependency(currentFile.Handle.Path, resolvedPath, identName) 512 | 513 | sym := NewLibrary(Location{ 514 | File: currentFile.Handle.Path, 515 | Range: ToRange(ident), 516 | }, resolvedPath, identName) 517 | scope.addSymbol(&sym) 518 | logging.Logger.Info("Current scope values", "scope", scope) 519 | 520 | } else if valueGrammarName == "environment" { 521 | logging.Logger.Info("AST Traversal: Got environment") 522 | // Move to the environment node. For some reason, the environment node is the next sibling of the value node, which is just the "environment" keyword 523 | value = value.NextSibling() 524 | envScope := NewScope(scope, ToRange(value)) 525 | 526 | // Value = (environment) node 527 | for i := uint(0); i < value.ChildCount(); i++ { 528 | // Parse each child of environment node 529 | logging.Logger.Info("AST Traversal: Parsing environment child", "child", value.Child(i).GrammarName()) 530 | workspace.ParseASTNode(value.Child(i), currentFile, envScope, store, visited, fileChan) 531 | } 532 | sym := NewEnvironment( 533 | Location{ 534 | File: currentFile.Handle.Path, 535 | Range: ToRange(ident), 536 | }, 537 | identName, 538 | envScope, 539 | ) 540 | scope.addSymbol(&sym) 541 | } else { 542 | if ident == nil { 543 | logging.Logger.Info("AST Traversal: Got definition without identifier. Ignoring.") 544 | return 545 | } 546 | 547 | logging.Logger.Info("Current scope values", "scope", scope) 548 | expr := NewScope(scope, ToRange(value)) 549 | for i := uint(0); i < node.ChildCount(); i++ { 550 | workspace.ParseASTNode(node.Child(i), currentFile, expr, store, visited, fileChan) 551 | } 552 | sym := NewDefinition( 553 | Location{ 554 | File: currentFile.Handle.Path, 555 | Range: ToRange(node), 556 | }, 557 | identName, 558 | value, expr, ParseDocumentation(node, currentFile.Content)) 559 | scope.addSymbol(&sym) 560 | } 561 | case "environment": 562 | logging.Logger.Info("AST Traversal: Parsing Environment without identifier", "environment", node.Utf8Text(currentFile.Content)) 563 | node = node.NextSibling() 564 | if node == nil { 565 | logging.Logger.Info("AST Traversal: Got environment without definitions. Ignoring.") 566 | return 567 | } 568 | envScope := NewScope(scope, ToRange(node)) 569 | 570 | for i := uint(0); i < node.ChildCount(); i++ { 571 | workspace.ParseASTNode(node.Child(i), currentFile, envScope, store, visited, fileChan) 572 | } 573 | sym := NewEnvironment( 574 | Location{ 575 | File: currentFile.Handle.Path, 576 | Range: ToRange(node), 577 | }, 578 | "", 579 | envScope, 580 | ) 581 | scope.addSymbol(&sym) 582 | logging.Logger.Info("AST Traversal: Parsed environment", "locatio", sym.Loc) 583 | 584 | case "function_definition": 585 | functionName := node.ChildByFieldName("name") 586 | if functionName == nil { 587 | logging.Logger.Error("AST Traversal: Function definition without name. Skipping") 588 | return 589 | } 590 | 591 | arguments := functionName.NextNamedSibling() 592 | if arguments == nil { 593 | logging.Logger.Error("AST Traversal: Function definition without arguments. Skipping") 594 | return 595 | } 596 | 597 | argumentsScope := NewScope(scope, ToRange(node)) 598 | logging.Logger.Info("AST Traversal: Got function_definition", "arguments", arguments.GrammarName(), "functionName", functionName.Utf8Text(currentFile.Content)) 599 | for i := uint(0); i < arguments.ChildCount(); i++ { 600 | argumentNode := arguments.Child(i) 601 | if !argumentNode.IsNamed() { 602 | continue 603 | } 604 | 605 | logging.Logger.Info("AST Traversal: Parsing function argument", "arg", argumentNode.GrammarName(), "content", argumentNode.Utf8Text(currentFile.Content)) 606 | 607 | arg := NewIdentifier( 608 | Location{ 609 | File: currentFile.Handle.Path, 610 | Range: ToRange(argumentNode), 611 | }, 612 | argumentNode.Utf8Text(currentFile.Content), 613 | ) 614 | argumentsScope.addSymbol(&arg) 615 | } 616 | if len(argumentsScope.Symbols) > 0 { 617 | logging.Logger.Info("Arguments Scope", "scope", argumentsScope.Symbols[0].Ident) 618 | } 619 | 620 | expression := node.ChildByFieldName("value") 621 | if expression == nil { 622 | logging.Logger.Error("AST Traversal: Function definition without expression. Skipping") 623 | return 624 | } 625 | 626 | // Treat it as a part of a pattern scope because arguments defined are only in function scope 627 | exprScope := NewScope(scope, ToRange(node)) 628 | logging.Logger.Info("Parsing function value using separate scope") 629 | for i := uint(0); i < node.ChildCount(); i++ { 630 | workspace.ParseASTNode(node.Child(i), currentFile, exprScope, store, visited, fileChan) 631 | } 632 | 633 | functionNode := NewFunction( 634 | Location{ 635 | File: currentFile.Handle.Path, 636 | Range: ToRange(node), 637 | }, 638 | functionName.Utf8Text(currentFile.Content), 639 | argumentsScope, 640 | expression, 641 | exprScope, 642 | ParseDocumentation(node, currentFile.Content), 643 | ) 644 | 645 | scope.addSymbol(&functionNode) 646 | logging.Logger.Info("Current scope values", "scope_children", len(scope.Children), "scope_symbols", len(scope.Symbols)) 647 | case "recinition": 648 | logging.Logger.Info("AST Traversal: Got recinition") 649 | ident := node.ChildByFieldName("name") 650 | expr := node.ChildByFieldName("expression") 651 | 652 | if ident == nil || expr == nil { 653 | logging.Logger.Error("AST Traversal: Recinition without ident or expr", "node is nil", ident == nil, "expr is nil", expr == nil) 654 | return 655 | } 656 | sym := NewDefinition( 657 | Location{ 658 | File: currentFile.Handle.Path, 659 | Range: ToRange(ident), 660 | }, 661 | ident.Utf8Text(currentFile.Content), 662 | expr, nil, ParseDocumentation(ident, currentFile.Content)) 663 | scope.addSymbol(&sym) 664 | logging.Logger.Info("Current scope values", "scope", scope) 665 | 666 | case "with_environment": 667 | logging.Logger.Info("AST Traversal: Got with environment", "text", node.Utf8Text(currentFile.Content)) 668 | 669 | expr := node.ChildByFieldName("expression") 670 | 671 | if expr == nil { 672 | logging.Logger.Error("AST Traversal: Environment without expression. Skipping") 673 | return 674 | } 675 | environment := node.ChildByFieldName("local_environment") 676 | if environment == nil { 677 | logging.Logger.Error("AST Traversal: Environment without local_environment. Skipping") 678 | return 679 | } 680 | 681 | withScope := NewScope(scope, ToRange(node)) 682 | for i := uint(0); i < environment.NamedChildCount(); i++ { 683 | logging.Logger.Info("AST Traversal: Parsing environment definition", "child", environment.NamedChild(i).GrammarName()) 684 | workspace.ParseASTNode(environment.NamedChild(i), currentFile, withScope, store, visited, fileChan) 685 | } 686 | 687 | exprScope := NewScope(scope, ToRange(node)) 688 | logging.Logger.Info("AST Traversal: Parsing expr definition", "child", expr.GrammarName()) 689 | workspace.ParseASTNode(expr, currentFile, exprScope, store, visited, fileChan) 690 | 691 | sym := NewWithEnvironment(Location{ 692 | File: currentFile.Handle.Path, 693 | Range: ToRange(node), 694 | }, withScope, expr, exprScope) 695 | scope.addSymbol(&sym) 696 | logging.Logger.Info("Current scope values", "scope", scope) 697 | 698 | case "letrec_environment": 699 | logging.Logger.Info("AST Traversal: Got letrec environment", "text", node.Utf8Text(currentFile.Content)) 700 | expr := node.ChildByFieldName("expression") 701 | if expr == nil { 702 | logging.Logger.Error("AST Traversal: LetRec environment without expression. Skipping") 703 | return 704 | } 705 | environment := node.ChildByFieldName("local_environment") 706 | if environment == nil { 707 | logging.Logger.Error("AST Traversal: LetRec environment without local_environment. Skipping") 708 | return 709 | } 710 | 711 | letRecScope := NewScope(scope, ToRange(node)) 712 | for i := uint(0); i < environment.ChildCount(); i++ { 713 | logging.Logger.Info("AST Traversal: Parsing child", "child", environment.Child(i).GrammarName()) 714 | workspace.ParseASTNode(environment.Child(i), currentFile, letRecScope, store, visited, fileChan) 715 | } 716 | 717 | exprScope := NewScope(scope, ToRange(node)) 718 | workspace.ParseASTNode(expr, currentFile, exprScope, store, visited, fileChan) 719 | 720 | sym := NewLetRecEnvironment(Location{ 721 | File: currentFile.Handle.Path, 722 | Range: ToRange(node), 723 | }, letRecScope, expr, exprScope) 724 | scope.addSymbol(&sym) 725 | logging.Logger.Info("Current scope values", "scope", scope) 726 | 727 | // Import statement 728 | case "file_import": 729 | fileNode := node.ChildByFieldName("filename") 730 | if fileNode == nil { 731 | logging.Logger.Info("AST Traversal: Got import statement without importing file. Ignoring.") 732 | return 733 | } 734 | 735 | // Strip quotes as file name comes as "file_name" not just file_name in tree_sitter grammar 736 | file := stripQuotes(fileNode.Utf8Text(currentFile.Content)) 737 | resolvedPath, _ := workspace.ResolveFilePath(file, workspace.Root) 738 | logging.Logger.Info("AST Traversal: Got import statement. Going through tree", "file", resolvedPath) 739 | 740 | fileChan <- resolvedPath 741 | 742 | store.Dependencies.RemoveDependenciesForFile(currentFile.Handle.Path) 743 | store.Dependencies.AddDependency(currentFile.Handle.Path, resolvedPath) 744 | 745 | sym := NewImport( 746 | Location{ 747 | File: currentFile.Handle.Path, 748 | Range: ToRange(node), 749 | }, 750 | resolvedPath) 751 | scope.addSymbol(&sym) 752 | logging.Logger.Info("Current scope values", "scope", scope) 753 | // TODO: Recursively parse the imported file if it exists 754 | 755 | case "iteration": 756 | logging.Logger.Info("AST Traversal: Got iteration node") 757 | 758 | currentIter := node.ChildByFieldName("current_iter") 759 | if currentIter == nil { 760 | logging.Logger.Error("AST Traversal: Iteration node without current_iter. Skipping") 761 | return 762 | } 763 | 764 | expr := node.ChildByFieldName("expression") 765 | if expr == nil { 766 | logging.Logger.Error("AST Traversal: Iteration node without expression. Skipping") 767 | return 768 | } 769 | 770 | // Create a new scope for the iteration 771 | iterScope := NewScope(scope, ToRange(node)) 772 | 773 | currentIterIdent := NewIdentifier( 774 | Location{ 775 | File: currentFile.Handle.Path, 776 | Range: ToRange(currentIter), 777 | }, 778 | currentIter.Utf8Text(currentFile.Content)) 779 | iterScope.addSymbol(¤tIterIdent) 780 | 781 | iterSym := NewIteration( 782 | Location{ 783 | File: currentFile.Handle.Path, 784 | Range: ToRange(node), 785 | }, 786 | iterScope, 787 | expr) 788 | 789 | scope.addSymbol(&iterSym) 790 | logging.Logger.Info("Parsed iteration", "current_iter", currentIterIdent.Ident, "scope", iterScope) 791 | logging.Logger.Info("Current scope values", "scope", scope) 792 | case "pattern": 793 | logging.Logger.Info("AST Traversal: Got pattern node") 794 | 795 | caseRules := []Symbol{} 796 | 797 | rules := node.NamedChild(0) 798 | 799 | if rules == nil { 800 | logging.Logger.Error("AST Traversal: Pattern node without rules. Skipping") 801 | return 802 | } 803 | 804 | for i := uint(0); i < rules.NamedChildCount(); i++ { 805 | ruleNode := rules.NamedChild(i) 806 | 807 | if ruleNode == nil { 808 | logging.Logger.Error("AST Traversal: Pattern node with nil child. Skipping") 809 | continue 810 | } 811 | 812 | if ruleNode.GrammarName() != "rule" { 813 | logging.Logger.Error("AST Traversal: Pattern node with non-rule child. Skipping", "child", ruleNode.GrammarName()) 814 | continue 815 | } 816 | 817 | arguments := ruleNode.NamedChild(0) // arguments are the first child of a rule node 818 | if arguments == nil { 819 | logging.Logger.Error("AST Traversal: Rule without arguments. Skipping") 820 | continue 821 | } 822 | logging.Logger.Info("AST Traversal: Parsing rule", "rule", arguments.ToSexp()) 823 | 824 | expression := ruleNode.ChildByFieldName("expression") 825 | if expression == nil { 826 | logging.Logger.Error("AST Traversal: Rule without expression. Skipping") 827 | continue 828 | } 829 | 830 | ruleScope := NewScope(scope, ToRange(ruleNode)) 831 | for j := uint(0); j < arguments.ChildCount(); j++ { 832 | argument := arguments.Child(j) 833 | argumentSym := NewIdentifier( 834 | Location{ 835 | File: currentFile.Handle.Path, 836 | Range: ToRange(argument), 837 | }, 838 | argument.Utf8Text(currentFile.Content)) 839 | ruleScope.addSymbol(&argumentSym) 840 | } 841 | 842 | ruleSym := NewRule(Location{ 843 | File: currentFile.Handle.Path, 844 | Range: ToRange(ruleNode), 845 | }, ruleScope, expression) 846 | 847 | caseRules = append(caseRules, ruleSym) 848 | logging.Logger.Info("AST Traversal: Parsed rule", "rule", ruleSym.Ident, "scope", ruleSym.Scope) 849 | } 850 | 851 | caseSymbol := NewCase( 852 | Location{ 853 | File: currentFile.Handle.Path, 854 | Range: ToRange(node), 855 | }, 856 | caseRules) 857 | scope.addSymbol(&caseSymbol) 858 | 859 | logging.Logger.Info("AST Traversal: Parsed pattern", "case_rules", len(caseSymbol.Children)) 860 | logging.Logger.Info("Current scope values", "scope", scope) 861 | default: 862 | for i := uint(0); i < node.ChildCount(); i++ { 863 | workspace.ParseASTNode(node.Child(i), currentFile, scope, store, visited, fileChan) 864 | } 865 | } 866 | } 867 | 868 | func ToRange(node *tree_sitter.Node) transport.Range { 869 | start := node.StartPosition() 870 | end := node.EndPosition() 871 | 872 | return transport.Range{ 873 | Start: transport.Position{Line: uint32(start.Row), Character: uint32(start.Column)}, 874 | End: transport.Position{Line: uint32(end.Row), Character: uint32(end.Column)}, 875 | } 876 | } 877 | 878 | func stripQuotes(s string) string { 879 | stripped := s[1 : len(s)-1] 880 | return stripped 881 | } 882 | 883 | func (w *Workspace) GetFaustDSPDir() string { 884 | faustCommand := w.Config.Command 885 | _, err := exec.LookPath(faustCommand) 886 | if err != nil { 887 | logging.Logger.Error("Couldn't find faust command in PATH", "cmd", faustCommand) 888 | } 889 | var output strings.Builder 890 | cmd := exec.Command(faustCommand, "-dspdir") 891 | cmd.Stdout = &output 892 | 893 | _ = cmd.Run() 894 | faustDSPDirPath := output.String() 895 | // Remove \n at the end 896 | faustDSPDirPath = faustDSPDirPath[:len(faustDSPDirPath)-1] 897 | return faustDSPDirPath 898 | } 899 | 900 | // Resolves a given file path like the Faust compiler does when it has to import a file 901 | // Returns the path along with the directory/workspace path the file was found in 902 | func (w *Workspace) ResolveFilePath(relPath util.Path, rootDir util.Path) (path util.Path, dir util.Path) { 903 | // File in workspace 904 | path1 := filepath.Join(rootDir, relPath) 905 | // logging.Logger.Info("Trying path", "path", path1) 906 | if util.IsValidPath(path1) { 907 | return path1, rootDir 908 | } 909 | 910 | // File in Faust System Library DSP directory 911 | faustDSPDir := w.GetFaustDSPDir() 912 | path2 := filepath.Join(faustDSPDir, relPath) 913 | // logging.Logger.Info("Trying path", "path", path2) 914 | if util.IsValidPath(path2) { 915 | return path2, faustDSPDir 916 | } 917 | 918 | logging.Logger.Info("Couldn't resolve file path") 919 | return "", "" 920 | } 921 | 922 | func FindSymbol(ident string, scope *Scope, store *Store) (Symbol, error) { 923 | var visited = make(map[util.Path]struct{}) 924 | 925 | return FindSymbolHelper(ident, scope, store, &visited) 926 | } 927 | 928 | func FindSymbolHelper(ident string, scope *Scope, store *Store, visited *map[util.Path]struct{}) (Symbol, error) { 929 | if scope == nil { 930 | return Symbol{}, fmt.Errorf("Invalid scope") 931 | } 932 | 933 | // 1) Check current scope's definitions for this symbol 934 | for _, symbol := range scope.Symbols { 935 | 936 | if symbol.Ident == ident { 937 | return *symbol, nil 938 | } 939 | } 940 | 941 | // 2) Check imported files for this symbol 942 | // TODO: Instead of 2 loops, get import symbols in the first loop itself and iterate through that 943 | logging.Logger.Info("Symbol not in scope, checking import statements") 944 | for i, symbol := range scope.Symbols { 945 | 946 | if symbol.Kind == Import { 947 | logging.Logger.Info("Symbol type", "type", symbol.Kind.String(), "index", i) 948 | f, ok := store.Files.GetFromPath(symbol.File) 949 | if ok { 950 | logging.Logger.Info("Found import statement, checking in file", "path", f.Handle.Path) 951 | found, err := FindSymbolHelper(ident, f.Scope, store, visited) 952 | if err == nil { 953 | return found, nil 954 | } 955 | } 956 | } 957 | } 958 | 959 | if scope.Parent != nil { 960 | logging.Logger.Info("Going to parent to find", "ident", ident) 961 | return FindSymbolHelper(ident, scope.Parent, store, visited) 962 | } else { 963 | return Symbol{}, fmt.Errorf("Couldn't find symbol") 964 | } 965 | 966 | } 967 | 968 | func FindSymbolDefinition(ident string, scope *Scope, store *Store) (Symbol, error) { 969 | identSplit := strings.Split(ident, ".") 970 | 971 | if len(identSplit) > 1 { 972 | logging.Logger.Info("Resolving library symbol", "symbol", identSplit) 973 | for i := range len(identSplit) - 1 { 974 | libIdent := identSplit[i] 975 | 976 | // Resolve as Environment 977 | sym, err := FindEnvironmentIdent(libIdent, scope, store) 978 | logging.Logger.Info("Resolved environment", "env", libIdent, "sym", sym.Ident, "loc", sym.Loc) 979 | if err == nil { 980 | scope = sym.Scope 981 | continue 982 | } 983 | 984 | // Resolve as Library if not resolved as environment 985 | file, err := FindLibraryIdent(libIdent, scope, store) 986 | if err != nil { 987 | break 988 | } 989 | logging.Logger.Info("Resolved library environment", "env", libIdent, "location", file) 990 | f, ok := store.Files.GetFromPath(file) 991 | if ok { 992 | f.mu.RLock() 993 | logging.Logger.Info("Setting New Scope to", "path", file) 994 | scope = f.Scope 995 | f.mu.RUnlock() 996 | if scope == nil { 997 | break 998 | } 999 | } 1000 | } 1001 | } 1002 | ident = identSplit[len(identSplit)-1] 1003 | 1004 | return FindSymbol(ident, scope, store) 1005 | } 1006 | 1007 | func FindDefinition(ident string, scope *Scope, store *Store) (Location, error) { 1008 | sym, err := FindSymbol(ident, scope, store) 1009 | return sym.Loc, err 1010 | } 1011 | 1012 | func FindDocs(ident string, scope *Scope, store *Store) (string, error) { 1013 | sym, err := FindSymbol(ident, scope, store) 1014 | return sym.Docs.Full, err 1015 | } 1016 | 1017 | func FindEnvironmentIdent(ident string, scope *Scope, store *Store) (Symbol, error) { 1018 | var visited = make(map[util.Path]struct{}) 1019 | 1020 | return FindEnvironmentHelper(ident, scope, store, &visited) 1021 | } 1022 | 1023 | func FindEnvironmentHelper(ident string, scope *Scope, store *Store, visited *map[util.Path]struct{}) (Symbol, error) { 1024 | if scope == nil { 1025 | return Symbol{}, fmt.Errorf("Invalid scope") 1026 | } 1027 | 1028 | // 1) Check current scope's definitions for this symbol 1029 | for _, symbol := range scope.Symbols { 1030 | logging.Logger.Info("Comparing with current symbol", "symbol", symbol.Ident, "expected", ident) 1031 | if symbol.Ident == ident { 1032 | logging.Logger.Info("Found symbol, now looking deeper to find environment", "sym", ident) 1033 | return FindFirstEnvironment(symbol) 1034 | } 1035 | } 1036 | 1037 | // 2) Check imported files for this symbol 1038 | // TODO: Instead of 2 loops, get import symbols in the first loop itself and iterate through that 1039 | logging.Logger.Info("Symbol not in scope, checking import statements") 1040 | for i, symbol := range scope.Symbols { 1041 | 1042 | if symbol.Kind == Import { 1043 | logging.Logger.Info("Symbol type", "type", symbol.Kind.String(), "index", i) 1044 | f, ok := store.Files.GetFromPath(symbol.File) 1045 | if ok { 1046 | logging.Logger.Info("Found import statement, checking in file", "path", f.Handle.Path) 1047 | found, err := FindEnvironmentHelper(ident, f.Scope, store, visited) 1048 | if err == nil { 1049 | return found, nil 1050 | } 1051 | } 1052 | } 1053 | } 1054 | 1055 | if scope.Parent != nil { 1056 | logging.Logger.Info("Going to parent to find", "ident", ident) 1057 | return FindEnvironmentHelper(ident, scope.Parent, store, visited) 1058 | } else { 1059 | return Symbol{}, fmt.Errorf("Couldn't find symbol") 1060 | } 1061 | 1062 | } 1063 | 1064 | func FindFirstEnvironment(sym *Symbol) (Symbol, error) { 1065 | switch sym.Kind { 1066 | case Environment: 1067 | // logging.Logger.Info("Already environment symbol, returning", "env", sym.Loc.Range) 1068 | return *sym, nil 1069 | case WithEnvironment, LetRecEnvironment: 1070 | // logging.Logger.Info("With Environment, looking in it's children") 1071 | for _, sym := range sym.Expression.Symbols { 1072 | return FindFirstEnvironment(sym) 1073 | } 1074 | case Function, Definition: 1075 | // logging.Logger.Info("Definition, looking in it's children") 1076 | for _, sym := range sym.Expression.Symbols { 1077 | return FindFirstEnvironment(sym) 1078 | } 1079 | default: 1080 | // logging.Logger.Info("Got unwanted symbol, ignoring", "kind", sym.Kind.String(), "loc", sym.Loc) 1081 | } 1082 | return Symbol{}, fmt.Errorf("Couldn't find environment in symbol") 1083 | 1084 | } 1085 | 1086 | func FindLibraryIdent(ident string, scope *Scope, store *Store) (util.Path, error) { 1087 | var visited = make(map[util.Path]struct{}) 1088 | 1089 | return FindLibraryHelper(ident, scope, store, &visited) 1090 | } 1091 | 1092 | func FindLibraryHelper(ident string, scope *Scope, store *Store, visited *map[util.Path]struct{}) (util.Path, error) { 1093 | if scope == nil { 1094 | return "", fmt.Errorf("Invalid scope") 1095 | } 1096 | 1097 | // 1) Check current scope's definitions for this symbol 1098 | for _, symbol := range scope.Symbols { 1099 | logging.Logger.Info("Comparing with current symbol", "symbol", symbol.Ident, "expected", ident) 1100 | if symbol.Ident == ident { 1101 | return symbol.File, nil 1102 | } 1103 | } 1104 | 1105 | // 2) Check imported files for this symbol 1106 | // TODO: Instead of 2 loops, get import symbols in the first loop itself and iterate through that 1107 | logging.Logger.Info("Symbol not in scope, checking import statements") 1108 | for i, symbol := range scope.Symbols { 1109 | if symbol.Kind == Import { 1110 | logging.Logger.Info("Symbol type", "type", symbol.Kind.String(), "index", i) 1111 | f, ok := store.Files.GetFromPath(symbol.File) 1112 | if ok { 1113 | logging.Logger.Info("Found import statement, checking in file", "path", f.Handle.Path) 1114 | found, err := FindLibraryHelper(ident, f.Scope, store, visited) 1115 | if err == nil { 1116 | return found, nil 1117 | } 1118 | } 1119 | } 1120 | } 1121 | 1122 | if scope.Parent != nil { 1123 | logging.Logger.Info("Going to parent to find", "ident", ident) 1124 | return FindLibraryHelper(ident, scope.Parent, store, visited) 1125 | } else { 1126 | return "", fmt.Errorf("Couldn't find symbol") 1127 | } 1128 | 1129 | } 1130 | 1131 | type CompletionSym struct { 1132 | name string 1133 | docs Documentation 1134 | } 1135 | 1136 | func GetPossibleSymbols(pos transport.Position, filePath util.Path, store *Store, encoding string) []CompletionSym { 1137 | f, ok := store.Files.GetFromPath(filePath) 1138 | if !ok { 1139 | logging.Logger.Info("Couldn't find file", "path", filePath) 1140 | return []CompletionSym{} 1141 | } 1142 | 1143 | // 1) Get scope at position 1144 | offset, err := PositionToOffset(pos, string(f.Content), encoding) 1145 | if err != nil { 1146 | logging.Logger.Info("Couldn't convert position to offset", "pos", pos, "err", err) 1147 | return []CompletionSym{} 1148 | } 1149 | 1150 | identifier, scope := FindSymbolScopeAtOffset(f.Content, f.Scope, offset, string(store.Files.encoding)) 1151 | if scope == nil { 1152 | logging.Logger.Info("Couldn't find scope at position", "pos", pos, "offset", offset) 1153 | return []CompletionSym{} 1154 | } 1155 | logging.Logger.Info("Found identifier at position", "ident", identifier, "scope_range", scope.Range, "len", len(scope.Symbols)) 1156 | 1157 | // 2) Split identifier by '.' to get symbol tree and find scope of last identifier 1158 | if identifier == "" { 1159 | logging.Logger.Info("No identifier found at position, returning all symbols possible in current scope", "pos", pos, "offset", offset) 1160 | 1161 | availableSymbols := []CompletionSym{} 1162 | for { 1163 | if scope == nil { 1164 | break 1165 | } 1166 | availableSymbols = append(availableSymbols, FindSymbolsNew(scope, "", store, make(map[util.Path]struct{}))...) 1167 | scope = scope.Parent 1168 | } 1169 | return availableSymbols 1170 | } 1171 | if identifier[len(identifier)-1] == '.' { 1172 | // Remove trailing '.' if any 1173 | // Example: a.f. -> a.f 1174 | // This is because completion is requested after '.' 1175 | // logging.Logger.Info("Removing trailing '.' from identifier", "ident", identifier) 1176 | identifier = identifier[:len(identifier)-1] 1177 | sym, err := FindSymbolDefinition(identifier, scope, store) 1178 | if err != nil { 1179 | // logging.Logger.Info("Couldn't find symbol definition for identifier, checking with previous identifier", "ident", identifier, "err", err) 1180 | identifierSplit := strings.Split(identifier, ".") 1181 | if len(identifierSplit) > 2 { 1182 | identifier = strings.Join(identifierSplit[:len(identifierSplit)-1], ".") 1183 | sym, err = FindSymbolDefinition(identifier, scope, store) 1184 | if err != nil { 1185 | // logging.Logger.Info("Couldn't find symbol definition for identifier", "ident", identifier, "err", err) 1186 | return []CompletionSym{} 1187 | } 1188 | } else { 1189 | return []CompletionSym{} 1190 | } 1191 | } 1192 | logging.Logger.Info("Found symbol definition for identifier", "ident", identifier, "loc", sym.Loc) 1193 | 1194 | if sym.Kind == Library { 1195 | logging.Logger.Info("Identifier is a library, getting symbols from file", "file", sym.File) 1196 | f, ok := store.Files.GetFromPath(sym.File) 1197 | if ok { 1198 | f.mu.RLock() 1199 | syms := FindSymbolsNew(f.Scope, "", store, make(map[util.Path]struct{})) 1200 | f.mu.RUnlock() 1201 | return syms 1202 | } else { 1203 | logging.Logger.Info("Couldn't find file for library", "file", sym.File) 1204 | return []CompletionSym{} 1205 | } 1206 | } else { 1207 | env, err := FindEnvironmentIdent(identifier, scope, store) 1208 | if err == nil { 1209 | return FindSymbolsNew(env.Scope, "", store, make(map[util.Path]struct{})) 1210 | } 1211 | return []CompletionSym{} 1212 | } 1213 | } else { 1214 | // logging.Logger.Info("Identifier doesn't end with '.', returning all symbols in current scope", "ident", identifier) 1215 | availableSymbols := []CompletionSym{} 1216 | for { 1217 | if scope == nil { 1218 | break 1219 | } 1220 | availableSymbols = append(availableSymbols, FindSymbolsNew(scope, "", store, make(map[util.Path]struct{}))...) 1221 | scope = scope.Parent 1222 | } 1223 | return availableSymbols 1224 | } 1225 | } 1226 | 1227 | func JoinEnvIdent(parentSymbol, childSymbol string) string { 1228 | if parentSymbol == "" { 1229 | return childSymbol 1230 | } 1231 | return parentSymbol + "." + childSymbol 1232 | } 1233 | 1234 | func AddEnvIdents(symbols []CompletionSym, parentSymbol string) []CompletionSym { 1235 | for i, symbol := range symbols { 1236 | sym := symbols[i] 1237 | sym.name = JoinEnvIdent(parentSymbol, symbol.name) 1238 | symbols[i] = sym 1239 | } 1240 | 1241 | return symbols 1242 | } 1243 | 1244 | func NewCompletionSym(sym *Symbol) CompletionSym { 1245 | return CompletionSym{name: sym.Ident, docs: sym.Docs} 1246 | } 1247 | 1248 | func FindSymbolsNew(scope *Scope, parentSymbol string, store *Store, visited map[util.Path]struct{}) []CompletionSym { 1249 | symbols := []CompletionSym{} 1250 | 1251 | for _, sym := range scope.Symbols { 1252 | // logging.Logger.Info("Found symbol in scope", "symbol", sym.Ident, "kind", sym.Kind.String(), "loc", sym.Loc) 1253 | if sym.Ident != "" { 1254 | symbols = append(symbols, NewCompletionSym(sym)) 1255 | } 1256 | if sym.Kind == Definition || sym.Kind == Function { 1257 | env, err := FindFirstEnvironment(sym) 1258 | if err != nil { 1259 | continue 1260 | } 1261 | childSyms := FindSymbolsNew(env.Scope, JoinEnvIdent(parentSymbol, sym.Ident), store, visited) 1262 | childSyms = AddEnvIdents(childSyms, JoinEnvIdent(parentSymbol, sym.Ident)) 1263 | symbols = slices.Concat(symbols, childSyms) 1264 | 1265 | } 1266 | if sym.Kind == WithEnvironment || sym.Kind == LetRecEnvironment { 1267 | // Find lowest environment 1268 | env, err := FindFirstEnvironment(sym) 1269 | if err != nil { 1270 | continue 1271 | } 1272 | 1273 | childSyms := FindSymbolsNew(env.Scope, JoinEnvIdent(parentSymbol, sym.Ident), store, visited) 1274 | childSyms = AddEnvIdents(childSyms, JoinEnvIdent(parentSymbol, sym.Ident)) 1275 | symbols = slices.Concat(symbols, childSyms) 1276 | } 1277 | if sym.Kind == Import { 1278 | symbols = slices.Concat(symbols, FindSymbolsInFile(sym, parentSymbol, store, visited)) 1279 | } 1280 | } 1281 | 1282 | return symbols 1283 | } 1284 | 1285 | func FindSymbolsInFile(sym *Symbol, parentSymbol string, store *Store, visited map[util.Path]struct{}) []CompletionSym { 1286 | // Used for adding symbols from other files when an import or library statement is encountered 1287 | symbols := []CompletionSym{} 1288 | 1289 | libPath := sym.File 1290 | _, ok := visited[libPath] 1291 | if !ok { 1292 | // logging.Logger.Info("Visiting file for the first time", "lib", libPath, "parentSymbol", parentSymbol) 1293 | visited[libPath] = struct{}{} 1294 | 1295 | f, ok := store.Files.GetFromPath(libPath) 1296 | if ok { 1297 | f.mu.RLock() 1298 | symbols = FindSymbolsNew(f.Scope, parentSymbol, store, visited) 1299 | f.mu.RUnlock() 1300 | } 1301 | 1302 | } else { 1303 | // logging.Logger.Info("File already visited", "path", libPath) 1304 | 1305 | } 1306 | 1307 | return symbols 1308 | } 1309 | 1310 | func FindSymbolScope(content []byte, scope *Scope, offset uint) (string, *Scope) { 1311 | tree := parser.ParseTree(content) 1312 | fileAST := tree.RootNode() 1313 | defer tree.Close() 1314 | node := fileAST.DescendantForByteRange(offset, offset) 1315 | logging.Logger.Info("Got descendant node as", "type", node.GrammarName(), "content", node.Utf8Text(content), "location", ToRange(node)) 1316 | switch node.GrammarName() { 1317 | case "identifier": 1318 | // If parent is access, keep finding scopes for each environment monoidically (e.g. lib.moo.foo.lay.f will be lib->moo->foo->lay->f) 1319 | 1320 | // Find outermost parent 1321 | outerMostParent := node 1322 | for { 1323 | parent := outerMostParent.Parent() 1324 | if parent != nil { 1325 | if parent.GrammarName() == "access" { 1326 | outerMostParent = parent 1327 | continue 1328 | } 1329 | } 1330 | break 1331 | } 1332 | 1333 | if outerMostParent.GrammarName() == "access" { 1334 | node = outerMostParent 1335 | } 1336 | 1337 | identString := node.Utf8Text(content) 1338 | // Get Node range and find smallest scope that contains it 1339 | identStart := node.StartPosition() 1340 | identEnd := node.EndPosition() 1341 | 1342 | identRange := transport.Range{ 1343 | Start: transport.Position{Line: uint32(identStart.Row), Character: uint32(identStart.Column)}, 1344 | End: transport.Position{Line: uint32(identEnd.Row), Character: uint32(identEnd.Column)}, 1345 | } 1346 | 1347 | lowestScope := FindLowestScopeContainingRange(scope, identRange) 1348 | 1349 | return identString, lowestScope 1350 | } 1351 | 1352 | return "", nil 1353 | } 1354 | 1355 | func FindSymbolScopeAtOffset(content []byte, scope *Scope, offset uint, encoding string) (string, *Scope) { 1356 | // Manual version of FindSymbolScope that doesn't use tree-sitter to find the identifier at the given offset 1357 | i, j := offset, offset 1358 | 1359 | // Move start till reaches non valid character 1360 | for { 1361 | if j == uint(len(content)-1) { 1362 | break 1363 | } 1364 | if unicode.IsLetter(rune(content[j])) || unicode.IsDigit(rune(content[j])) || content[j] == '.' { 1365 | j++ 1366 | } else { 1367 | break 1368 | } 1369 | } 1370 | 1371 | // Move end till reaches non valid character 1372 | for { 1373 | if i == 0 { 1374 | break 1375 | } 1376 | if unicode.IsLetter(rune(content[i])) || unicode.IsDigit(rune(content[i])) || content[i] == '.' { 1377 | i-- 1378 | } else { 1379 | break 1380 | } 1381 | } 1382 | ident := content[i:j] 1383 | if string(ident) == "" { 1384 | // Trying to go back from offset to find identifier 1385 | i-- 1386 | for { 1387 | if i <= 0 { 1388 | break 1389 | } 1390 | if unicode.IsLetter(rune(content[i])) || unicode.IsDigit(rune(content[i])) || content[i] == '.' { 1391 | i-- 1392 | } else { 1393 | break 1394 | } 1395 | } 1396 | ident = content[i+1 : j] 1397 | } 1398 | 1399 | //logging.Logger.Info("Found identifier at offset", "ident", string(ident), "start", i+1, "end", j, "offset", offset) 1400 | start, err := OffsetToPosition(i, string(content), encoding) 1401 | end, err := OffsetToPosition(j, string(content), encoding) 1402 | if err != nil { 1403 | return "", nil 1404 | } 1405 | identRange := transport.Range{ 1406 | Start: start, 1407 | End: end, 1408 | } 1409 | lowestScope := FindLowestScopeContainingRange(scope, identRange) 1410 | return string(ident), lowestScope 1411 | } 1412 | 1413 | func FindLowestScopeContainingRange(scope *Scope, identRange transport.Range) *Scope { 1414 | if scope != nil { 1415 | // logging.Logger.Info("Scope children", "length", len(scope.Children)) 1416 | for _, childScope := range scope.Children { 1417 | // logging.Logger.Info("Current child scope", "no", i) 1418 | // logging.Logger.Info("Looking in child scope to find lowest scope", "current", scope.Range, "child", childScope.Range, "target", identRange) 1419 | // logging.Logger.Info("What is parent scope ?", "scope", scope.Symbols[0]) 1420 | if childScope != nil { 1421 | if RangeContains(childScope.Range, identRange) { 1422 | // logging.Logger.Info("Scope contains identifier", "scope", childScope.Range, "ident", identRange) 1423 | return FindLowestScopeContainingRange(childScope, identRange) 1424 | } else { 1425 | // logging.Logger.Info("Parent scope does not contain child scope", "parent", scope.Range, "child", childScope.Range) 1426 | } 1427 | } 1428 | } 1429 | } 1430 | // logging.Logger.Info("Returning current scope", "scope", scope.Range) 1431 | return scope 1432 | } 1433 | 1434 | func RangeContains(parent transport.Range, child transport.Range) bool { 1435 | // OLD 1436 | // below := parent.Start.Line <= child.Start.Line && parent.Start.Character <= child.Start.Character 1437 | // above := child.End.Line <= parent.End.Line && child.End.Character <= parent.End.Character 1438 | // return above && below 1439 | 1440 | // NEW 1441 | // Failed cases: Parent: {{0, 0}, {2, 0}}, Child: {{1,0}, {1,17}} 1442 | start_is_between := (parent.Start.Line < child.Start.Line) || 1443 | (parent.Start.Line == child.Start.Line && parent.Start.Character <= child.Start.Character) 1444 | end_is_between := (parent.End.Line > child.End.Line) || 1445 | (parent.End.Line == child.End.Line && parent.End.Character >= child.End.Character) 1446 | 1447 | return start_is_between && end_is_between 1448 | } 1449 | --------------------------------------------------------------------------------