├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── L │ ├── doc.go │ ├── main.go │ └── main_test.go ├── Lone │ ├── doc.go │ └── main.go ├── acme-lsp │ ├── doc.go │ └── main.go └── acmefocused │ ├── main.go │ └── main_test.go ├── go.mod ├── go.sum ├── internal ├── acme │ ├── LICENSE │ ├── acme.go │ ├── acme_p9p.go │ └── acme_plan9.go ├── acmeutil │ └── acme.go ├── gomod │ └── gomod.go ├── lsp │ ├── acmelsp │ │ ├── acmelsp.go │ │ ├── acmelsp_test.go │ │ ├── assist.go │ │ ├── client.go │ │ ├── client_test.go │ │ ├── config │ │ │ ├── config.go │ │ │ ├── file.go │ │ │ ├── file_go1.12.go │ │ │ └── file_go1.13.go │ │ ├── diagnostics.go │ │ ├── exec.go │ │ ├── exec_test.go │ │ ├── files.go │ │ ├── proxy.go │ │ └── remote.go │ ├── cmd │ │ └── cmd.go │ ├── proxy │ │ ├── client.go │ │ ├── context.go │ │ ├── doc.go │ │ ├── protocol.go │ │ └── server.go │ ├── text │ │ ├── edit.go │ │ ├── edit_test.go │ │ ├── line.go │ │ └── line_test.go │ ├── utils.go │ └── utils_test.go └── p9service │ ├── p9service.go │ ├── p9service_plan9.go │ └── p9service_unix.go └── scripts └── mkdocs.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Disable '\n' -> '\r\n' conversion in Windows, 2 | # which can make gofmt unhappy. 3 | * text=auto 4 | *.go text eol=lf 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | 4 | jobs: 5 | test: 6 | timeout-minutes: 30 7 | strategy: 8 | matrix: 9 | go-version: [1.21.x, 1.22.x] 10 | platform: [ubuntu-latest] 11 | fail-fast: false 12 | runs-on: ${{ matrix.platform }} 13 | env: 14 | GO111MODULE: on 15 | 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | 22 | - name: Go environment 23 | run: | 24 | go version 25 | go env 26 | echo PATH is $PATH 27 | shell: bash 28 | 29 | - name: Checkout code 30 | uses: actions/checkout@v3 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | 34 | - name: Check gofmt 35 | run: | 36 | gofmt -l . 37 | test `gofmt -l . | wc -l` = 0 38 | shell: bash 39 | 40 | - name: Install gopls 41 | run: go install golang.org/x/tools/gopls@latest 42 | working-directory: / 43 | 44 | - name: Install pyls on unix 45 | if: matrix.platform != 'windows-latest' 46 | run: sudo python3 -m pip install 'python-language-server[yapf]' 47 | 48 | - name: Install pyls on windows 49 | if: matrix.platform == 'windows-latest' 50 | run: pip install 'python-language-server[yapf]' 51 | 52 | - name: Check if 'go install' modified go.mod file 53 | run: git diff --exit-code 54 | 55 | - name: Test on unix 56 | if: matrix.platform != 'windows-latest' 57 | run: | 58 | export PATH=${PATH}:$(go env GOPATH)/bin 59 | go test -race -v ./... 60 | shell: bash 61 | 62 | - name: Test on windows 63 | if: matrix.platform == 'windows-latest' 64 | run: | 65 | # 8.3 filenames causes issues. See 66 | # https://github.com/actions/virtual-environments/issues/712 67 | $env:TMP = "$env:USERPROFILE\AppData\Local\Temp" 68 | $env:TEMP = "$env:USERPROFILE\AppData\Local\Temp" 69 | 70 | $env:path += ";C:\Users\runneradmin\go\bin" 71 | go test -race -v ./... 72 | 73 | - name: Cross compile for plan9 74 | if: matrix.platform == 'ubuntu-latest' 75 | env: 76 | GOOS: plan9 77 | run: go build ./... 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/acme-lsp/acme-lsp 2 | cmd/L/L 3 | cmd/Lone/Lone 4 | guide 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2018 Fazlul Shahriar. All rights reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Actions Status](https://github.com/9fans/acme-lsp/workflows/Test/badge.svg)](https://github.com/9fans/acme-lsp/actions?query=branch%3Amaster+event%3Apush) 2 | [![Go Reference](https://pkg.go.dev/badge/9fans.net/acme-lsp/cmd/acme-lsp.svg)](https://pkg.go.dev/9fans.net/acme-lsp/cmd/acme-lsp) 3 | [![Go Report Card](https://goreportcard.com/badge/9fans.net/acme-lsp)](https://goreportcard.com/report/9fans.net/acme-lsp) 4 | 5 | # acme-lsp 6 | 7 | [Language Server Protocol](https://langserver.org/) tools for [acme](https://en.wikipedia.org/wiki/Acme_(text_editor)) text editor. 8 | 9 | The main tool is 10 | [acme-lsp](https://pkg.go.dev/9fans.net/acme-lsp/cmd/acme-lsp), 11 | which listens for commands from the [L 12 | command](https://pkg.go.dev/9fans.net/acme-lsp/cmd/L). 13 | It also watches for files created (`New`), loaded (`Get`), saved 14 | (`Put`), or deleted (`Del`) in acme, and tells the LSP server about 15 | these changes. The LSP server in turn responds by sending diagnostics 16 | information (compiler errors, lint errors, etc.) which are shown in an 17 | acme window. When `Put` is executed in an acme window, `acme-lsp` 18 | also organizes import paths in the window and formats it. 19 | 20 | Currently, `acme-lsp` has been tested with 21 | [gopls](https://github.com/golang/tools/tree/master/gopls), 22 | [go-langserver](https://github.com/sourcegraph/go-langserver) and 23 | [pyls](https://github.com/palantir/python-language-server). Please report 24 | incompatibilities with those or other servers. 25 | 26 | ## Installation 27 | 28 | Install the latest release: 29 | 30 | GO111MODULE=on go install 9fans.net/acme-lsp/cmd/acme-lsp@latest 31 | GO111MODULE=on go install 9fans.net/acme-lsp/cmd/L@latest 32 | 33 | ## gopls 34 | 35 | First install the latest release of gopls: 36 | 37 | GO111MODULE=on go install golang.org/x/tools/gopls@latest 38 | 39 | Start acme-lsp like this: 40 | 41 | acme-lsp -server '([/\\]go\.mod)|([/\\]go\.sum)|(\.go)$:gopls serve' -workspaces /path/to/mod1:/path/to/mod2 42 | 43 | where mod1 and mod2 are module directories with a `go.mod` file. 44 | The set of workspace directories can be changed at runtime 45 | by using the `L ws+` and `L ws-` sub-commands. 46 | 47 | When `Put` is executed in an acme window editing `.go` file, acme-lsp 48 | will update import paths and gofmt the window buffer if needed. It also 49 | enables commands like `L def` (jump to defenition), `L refs` (list of 50 | references), etc. within acme. The `L assist` command opens a window 51 | where completion, hover, or signature help output is shown for the 52 | current cursor position in the `.go` file being edited. 53 | 54 | If you want to change `gopls` 55 | [settings](https://github.com/golang/tools/blob/master/gopls/doc/settings.md), 56 | you can create a configuration file at 57 | `UserConfigDir/acme-lsp/config.toml` (the `-showconfig` flag prints 58 | the exact location) and then run `acme-lsp` without any flags. Example 59 | config file: 60 | ```toml 61 | WorkspaceDirectories = [ 62 | "/path/to/mod1", 63 | "/path/to/mod2", 64 | ] 65 | FormatOnPut = true 66 | CodeActionsOnPut = ["source.organizeImports"] 67 | 68 | [Servers] 69 | [Servers.gopls] 70 | Command = ["gopls", "serve", "-rpc.trace"] 71 | StderrFile = "gopls.stderr.log" 72 | LogFile = "gopls.log" 73 | 74 | # These settings gets passed to gopls 75 | [Servers.gopls.Options] 76 | hoverKind = "FullDocumentation" 77 | 78 | [[FilenameHandlers]] 79 | Pattern = "[/\\\\]go\\.mod$" 80 | LanguageID = "go.mod" 81 | ServerKey = "gopls" 82 | 83 | [[FilenameHandlers]] 84 | Pattern = "[/\\\\]go\\.sum$" 85 | LanguageID = "go.sum" 86 | ServerKey = "gopls" 87 | 88 | [[FilenameHandlers]] 89 | Pattern = "\\.go$" 90 | LanguageID = "go" 91 | ServerKey = "gopls" 92 | ``` 93 | 94 | ## Hints & Tips 95 | 96 | * If a file gets out of sync in the LSP server (e.g. because you edited 97 | the file outside of acme), executing `Get` on the file will update it 98 | in the LSP server. 99 | 100 | * Create scripts like `Ldef`, `Lrefs`, `Ltype`, etc., so that you can 101 | easily execute those commands with a single middle click: 102 | ``` 103 | for(cmd in comp def fmt hov impls refs rn sig syms type assist ws ws+ ws-){ 104 | > L^$cmd { 105 | echo '#!/bin/rc' 106 | echo exec L $cmd '$*' 107 | } 108 | chmod +x L^$cmd 109 | } 110 | ``` 111 | 112 | * Create custom keybindings that allow you to do completion 113 | (`L comp -e`) and show signature help (`L sig`) while you're 114 | typing. This can be achieved by using a general keybinding daemon 115 | (e.g. [xbindkeys](http://www.nongnu.org/xbindkeys/xbindkeys.html) 116 | in X11) and running 117 | [acmefocused](https://pkg.go.dev/9fans.net/acme-lsp/cmd/acmefocused). 118 | 119 | ## See also 120 | 121 | * [A setup with Acme on Darwin using acme-lsp with ccls](https://www.bytelabs.org/posts/acme-lsp/) by Igor Böhm 122 | * https://github.com/davidrjenni/A - Similar tool but only for Go programming language 123 | * https://pkg.go.dev/9fans.net/go/acme/acmego - Implements formatting and import fixes for Go 124 | * https://pkg.go.dev/github.com/fhs/misc/cmd/acmepy - Python formatter based on acmego 125 | * https://github.com/ilanpillemer/acmecrystal - Crystal formatter 126 | -------------------------------------------------------------------------------- /cmd/L/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | The program L sends messages to the Language Server Protocol 3 | proxy server acme-lsp. 4 | 5 | L is usually run from within the acme text editor, where $winid 6 | environment variable is set to the ID of currently focused window. 7 | It sends this ID to acme-lsp, which uses it to compute the context for 8 | LSP commands. 9 | 10 | If L is run outside of acme (therefore $winid is not set), L will 11 | attempt to find the focused window ID by connecting to acmefocused 12 | (https://godoc.org/9fans.net/acme-lsp/cmd/acmefocused). 13 | 14 | Usage: L [args...] 15 | 16 | List of sub-commands: 17 | 18 | comp [-e] 19 | Print candidate completions at the cursor position. If 20 | -e (edit) flag is given and there is only one candidate, 21 | the completion is applied instead of being printed. 22 | 23 | def [-p] 24 | Find where the symbol at the cursor position is defined 25 | and send the location to the plumber. If -p flag is given, 26 | the location is printed to stdout instead. 27 | 28 | fmt 29 | Organize imports and format current window buffer. 30 | 31 | hov 32 | Show more information about the symbol under the cursor 33 | ("hover"). 34 | 35 | impls 36 | List implementation location(s) of the symbol under the cursor. 37 | 38 | refs 39 | List locations where the symbol under the cursor is used 40 | ("references"). 41 | 42 | rn 43 | Rename the symbol under the cursor to newname. 44 | 45 | sig 46 | Show signature help for the function, method, etc. under 47 | the cursor. 48 | 49 | syms 50 | List symbols in the current file. 51 | 52 | type [-p] 53 | Find where the type of the symbol at the cursor position 54 | is defined and send the location to the plumber. If -p 55 | flag is given, the location is printed to stdout instead. 56 | 57 | assist [comp|hov|sig] 58 | A new window is created where completion (comp), hover 59 | (hov), or signature help (sig) output is shown depending 60 | on the cursor position in the focused window and the 61 | text surrounding the cursor. If the optional argument is 62 | given, the output will be limited to only that command. 63 | Note: this is a very experimental feature, and may not 64 | be very useful in practice. 65 | 66 | ws 67 | List current set of workspace directories. 68 | 69 | ws+ [directories...] 70 | Add given directories to the set of workspace directories. 71 | Current working directory is added if no directory is specified. 72 | 73 | ws- [directories...] 74 | Remove given directories to the set of workspace directories. 75 | Current working directory is removed if no directory is specified. 76 | 77 | -acme.addr string 78 | address where acme is serving 9P file system (default "/tmp/ns.fhs.:0/acme") 79 | -acme.net string 80 | network where acme is serving 9P file system (default "unix") 81 | -proxy.addr string 82 | address used for communication between acme-lsp and L (default "/tmp/ns.fhs.:0/acme-lsp.rpc") 83 | -proxy.net string 84 | network used for communication between acme-lsp and L (default "unix") 85 | -showconfig 86 | show configuration values and exit 87 | -v Verbose output 88 | */ 89 | package main 90 | -------------------------------------------------------------------------------- /cmd/L/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | 15 | "9fans.net/acme-lsp/internal/lsp" 16 | "9fans.net/acme-lsp/internal/lsp/acmelsp" 17 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 18 | "9fans.net/acme-lsp/internal/lsp/cmd" 19 | "9fans.net/acme-lsp/internal/lsp/proxy" 20 | "9fans.net/internal/go-lsp/lsp/protocol" 21 | p9client "github.com/fhs/9fans-go/plan9/client" 22 | "github.com/sourcegraph/jsonrpc2" 23 | ) 24 | 25 | //go:generate ../../scripts/mkdocs.sh 26 | 27 | const mainDoc = `The program L sends messages to the Language Server Protocol 28 | proxy server acme-lsp. 29 | 30 | L is usually run from within the acme text editor, where $winid 31 | environment variable is set to the ID of currently focused window. 32 | It sends this ID to acme-lsp, which uses it to compute the context for 33 | LSP commands. 34 | 35 | If L is run outside of acme (therefore $winid is not set), L will 36 | attempt to find the focused window ID by connecting to acmefocused 37 | (https://godoc.org/9fans.net/acme-lsp/cmd/acmefocused). 38 | 39 | Usage: L [args...] 40 | 41 | List of sub-commands: 42 | 43 | comp [-e] [-E] 44 | Print candidate completions at the cursor position. If 45 | -e (edit) flag is given and there is only one candidate, 46 | the completion is applied instead of being printed. If 47 | -E (Edit) flag is given, the first matching candidate is 48 | applied, and all matches will be displayed in a dedicated 49 | Acme window named /LSP/Completions. 50 | 51 | def [-p] 52 | Find where the symbol at the cursor position is defined 53 | and send the location to the plumber. If -p flag is given, 54 | the location is printed to stdout instead. 55 | 56 | fmt 57 | Organize imports and format current window buffer. 58 | 59 | hov 60 | Show more information about the symbol under the cursor 61 | ("hover"). 62 | 63 | impls 64 | List implementation location(s) of the symbol under the cursor. 65 | 66 | refs 67 | List locations where the symbol under the cursor is used 68 | ("references"). 69 | 70 | rn 71 | Rename the symbol under the cursor to newname. 72 | 73 | sig 74 | Show signature help for the function, method, etc. under 75 | the cursor. 76 | 77 | syms 78 | List symbols in the current file. 79 | 80 | type [-p] 81 | Find where the type of the symbol at the cursor position 82 | is defined and send the location to the plumber. If -p 83 | flag is given, the location is printed to stdout instead. 84 | 85 | assist [comp|hov|sig] 86 | A new window is created where completion (comp), hover 87 | (hov), or signature help (sig) output is shown depending 88 | on the cursor position in the focused window and the 89 | text surrounding the cursor. If the optional argument is 90 | given, the output will be limited to only that command. 91 | Note: this is a very experimental feature, and may not 92 | be very useful in practice. 93 | 94 | ws 95 | List current set of workspace directories. 96 | 97 | ws+ [directories...] 98 | Add given directories to the set of workspace directories. 99 | Current working directory is added if no directory is specified. 100 | 101 | ws- [directories...] 102 | Remove given directories to the set of workspace directories. 103 | Current working directory is removed if no directory is specified. 104 | ` 105 | 106 | func usage() { 107 | os.Stderr.Write([]byte(mainDoc)) 108 | fmt.Fprintf(os.Stderr, "\n") 109 | flag.PrintDefaults() 110 | os.Exit(2) 111 | } 112 | 113 | func main() { 114 | flag.Usage = usage 115 | cfg := cmd.Setup(config.ProxyFlags) 116 | 117 | err := run(cfg, flag.Args()) 118 | if err != nil { 119 | log.Fatalf("%v", err) 120 | } 121 | } 122 | 123 | func run(cfg *config.Config, args []string) error { 124 | ctx := context.Background() 125 | 126 | if len(args) == 0 { 127 | usage() 128 | } 129 | 130 | conn, err := net.Dial(cfg.ProxyNetwork, cfg.ProxyAddress) 131 | if err != nil { 132 | return fmt.Errorf("dial failed: %v", err) 133 | } 134 | defer conn.Close() 135 | 136 | stream := jsonrpc2.NewBufferedStream(conn, jsonrpc2.VSCodeObjectCodec{}) 137 | var opts []jsonrpc2.ConnOpt 138 | if cfg.Verbose { 139 | opts = append(opts, lsp.LogMessages(log.Default())) 140 | } 141 | rpc := jsonrpc2.NewConn(ctx, stream, nil, opts...) 142 | defer rpc.Close() 143 | server := proxy.NewServer(rpc) 144 | 145 | ver, err := server.Version(ctx) 146 | if err != nil { 147 | return err 148 | } 149 | if ver != proxy.Version { 150 | return fmt.Errorf("acme-lsp speaks protocol version %v but L speaks version %v (make sure they come from the same release)", ver, proxy.Version) 151 | } 152 | 153 | switch args[0] { 154 | case "ws": 155 | folders, err := server.WorkspaceFolders(ctx) 156 | if err != nil { 157 | return err 158 | } 159 | for _, d := range folders { 160 | fmt.Printf("%v\n", d.Name) 161 | } 162 | return nil 163 | case "ws+": 164 | dirs, err := dirsOrCurrentDir(args[1:]) 165 | if err != nil { 166 | return err 167 | } 168 | return server.DidChangeWorkspaceFolders(ctx, &protocol.DidChangeWorkspaceFoldersParams{ 169 | Event: protocol.WorkspaceFoldersChangeEvent{ 170 | Added: dirs, 171 | }, 172 | }) 173 | case "ws-": 174 | dirs, err := dirsOrCurrentDir(args[1:]) 175 | if err != nil { 176 | return err 177 | } 178 | return server.DidChangeWorkspaceFolders(ctx, &protocol.DidChangeWorkspaceFoldersParams{ 179 | Event: protocol.WorkspaceFoldersChangeEvent{ 180 | Removed: dirs, 181 | }, 182 | }) 183 | case "win", "assist": // "win" is deprecated 184 | args = args[1:] 185 | sm := &acmelsp.UnitServerMatcher{Server: server} 186 | if len(args) == 0 { 187 | return acmelsp.Assist(sm, "auto") 188 | } 189 | switch args[0] { 190 | case "comp", "sig", "hov", "auto": 191 | return acmelsp.Assist(sm, args[0]) 192 | } 193 | return fmt.Errorf("unknown assist command %q", args[0]) 194 | } 195 | 196 | winid, err := getWinID() 197 | if err != nil { 198 | return err 199 | } 200 | 201 | rc := acmelsp.NewRemoteCmd(server, winid) 202 | 203 | // In case the window has unsaved changes (it's dirty), sync changes with LSP server. 204 | err = rc.DidChange(ctx) 205 | if err != nil { 206 | return fmt.Errorf("DidChange failed: %v", err) 207 | } 208 | 209 | switch args[0] { 210 | case "comp": 211 | args = args[1:] 212 | 213 | var kind acmelsp.CompletionKind 214 | if len(args) > 0 { 215 | switch args[0] { 216 | case "-e": 217 | kind = acmelsp.CompleteInsertOnlyMatch 218 | case "-E": 219 | kind = acmelsp.CompleteInsertFirstMatch 220 | } 221 | } 222 | 223 | return rc.Completion(ctx, kind) 224 | case "def": 225 | args = args[1:] 226 | return rc.Definition(ctx, len(args) > 0 && args[0] == "-p") 227 | case "fmt": 228 | return rc.OrganizeImportsAndFormat(ctx) 229 | case "hov": 230 | return rc.Hover(ctx) 231 | case "impls": 232 | return rc.Implementation(ctx, true) 233 | case "refs": 234 | return rc.References(ctx) 235 | case "rn": 236 | args = args[1:] 237 | if len(args) < 1 { 238 | usage() 239 | } 240 | return rc.Rename(ctx, args[0]) 241 | case "sig": 242 | return rc.SignatureHelp(ctx) 243 | case "syms": 244 | return rc.DocumentSymbol(ctx) 245 | case "type": 246 | args = args[1:] 247 | return rc.TypeDefinition(ctx, len(args) > 0 && args[0] == "-p") 248 | } 249 | return fmt.Errorf("unknown command %q", args[0]) 250 | } 251 | 252 | func getWinID() (int, error) { 253 | winid, err := getFocusedWinID(filepath.Join(p9client.Namespace(), "acmefocused")) 254 | if err != nil { 255 | return 0, fmt.Errorf("could not get focused window ID: %v", err) 256 | } 257 | n, err := strconv.Atoi(winid) 258 | if err != nil { 259 | return 0, fmt.Errorf("failed to parse $winid: %v", err) 260 | } 261 | return n, nil 262 | } 263 | 264 | func dirsOrCurrentDir(dirs []string) ([]protocol.WorkspaceFolder, error) { 265 | if len(dirs) == 0 { 266 | d, err := os.Getwd() 267 | if err != nil { 268 | return nil, err 269 | } 270 | dirs = []string{d} 271 | } 272 | return lsp.DirsToWorkspaceFolders(dirs) 273 | } 274 | 275 | func getFocusedWinID(addr string) (string, error) { 276 | winid := os.Getenv("winid") 277 | if winid == "" { 278 | conn, err := net.Dial("unix", addr) 279 | if err != nil { 280 | return "", fmt.Errorf("$winid is empty and could not dial acmefocused: %v", err) 281 | } 282 | defer conn.Close() 283 | b, err := ioutil.ReadAll(conn) 284 | if err != nil { 285 | return "", fmt.Errorf("$winid is empty and could not read acmefocused: %v", err) 286 | } 287 | return string(bytes.TrimSpace(b)), nil 288 | } 289 | return winid, nil 290 | } 291 | -------------------------------------------------------------------------------- /cmd/L/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | func TestGetFocusedWinIDFromEnv(t *testing.T) { 13 | os.Setenv("winid", "123") 14 | defer os.Unsetenv("winid") 15 | 16 | want := "123" 17 | got, err := getFocusedWinID("") 18 | if err != nil { 19 | t.Fatalf("getFocusedWinID failed with error %v", err) 20 | } 21 | if got != want { 22 | t.Errorf("$winid is %v; want %v", got, want) 23 | } 24 | } 25 | 26 | func WriteAcmeFocused(ln net.Listener, winid string) error { 27 | conn, err := ln.Accept() 28 | if err != nil { 29 | return err 30 | } 31 | defer conn.Close() 32 | 33 | fmt.Fprintf(conn, "%v\n", winid) 34 | return nil 35 | } 36 | 37 | func TestGetFocusedWinIDFromServer(t *testing.T) { 38 | os.Unsetenv("winid") 39 | want := "321" 40 | 41 | dir, err := ioutil.TempDir("", "acmefocused") 42 | if err != nil { 43 | t.Fatalf("couldn't create temporary directory: %v", err) 44 | } 45 | defer os.RemoveAll(dir) 46 | addr := filepath.Join(dir, "acmefocused") 47 | 48 | ln, err := net.Listen("unix", addr) 49 | if err != nil { 50 | t.Fatalf("listen failed: %v", err) 51 | } 52 | defer ln.Close() 53 | 54 | go func() { 55 | err := WriteAcmeFocused(ln, want) 56 | if err != nil { 57 | t.Errorf("acmefocused server failed: %v", err) 58 | } 59 | }() 60 | 61 | got, err := getFocusedWinID(addr) 62 | if err != nil { 63 | t.Fatalf("getFocusedWinID failed with error %v", err) 64 | } 65 | if got != want { 66 | t.Errorf("$winid is %v; want %v", got, want) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cmd/Lone/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | The program Lone is a standalone client for the acme text editor that 3 | interacts with a Language Server. 4 | 5 | Deprecated: This program is similar to the L command, except it also does 6 | the work of acme-lsp command by executing a LSP server on-demand. It's 7 | recommended to use the L and acme-lsp commands instead, which takes 8 | advantage of LSP server caches and should give faster responses. 9 | 10 | A Language Server implements the Language Server Protocol 11 | (see https://langserver.org/), which provides language features 12 | like auto complete, go to definition, find all references, etc. 13 | Lone depends on one or more language servers already being installed 14 | in the system. See this page of a list of language servers: 15 | https://microsoft.github.io/language-server-protocol/implementors/servers/. 16 | 17 | Usage: Lone [flags] [args...] 18 | 19 | List of sub-commands: 20 | 21 | comp 22 | Show auto-completion for the current cursor position. 23 | 24 | def 25 | Find where identifier at the cursor position is define and 26 | send the location to the plumber. 27 | 28 | fmt 29 | Organize imports and format current window buffer. 30 | 31 | hov 32 | Show more information about the identifier under the cursor 33 | ("hover"). 34 | 35 | monitor 36 | Format window buffer after each Put. 37 | 38 | refs 39 | List locations where the identifier under the cursor is used 40 | ("references"). 41 | 42 | rn 43 | Rename the identifier under the cursor to newname. 44 | 45 | servers 46 | Print list of known language servers. 47 | 48 | sig 49 | Show signature help for the function, method, etc. under 50 | the cursor. 51 | 52 | syms 53 | List symbols in the current file. 54 | 55 | assist [comp|hov|sig] 56 | A new window is created where completion (comp), hover 57 | (hov), or signature help (sig) output is shown depending 58 | on the cursor position in the focused window and the 59 | text surrounding the cursor. If the optional argument is 60 | given, the output will be limited to only that command. 61 | Note: this is a very experimental feature, and may not 62 | be very useful in practice. 63 | 64 | -acme.addr string 65 | address where acme is serving 9P file system (default "/tmp/ns.fhs.:0/acme") 66 | -acme.net string 67 | network where acme is serving 9P file system (default "unix") 68 | -debug 69 | turn on debugging prints (deprecated: use -v) 70 | -dial value 71 | language server address for filename match (e.g. '\.go$:localhost:4389') 72 | -rootdir string 73 | root directory used for LSP initialization. (default "/") 74 | -server value 75 | language server command for filename match (e.g. '\.go$:gopls') 76 | -showconfig 77 | show configuration values and exit 78 | -v Verbose output 79 | -workspaces string 80 | colon-separated list of initial workspace directories 81 | */ 82 | package main 83 | -------------------------------------------------------------------------------- /cmd/Lone/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "9fans.net/acme-lsp/internal/lsp/acmelsp" 11 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 12 | "9fans.net/acme-lsp/internal/lsp/cmd" 13 | ) 14 | 15 | //go:generate ../../scripts/mkdocs.sh 16 | 17 | const mainDoc = `The program Lone is a standalone client for the acme text editor that 18 | interacts with a Language Server. 19 | 20 | Deprecated: This program is similar to the L command, except it also does 21 | the work of acme-lsp command by executing a LSP server on-demand. It's 22 | recommended to use the L and acme-lsp commands instead, which takes 23 | advantage of LSP server caches and should give faster responses. 24 | 25 | A Language Server implements the Language Server Protocol 26 | (see https://langserver.org/), which provides language features 27 | like auto complete, go to definition, find all references, etc. 28 | Lone depends on one or more language servers already being installed 29 | in the system. See this page of a list of language servers: 30 | https://microsoft.github.io/language-server-protocol/implementors/servers/. 31 | 32 | Usage: Lone [flags] [args...] 33 | 34 | List of sub-commands: 35 | 36 | comp 37 | Show auto-completion for the current cursor position. 38 | 39 | def 40 | Find where identifier at the cursor position is define and 41 | send the location to the plumber. 42 | 43 | fmt 44 | Organize imports and format current window buffer. 45 | 46 | hov 47 | Show more information about the identifier under the cursor 48 | ("hover"). 49 | 50 | monitor 51 | Format window buffer after each Put. 52 | 53 | refs 54 | List locations where the identifier under the cursor is used 55 | ("references"). 56 | 57 | rn 58 | Rename the identifier under the cursor to newname. 59 | 60 | servers 61 | Print list of known language servers. 62 | 63 | sig 64 | Show signature help for the function, method, etc. under 65 | the cursor. 66 | 67 | syms 68 | List symbols in the current file. 69 | 70 | assist [comp|hov|sig] 71 | A new window is created where completion (comp), hover 72 | (hov), or signature help (sig) output is shown depending 73 | on the cursor position in the focused window and the 74 | text surrounding the cursor. If the optional argument is 75 | given, the output will be limited to only that command. 76 | Note: this is a very experimental feature, and may not 77 | be very useful in practice. 78 | ` 79 | 80 | func usage() { 81 | os.Stderr.Write([]byte(mainDoc)) 82 | fmt.Fprintf(os.Stderr, "\n") 83 | flag.PrintDefaults() 84 | os.Exit(2) 85 | } 86 | 87 | func main() { 88 | flag.Usage = usage 89 | cfg := cmd.Setup(config.LangServerFlags) 90 | 91 | err := run(cfg, flag.Args()) 92 | if err != nil { 93 | log.Fatalf("%v", err) 94 | } 95 | } 96 | 97 | func run(cfg *config.Config, args []string) error { 98 | serverSet, err := acmelsp.NewServerSet(cfg, acmelsp.NewDiagnosticsWriter()) 99 | if err != nil { 100 | return fmt.Errorf("failed to create server set: %v", err) 101 | } 102 | defer serverSet.CloseAll() 103 | 104 | if len(serverSet.Data) == 0 { 105 | return fmt.Errorf("no servers found in the configuration file or command line flags") 106 | } 107 | 108 | if len(args) == 0 { 109 | usage() 110 | } 111 | 112 | fm, err := acmelsp.NewFileManager(serverSet, cfg) 113 | if err != nil { 114 | return fmt.Errorf("failed to create file manager: %v", err) 115 | } 116 | switch args[0] { 117 | case "win", "assist": // "win" is deprecated 118 | assist := "auto" 119 | if len(args) >= 2 { 120 | assist = args[1] 121 | } 122 | if err := acmelsp.Assist(serverSet, assist); err != nil { 123 | return fmt.Errorf("assist failed: %v", err) 124 | } 125 | return nil 126 | 127 | case "monitor": 128 | fm.Run() 129 | return nil 130 | 131 | case "servers": 132 | serverSet.PrintTo(os.Stdout) 133 | return nil 134 | } 135 | 136 | rc, err := acmelsp.CurrentWindowRemoteCmd(serverSet, fm) 137 | if err != nil { 138 | return fmt.Errorf("CurrentWindowRemoteCmd failed: %v", err) 139 | } 140 | 141 | ctx := context.Background() 142 | 143 | switch args[0] { 144 | case "comp": 145 | err = rc.Completion(ctx, acmelsp.CompleteNoEdit) 146 | case "def": 147 | err = rc.Definition(ctx, false) 148 | case "fmt": 149 | err = rc.OrganizeImportsAndFormat(ctx) 150 | case "hov": 151 | err = rc.Hover(ctx) 152 | case "refs": 153 | err = rc.References(ctx) 154 | case "rn": 155 | if len(args) < 2 { 156 | usage() 157 | } 158 | err = rc.Rename(ctx, args[1]) 159 | case "sig": 160 | err = rc.SignatureHelp(ctx) 161 | case "syms": 162 | err = rc.DocumentSymbol(ctx) 163 | default: 164 | return fmt.Errorf("unknown command %q", args[0]) 165 | } 166 | return err 167 | } 168 | -------------------------------------------------------------------------------- /cmd/acme-lsp/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | The program acme-lsp is a client for the acme text editor that 3 | acts as a proxy for a set of Language Server Protocol servers. 4 | 5 | A Language Server implements the Language Server Protocol 6 | (see https://langserver.org/), which provides language features 7 | like auto complete, go to definition, find all references, etc. 8 | Acme-lsp depends on one or more language servers already being 9 | installed in the system. See this page of a list of language servers: 10 | https://microsoft.github.io/language-server-protocol/implementors/servers/. 11 | 12 | Acme-lsp is optionally configured using a TOML-based configuration file 13 | located at UserConfigDir/acme-lsp/config.toml (the -showconfig flag 14 | prints the exact location). The command line flags will override the 15 | configuration values. The configuration options are described here: 16 | https://godoc.org/9fans.net/acme-lsp/internal/lsp/acmelsp/config#File 17 | 18 | Acme-lsp executes or connects to a set of LSP servers described in the 19 | configuration file or in the -server or -dial flags. It then listens for 20 | messages sent by the L command, which direct acme-lsp to run commands 21 | on the LSP servers and apply/show the results in acme. The communication 22 | protocol used here is an implementation detail that is subject to change. 23 | 24 | Acme-lsp watches for files created (New), loaded (Get), saved (Put), or 25 | deleted (Del) in acme, and tells the LSP server about these changes. The 26 | LSP server in turn responds by sending diagnostics information (compiler 27 | errors, lint errors, etc.) which are shown in a "/LSP/Diagnostics" window. 28 | Also, when Put is executed in an acme window, acme-lsp will organize 29 | import paths in the window and format it by default. This behavior can 30 | be changed by the FormatOnPut and CodeActionsOnPut configuration options. 31 | 32 | Usage: acme-lsp [flags] 33 | 34 | -acme.addr string 35 | address where acme is serving 9P file system (default "/tmp/ns.fhs.:0/acme") 36 | -acme.net string 37 | network where acme is serving 9P file system (default "unix") 38 | -debug 39 | turn on debugging prints (deprecated: use -v) 40 | -dial value 41 | language server address for filename match (e.g. '\.go$:localhost:4389') 42 | -proxy.addr string 43 | address used for communication between acme-lsp and L (default "/tmp/ns.fhs.:0/acme-lsp.rpc") 44 | -proxy.net string 45 | network used for communication between acme-lsp and L (default "unix") 46 | -rootdir string 47 | root directory used for LSP initialization. (default "/") 48 | -server value 49 | language server command for filename match (e.g. '\.go$:gopls') 50 | -showconfig 51 | show configuration values and exit 52 | -v Verbose output 53 | -workspaces string 54 | colon-separated list of initial workspace directories 55 | */ 56 | package main 57 | -------------------------------------------------------------------------------- /cmd/acme-lsp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "9fans.net/acme-lsp/internal/lsp/acmelsp" 11 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 12 | "9fans.net/acme-lsp/internal/lsp/cmd" 13 | ) 14 | 15 | //go:generate ../../scripts/mkdocs.sh 16 | 17 | const mainDoc = `The program acme-lsp is a client for the acme text editor that 18 | acts as a proxy for a set of Language Server Protocol servers. 19 | 20 | A Language Server implements the Language Server Protocol 21 | (see https://langserver.org/), which provides language features 22 | like auto complete, go to definition, find all references, etc. 23 | Acme-lsp depends on one or more language servers already being 24 | installed in the system. See this page of a list of language servers: 25 | https://microsoft.github.io/language-server-protocol/implementors/servers/. 26 | 27 | Acme-lsp is optionally configured using a TOML-based configuration file 28 | located at UserConfigDir/acme-lsp/config.toml (the -showconfig flag 29 | prints the exact location). The command line flags will override the 30 | configuration values. The configuration options are described here: 31 | https://godoc.org/9fans.net/acme-lsp/internal/lsp/acmelsp/config#File 32 | 33 | Acme-lsp executes or connects to a set of LSP servers described in the 34 | configuration file or in the -server or -dial flags. It then listens for 35 | messages sent by the L command, which direct acme-lsp to run commands 36 | on the LSP servers and apply/show the results in acme. The communication 37 | protocol used here is an implementation detail that is subject to change. 38 | 39 | Acme-lsp watches for files created (New), loaded (Get), saved (Put), or 40 | deleted (Del) in acme, and tells the LSP server about these changes. The 41 | LSP server in turn responds by sending diagnostics information (compiler 42 | errors, lint errors, etc.) which are shown in a "/LSP/Diagnostics" window. 43 | Also, when Put is executed in an acme window, acme-lsp will organize 44 | import paths in the window and format it by default. This behavior can 45 | be changed by the FormatOnPut and CodeActionsOnPut configuration options. 46 | 47 | Usage: acme-lsp [flags] 48 | ` 49 | 50 | func usage() { 51 | os.Stderr.Write([]byte(mainDoc)) 52 | fmt.Fprintf(os.Stderr, "\n") 53 | flag.PrintDefaults() 54 | os.Exit(2) 55 | } 56 | 57 | func main() { 58 | flag.Usage = usage 59 | cfg := cmd.Setup(config.LangServerFlags | config.ProxyFlags) 60 | 61 | ctx := context.Background() 62 | app, err := NewApplication(ctx, cfg, flag.Args()) 63 | if err != nil { 64 | log.Fatalf("%v", err) 65 | } 66 | err = app.Run(ctx) 67 | if err != nil { 68 | log.Fatalf("%v", err) 69 | } 70 | } 71 | 72 | type Application struct { 73 | cfg *config.Config 74 | fm *acmelsp.FileManager 75 | ss *acmelsp.ServerSet 76 | } 77 | 78 | func NewApplication(ctx context.Context, cfg *config.Config, args []string) (*Application, error) { 79 | ss, err := acmelsp.NewServerSet(cfg, acmelsp.NewDiagnosticsWriter()) 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to create server set: %v", err) 82 | } 83 | 84 | if len(ss.Data) == 0 { 85 | return nil, fmt.Errorf("no servers found in the configuration file or command line flags") 86 | } 87 | 88 | fm, err := acmelsp.NewFileManager(ss, cfg) 89 | if err != nil { 90 | return nil, fmt.Errorf("failed to create file manager: %v", err) 91 | } 92 | return &Application{ 93 | cfg: cfg, 94 | ss: ss, 95 | fm: fm, 96 | }, nil 97 | } 98 | 99 | func (app *Application) Run(ctx context.Context) error { 100 | go app.fm.Run() 101 | 102 | err := acmelsp.ListenAndServeProxy(ctx, app.cfg, app.ss, app.fm) 103 | if err != nil { 104 | return fmt.Errorf("proxy failed: %v", err) 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /cmd/acmefocused/main.go: -------------------------------------------------------------------------------- 1 | // Program acmefocused is a server that tells acme's focused window ID 2 | // to clients. 3 | // 4 | // Acmefocus will listen on a unix domain socket at NAMESPACE/acmefocused. 5 | // The window ID is written to a client and the connection to the client 6 | // is closed immediately. The window ID is useful for acme clients that 7 | // expects $winid environment variable to be defined but it is being 8 | // run outside of acme. 9 | // 10 | // Usage: 11 | // 12 | // $ acme & 13 | // $ acmefocused & 14 | // $ dial 'unix!'$(namespace)/acmefocused 15 | // 1 16 | // $ 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "log" 23 | "net" 24 | "path/filepath" 25 | "sync" 26 | 27 | "9fans.net/acme-lsp/internal/p9service" 28 | "github.com/fhs/9fans-go/acme" 29 | "github.com/fhs/9fans-go/plan9/client" 30 | ) 31 | 32 | func main() { 33 | var fw focusedWin 34 | go fw.readLog() 35 | 36 | listenAndServe(listenAddr(), func(conn net.Conn) { 37 | fmt.Fprintf(conn, "%d\n", fw.ID()) 38 | conn.Close() 39 | }) 40 | } 41 | 42 | type focusedWin struct { 43 | id int 44 | mu sync.Mutex 45 | } 46 | 47 | // ID returns the window ID of currently focused window. 48 | func (fw *focusedWin) ID() int { 49 | fw.mu.Lock() 50 | defer fw.mu.Unlock() 51 | return fw.id 52 | } 53 | 54 | func (fw *focusedWin) readLog() { 55 | alog, err := acme.Log() 56 | if err != nil { 57 | log.Fatalf("failed to open acme log: %v\n", err) 58 | } 59 | defer alog.Close() 60 | for { 61 | ev, err := alog.Read() 62 | if err != nil { 63 | log.Fatalf("acme log read failed: %v\n", err) 64 | } 65 | if ev.Op == "focus" { 66 | fw.mu.Lock() 67 | fw.id = ev.ID 68 | fw.mu.Unlock() 69 | } 70 | } 71 | } 72 | 73 | func listenAddr() string { 74 | return filepath.Join(client.Namespace(), "acmefocused") 75 | } 76 | 77 | func listenAndServe(addr string, handle func(net.Conn)) { 78 | ln, err := p9service.Listen(context.Background(), "unix", addr) 79 | if err != nil { 80 | log.Fatalf("listen failed: %v\n", err) 81 | } 82 | defer ln.Close() 83 | 84 | for { 85 | conn, err := ln.Accept() 86 | if err != nil { 87 | log.Fatalf("accept failed: %v\n", err) 88 | } 89 | go handle(conn) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/acmefocused/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "os" 10 | "os/exec" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | "time" 15 | 16 | "9fans.net/acme-lsp/internal/p9service" 17 | ) 18 | 19 | const testWinID = "42" 20 | 21 | func TestMain(m *testing.M) { 22 | switch os.Getenv("TEST_MAIN") { 23 | case "acmefocused": 24 | listenAndServe(listenAddr(), func(conn net.Conn) { 25 | fmt.Fprintf(conn, "%s\n", testWinID) 26 | conn.Close() 27 | }) 28 | default: 29 | os.Exit(m.Run()) 30 | } 31 | } 32 | 33 | func TestListenAndServe(t *testing.T) { 34 | if runtime.GOOS == "windows" { 35 | t.Skip("skipping on windows because unix domain sockets are not supported") 36 | } 37 | 38 | dir, err := ioutil.TempDir("", "acmefocused-test") 39 | if err != nil { 40 | t.Fatalf("couldn't create temporary directory: %v", err) 41 | } 42 | defer os.RemoveAll(dir) 43 | os.Setenv("NAMESPACE", dir) 44 | addr := listenAddr() 45 | 46 | cmd := exec.Command(os.Args[0]) 47 | cmd.Env = append( 48 | os.Environ(), 49 | "TEST_MAIN=acmefocused", 50 | fmt.Sprintf("NAMESPACE=%v", dir), 51 | ) 52 | cmd.Stderr = os.Stderr 53 | cmd.Stdout = os.Stdout 54 | killed := make(chan struct{}) 55 | err = cmd.Start() 56 | if err != nil { 57 | t.Fatalf("command start failed: %v", err) 58 | } 59 | go func() { 60 | err := cmd.Wait() 61 | if e, ok := err.(*exec.ExitError); !ok || !strings.Contains(e.Error(), "killed") { 62 | t.Errorf("process exited with error %v; want exit due to kill", err) 63 | } 64 | close(killed) 65 | }() 66 | 67 | for i := 0; i < 10; i++ { 68 | conn, err := net.Dial("unix", addr) 69 | if err != nil { 70 | if i >= 9 { 71 | t.Fatalf("dial failed after multiple attempts: %v", err) 72 | } 73 | time.Sleep(time.Second) 74 | continue 75 | } 76 | want := []byte(testWinID + "\n") 77 | got, err := ioutil.ReadAll(conn) 78 | if err != nil { 79 | t.Errorf("read failed: %v", err) 80 | } 81 | if !bytes.Equal(want, got) { 82 | t.Errorf("got bytes %q; want %q", got, want) 83 | } 84 | break 85 | } 86 | 87 | err = cmd.Process.Kill() 88 | if err != nil { 89 | t.Fatalf("kill failed: %v", err) 90 | } 91 | <-killed 92 | 93 | // This should reuse the unix domain socket. 94 | _, err = p9service.Listen(context.Background(), "unix", addr) 95 | if err != nil { 96 | t.Errorf("second listen returned error %v", err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module 9fans.net/acme-lsp 2 | 3 | go 1.21.9 4 | 5 | require ( 6 | 9fans.net/internal/go-lsp v0.0.0-20240621142652-b2eeae9fa405 7 | github.com/BurntSushi/toml v0.3.1 8 | github.com/fhs/9fans-go v0.0.0-fhs.20200606 9 | github.com/google/go-cmp v0.3.0 10 | github.com/sourcegraph/jsonrpc2 v0.2.0 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | 9fans.net/internal/go-lsp v0.0.0-20240406143457-bb36aa85881a h1:XAfaMe2Mt0aBSGuG6jfuJv3ZxkFyaV7kmtZzdizJDY4= 2 | 9fans.net/internal/go-lsp v0.0.0-20240406143457-bb36aa85881a/go.mod h1:0/rCSlZo+66g+WpzTJuod4f7dHG6Yo8kxehfOhrt9RU= 3 | 9fans.net/internal/go-lsp v0.0.0-20240621142652-b2eeae9fa405 h1:+pdspglkOl5x1bQLI4TS/hZNTJ3E9BwqXBbuIqkQz5Y= 4 | 9fans.net/internal/go-lsp v0.0.0-20240621142652-b2eeae9fa405/go.mod h1:0/rCSlZo+66g+WpzTJuod4f7dHG6Yo8kxehfOhrt9RU= 5 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 6 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 7 | github.com/fhs/9fans-go v0.0.0-fhs.20200606 h1:kwqns/76paQLNLDx9I5UFkFG1z1tSqHq8a3j/I0kLr4= 8 | github.com/fhs/9fans-go v0.0.0-fhs.20200606/go.mod h1:9PelHyep+qBAEyYdGnqAa66ZGjWE0L8EJP+GIDz8p7M= 9 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 10 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 11 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 12 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 13 | github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U= 14 | github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= 15 | -------------------------------------------------------------------------------- /internal/acme/LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2009 Google Inc. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Google Inc. nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | // 29 | // Subject to the terms and conditions of this License, Google hereby 30 | // grants to You a perpetual, worldwide, non-exclusive, no-charge, 31 | // royalty-free, irrevocable (except as stated in this section) patent 32 | // license to make, have made, use, offer to sell, sell, import, and 33 | // otherwise transfer this implementation of Go, where such license 34 | // applies only to those patent claims licensable by Google that are 35 | // necessarily infringed by use of this implementation of Go. If You 36 | // institute patent litigation against any entity (including a 37 | // cross-claim or counterclaim in a lawsuit) alleging that this 38 | // implementation of Go or a Contribution incorporated within this 39 | // implementation of Go constitutes direct or contributory patent 40 | // infringement, then any patent licenses granted to You under this 41 | // License for this implementation of Go shall terminate as of the date 42 | // such litigation is filed. 43 | -------------------------------------------------------------------------------- /internal/acme/acme.go: -------------------------------------------------------------------------------- 1 | // Package acme is a simple interface for interacting with acme windows. 2 | // 3 | // Many of the functions in this package take a format string and optional 4 | // parameters. In the documentation, the notation format, ... denotes the result 5 | // of formatting the string and arguments using fmt.Sprintf. 6 | package acme // import "github.com/fhs/9fans-go/acme" 7 | 8 | import ( 9 | "bufio" 10 | "bytes" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "io/ioutil" 15 | "log" 16 | "os" 17 | "path" 18 | "reflect" 19 | "sort" 20 | "strconv" 21 | "strings" 22 | "sync" 23 | "time" 24 | 25 | "github.com/fhs/9fans-go/draw" 26 | "github.com/fhs/9fans-go/plan9" 27 | "github.com/fhs/9fans-go/plan9/client" 28 | ) 29 | 30 | // A Win represents a single acme window and its control files. 31 | type Win struct { 32 | id int 33 | ctl *client.Fid 34 | tag *client.Fid 35 | body *client.Fid 36 | addr *client.Fid 37 | event *client.Fid 38 | data *client.Fid 39 | xdata *client.Fid 40 | errors *client.Fid 41 | ebuf *bufio.Reader 42 | c chan *Event 43 | next, prev *Win 44 | buf []byte 45 | e2, e3, e4 Event 46 | name string 47 | 48 | errorPrefix string 49 | } 50 | 51 | var windowsMu sync.Mutex 52 | var windows, last *Win 53 | var autoExit bool 54 | 55 | var Network string 56 | var Address string 57 | 58 | var fsys *client.Fsys 59 | var fsysErr error 60 | var fsysOnce sync.Once 61 | 62 | // AutoExit sets whether to call os.Exit the next time the last managed acme window is deleted. 63 | // If there are no acme windows at the time of the call, the exit does not happen until one 64 | // is created and then deleted. 65 | func AutoExit(exit bool) { 66 | windowsMu.Lock() 67 | defer windowsMu.Unlock() 68 | autoExit = exit 69 | } 70 | 71 | // New creates a new window. 72 | func New() (*Win, error) { 73 | fsysOnce.Do(mountAcme) 74 | if fsysErr != nil { 75 | return nil, fsysErr 76 | } 77 | fid, err := fsys.Open("new/ctl", plan9.ORDWR) 78 | if err != nil { 79 | return nil, err 80 | } 81 | buf := make([]byte, 100) 82 | n, err := fid.Read(buf) 83 | if err != nil { 84 | fid.Close() 85 | return nil, err 86 | } 87 | a := strings.Fields(string(buf[0:n])) 88 | if len(a) == 0 { 89 | fid.Close() 90 | return nil, errors.New("short read from acme/new/ctl") 91 | } 92 | id, err := strconv.Atoi(a[0]) 93 | if err != nil { 94 | fid.Close() 95 | return nil, errors.New("invalid window id in acme/new/ctl: " + a[0]) 96 | } 97 | return Open(id, fid) 98 | } 99 | 100 | type WinInfo struct { 101 | ID int 102 | Name string 103 | } 104 | 105 | // A LogReader provides read access to the acme log file. 106 | type LogReader struct { 107 | f *client.Fid 108 | buf [8192]byte 109 | } 110 | 111 | func (r *LogReader) Close() error { 112 | return r.f.Close() 113 | } 114 | 115 | // A LogEvent is a single event in the acme log file. 116 | type LogEvent struct { 117 | ID int 118 | Op string 119 | Name string 120 | } 121 | 122 | // Read reads an event from the acme log file. 123 | func (r *LogReader) Read() (LogEvent, error) { 124 | n, err := r.f.Read(r.buf[:]) 125 | if err != nil { 126 | return LogEvent{}, err 127 | } 128 | f := strings.SplitN(string(r.buf[:n]), " ", 3) 129 | if len(f) != 3 { 130 | return LogEvent{}, fmt.Errorf("malformed log event") 131 | } 132 | id, _ := strconv.Atoi(f[0]) 133 | op := f[1] 134 | name := f[2] 135 | name = strings.TrimSpace(name) 136 | return LogEvent{id, op, name}, nil 137 | } 138 | 139 | // Log returns a reader reading the acme/log file. 140 | func Log() (*LogReader, error) { 141 | fsysOnce.Do(mountAcme) 142 | if fsysErr != nil { 143 | return nil, fsysErr 144 | } 145 | f, err := fsys.Open("log", plan9.OREAD) 146 | if err != nil { 147 | return nil, err 148 | } 149 | return &LogReader{f: f}, nil 150 | } 151 | 152 | // Windows returns a list of the existing acme windows. 153 | func Windows() ([]WinInfo, error) { 154 | fsysOnce.Do(mountAcme) 155 | if fsysErr != nil { 156 | return nil, fsysErr 157 | } 158 | index, err := fsys.Open("index", plan9.OREAD) 159 | if err != nil { 160 | return nil, err 161 | } 162 | defer index.Close() 163 | data, err := ioutil.ReadAll(index) 164 | if err != nil { 165 | return nil, err 166 | } 167 | var info []WinInfo 168 | for _, line := range strings.Split(string(data), "\n") { 169 | f := strings.Fields(line) 170 | if len(f) < 6 { 171 | continue 172 | } 173 | n, _ := strconv.Atoi(f[0]) 174 | info = append(info, WinInfo{n, f[5]}) 175 | } 176 | return info, nil 177 | } 178 | 179 | // Show looks and causes acme to show the window with the given name, 180 | // returning that window. 181 | // If this process has not created a window with the given name 182 | // (or if any such window has since been deleted), 183 | // Show returns nil. 184 | func Show(name string) *Win { 185 | windowsMu.Lock() 186 | defer windowsMu.Unlock() 187 | 188 | for w := windows; w != nil; w = w.next { 189 | if w.name == name { 190 | if err := w.Ctl("show"); err != nil { 191 | w.dropLocked() 192 | return nil 193 | } 194 | return w 195 | } 196 | } 197 | return nil 198 | } 199 | 200 | // Open connects to the existing window with the given id. 201 | // If ctl is non-nil, Open uses it as the window's control file 202 | // and takes ownership of it. 203 | func Open(id int, ctl *client.Fid) (*Win, error) { 204 | fsysOnce.Do(mountAcme) 205 | if fsysErr != nil { 206 | return nil, fsysErr 207 | } 208 | if ctl == nil { 209 | var err error 210 | ctl, err = fsys.Open(fmt.Sprintf("%d/ctl", id), plan9.ORDWR) 211 | if err != nil { 212 | return nil, err 213 | } 214 | } 215 | 216 | w := new(Win) 217 | w.id = id 218 | w.ctl = ctl 219 | w.next = nil 220 | w.prev = last 221 | if last != nil { 222 | last.next = w 223 | } else { 224 | windows = w 225 | } 226 | last = w 227 | return w, nil 228 | } 229 | 230 | // Addr writes format, ... to the window's addr file. 231 | func (w *Win) Addr(format string, args ...interface{}) error { 232 | return w.Fprintf("addr", format, args...) 233 | } 234 | 235 | // CloseFiles closes all the open files associated with the window w. 236 | // (These file descriptors are cached across calls to Ctl, etc.) 237 | func (w *Win) CloseFiles() { 238 | w.ctl.Close() 239 | w.ctl = nil 240 | 241 | w.body.Close() 242 | w.body = nil 243 | 244 | w.addr.Close() 245 | w.addr = nil 246 | 247 | w.tag.Close() 248 | w.tag = nil 249 | 250 | w.event.Close() 251 | w.event = nil 252 | w.ebuf = nil 253 | 254 | w.data.Close() 255 | w.data = nil 256 | 257 | w.xdata.Close() 258 | w.xdata = nil 259 | 260 | w.errors.Close() 261 | w.errors = nil 262 | } 263 | 264 | // Ctl writes the command format, ... to the window's ctl file. 265 | func (w *Win) Ctl(format string, args ...interface{}) error { 266 | return w.Fprintf("ctl", format+"\n", args...) 267 | } 268 | 269 | // Winctl deletes the window, writing `del' (or, if sure is true, `delete') to the ctl file. 270 | func (w *Win) Del(sure bool) error { 271 | cmd := "del" 272 | if sure { 273 | cmd = "delete" 274 | } 275 | return w.Ctl(cmd) 276 | } 277 | 278 | // DeleteAll deletes all windows. 279 | func DeleteAll() { 280 | for w := windows; w != nil; w = w.next { 281 | w.Ctl("delete") 282 | } 283 | } 284 | 285 | func (w *Win) OpenEvent() error { 286 | _, err := w.fid("event") 287 | return err 288 | } 289 | 290 | func (w *Win) fid(name string) (*client.Fid, error) { 291 | var f **client.Fid 292 | var mode uint8 = plan9.ORDWR 293 | switch name { 294 | case "addr": 295 | f = &w.addr 296 | case "body": 297 | f = &w.body 298 | case "ctl": 299 | f = &w.ctl 300 | case "data": 301 | f = &w.data 302 | case "event": 303 | f = &w.event 304 | case "tag": 305 | f = &w.tag 306 | case "xdata": 307 | f = &w.xdata 308 | case "errors": 309 | f = &w.errors 310 | mode = plan9.OWRITE 311 | default: 312 | return nil, errors.New("unknown acme file: " + name) 313 | } 314 | if *f == nil { 315 | var err error 316 | *f, err = fsys.Open(fmt.Sprintf("%d/%s", w.id, name), mode) 317 | if err != nil { 318 | return nil, err 319 | } 320 | } 321 | return *f, nil 322 | } 323 | 324 | // ReadAll 325 | func (w *Win) ReadAll(file string) ([]byte, error) { 326 | f, err := w.fid(file) 327 | f.Seek(0, 0) 328 | if err != nil { 329 | return nil, err 330 | } 331 | return ioutil.ReadAll(f) 332 | } 333 | 334 | func (w *Win) ID() int { 335 | return w.id 336 | } 337 | 338 | func (w *Win) Name(format string, args ...interface{}) error { 339 | name := fmt.Sprintf(format, args...) 340 | if err := w.Ctl("name %s", name); err != nil { 341 | return err 342 | } 343 | w.name = name 344 | return nil 345 | } 346 | 347 | func (w *Win) Fprintf(file, format string, args ...interface{}) error { 348 | f, err := w.fid(file) 349 | if err != nil { 350 | return err 351 | } 352 | var buf bytes.Buffer 353 | fmt.Fprintf(&buf, format, args...) 354 | _, err = f.Write(buf.Bytes()) 355 | return err 356 | } 357 | 358 | func (w *Win) Read(file string, b []byte) (n int, err error) { 359 | f, err := w.fid(file) 360 | if err != nil { 361 | return 0, err 362 | } 363 | return f.Read(b) 364 | } 365 | 366 | func (w *Win) ReadAddr() (q0, q1 int, err error) { 367 | f, err := w.fid("addr") 368 | if err != nil { 369 | return 0, 0, err 370 | } 371 | buf := make([]byte, 40) 372 | n, err := f.ReadAt(buf, 0) 373 | if err != nil && err != io.EOF { 374 | return 0, 0, err 375 | } 376 | a := strings.Fields(string(buf[0:n])) 377 | if len(a) < 2 { 378 | return 0, 0, errors.New("short read from acme addr") 379 | } 380 | q0, err0 := strconv.Atoi(a[0]) 381 | q1, err1 := strconv.Atoi(a[1]) 382 | if err0 != nil || err1 != nil { 383 | return 0, 0, errors.New("invalid read from acme addr") 384 | } 385 | return q0, q1, nil 386 | } 387 | 388 | func (w *Win) Seek(file string, offset int64, whence int) (int64, error) { 389 | f, err := w.fid(file) 390 | if err != nil { 391 | return 0, err 392 | } 393 | return f.Seek(offset, whence) 394 | } 395 | 396 | func (w *Win) Write(file string, b []byte) (n int, err error) { 397 | f, err := w.fid(file) 398 | if err != nil { 399 | return 0, err 400 | } 401 | return f.Write(b) 402 | } 403 | 404 | const eventSize = 256 405 | 406 | // An Event represents an event originating in a particular window. 407 | // The fields correspond to the fields in acme's event messages. 408 | // See http://swtch.com/plan9port/man/man4/acme.html for details. 409 | type Event struct { 410 | // The two event characters, indicating origin and type of action 411 | C1, C2 rune 412 | 413 | // The character addresses of the action. 414 | // If the original event had an empty selection (OrigQ0=OrigQ1) 415 | // and was accompanied by an expansion (the 2 bit is set in Flag), 416 | // then Q0 and Q1 will indicate the expansion rather than the 417 | // original event. 418 | Q0, Q1 int 419 | 420 | // The Q0 and Q1 of the original event, even if it was expanded. 421 | // If there was no expansion, OrigQ0=Q0 and OrigQ1=Q1. 422 | OrigQ0, OrigQ1 int 423 | 424 | // The flag bits. 425 | Flag int 426 | 427 | // The number of bytes in the optional text. 428 | Nb int 429 | 430 | // The number of characters (UTF-8 sequences) in the optional text. 431 | Nr int 432 | 433 | // The optional text itself, encoded in UTF-8. 434 | Text []byte 435 | 436 | // The chorded argument, if present (the 8 bit is set in the flag). 437 | Arg []byte 438 | 439 | // The chorded location, if present (the 8 bit is set in the flag). 440 | Loc []byte 441 | } 442 | 443 | // ReadEvent reads the next event from the window's event file. 444 | func (w *Win) ReadEvent() (e *Event, err error) { 445 | defer func() { 446 | if v := recover(); v != nil { 447 | e = nil 448 | err = errors.New("malformed acme event: " + v.(string)) 449 | } 450 | }() 451 | 452 | if _, err = w.fid("event"); err != nil { 453 | return nil, err 454 | } 455 | 456 | e = new(Event) 457 | w.gete(e) 458 | e.OrigQ0 = e.Q0 459 | e.OrigQ1 = e.Q1 460 | 461 | // expansion 462 | if e.Flag&2 != 0 { 463 | e2 := new(Event) 464 | w.gete(e2) 465 | if e.Q0 == e.Q1 { 466 | e2.OrigQ0 = e.Q0 467 | e2.OrigQ1 = e.Q1 468 | e2.Flag = e.Flag 469 | e = e2 470 | } 471 | } 472 | 473 | // chorded argument 474 | if e.Flag&8 != 0 { 475 | e3 := new(Event) 476 | e4 := new(Event) 477 | w.gete(e3) 478 | w.gete(e4) 479 | e.Arg = e3.Text 480 | e.Loc = e4.Text 481 | } 482 | 483 | return e, nil 484 | } 485 | 486 | func (w *Win) gete(e *Event) { 487 | if w.ebuf == nil { 488 | w.ebuf = bufio.NewReader(w.event) 489 | } 490 | e.C1 = w.getec() 491 | e.C2 = w.getec() 492 | e.Q0 = w.geten() 493 | e.Q1 = w.geten() 494 | e.Flag = w.geten() 495 | e.Nr = w.geten() 496 | if e.Nr > eventSize { 497 | panic("event string too long") 498 | } 499 | r := make([]rune, e.Nr) 500 | for i := 0; i < e.Nr; i++ { 501 | r[i] = w.getec() 502 | } 503 | e.Text = []byte(string(r)) 504 | if w.getec() != '\n' { 505 | panic("phase error") 506 | } 507 | } 508 | 509 | func (w *Win) getec() rune { 510 | c, _, err := w.ebuf.ReadRune() 511 | if err != nil { 512 | panic(err.Error()) 513 | } 514 | return c 515 | } 516 | 517 | func (w *Win) geten() int { 518 | var ( 519 | c rune 520 | n int 521 | ) 522 | for { 523 | c = w.getec() 524 | if c < '0' || c > '9' { 525 | break 526 | } 527 | n = n*10 + int(c) - '0' 528 | } 529 | if c != ' ' { 530 | panic("event number syntax") 531 | } 532 | return n 533 | } 534 | 535 | // WriteEvent writes an event back to the window's event file, 536 | // indicating to acme that the event should be handled internally. 537 | func (w *Win) WriteEvent(e *Event) error { 538 | var buf bytes.Buffer 539 | fmt.Fprintf(&buf, "%c%c%d %d \n", e.C1, e.C2, e.Q0, e.Q1) 540 | _, err := w.Write("event", buf.Bytes()) 541 | return err 542 | } 543 | 544 | // EventChan returns a channel on which events can be read. 545 | // The first call to EventChan allocates a channel and starts a 546 | // new goroutine that loops calling ReadEvent and sending 547 | // the result into the channel. Subsequent calls return the 548 | // same channel. Clients should not call ReadEvent after calling 549 | // EventChan. 550 | func (w *Win) EventChan() <-chan *Event { 551 | if w.c == nil { 552 | w.c = make(chan *Event, 0) 553 | go w.eventReader() 554 | } 555 | return w.c 556 | } 557 | 558 | func (w *Win) eventReader() { 559 | for { 560 | e, err := w.ReadEvent() 561 | if err != nil { 562 | break 563 | } 564 | w.c <- e 565 | } 566 | w.c <- new(Event) // make sure event reader is done processing last event; drop might exit 567 | w.drop() 568 | close(w.c) 569 | } 570 | 571 | func (w *Win) drop() { 572 | windowsMu.Lock() 573 | defer windowsMu.Unlock() 574 | w.dropLocked() 575 | } 576 | 577 | func (w *Win) dropLocked() { 578 | if w.prev == nil && w.next == nil && windows != w { 579 | return 580 | } 581 | if w.prev != nil { 582 | w.prev.next = w.next 583 | } else { 584 | windows = w.next 585 | } 586 | if w.next != nil { 587 | w.next.prev = w.prev 588 | } else { 589 | last = w.prev 590 | } 591 | w.prev = nil 592 | w.next = nil 593 | if autoExit && windows == nil { 594 | os.Exit(0) 595 | } 596 | } 597 | 598 | var fontCache struct { 599 | sync.Mutex 600 | m map[string]*draw.Font 601 | } 602 | 603 | // Font returns the window's current tab width (in zeros) and font. 604 | func (w *Win) Font() (tab int, font *draw.Font, err error) { 605 | ctl := make([]byte, 1000) 606 | w.Seek("ctl", 0, 0) 607 | n, err := w.Read("ctl", ctl) 608 | if err != nil { 609 | return 0, nil, err 610 | } 611 | f := strings.Fields(string(ctl[:n])) 612 | if len(f) < 8 { 613 | return 0, nil, fmt.Errorf("malformed ctl file") 614 | } 615 | tab, _ = strconv.Atoi(f[7]) 616 | if tab == 0 { 617 | return 0, nil, fmt.Errorf("malformed ctl file") 618 | } 619 | name := f[6] 620 | 621 | fontCache.Lock() 622 | font = fontCache.m[name] 623 | fontCache.Unlock() 624 | 625 | if font != nil { 626 | return tab, font, nil 627 | } 628 | 629 | var disp *draw.Display = nil 630 | font, err = disp.OpenFont(name) 631 | if err != nil { 632 | return tab, nil, err 633 | } 634 | 635 | fontCache.Lock() 636 | if fontCache.m == nil { 637 | fontCache.m = make(map[string]*draw.Font) 638 | } 639 | if fontCache.m[name] != nil { 640 | font = fontCache.m[name] 641 | } else { 642 | fontCache.m[name] = font 643 | } 644 | fontCache.Unlock() 645 | 646 | return tab, font, nil 647 | } 648 | 649 | // Blink starts the window tag blinking and returns a function that stops it. 650 | // When stop returns, the blinking is over and the window state is clean. 651 | func (w *Win) Blink() (stop func()) { 652 | c := make(chan struct{}) 653 | go func() { 654 | t := time.NewTicker(1000 * time.Millisecond) 655 | defer t.Stop() 656 | dirty := false 657 | for { 658 | select { 659 | case <-t.C: 660 | dirty = !dirty 661 | if dirty { 662 | w.Ctl("dirty") 663 | } else { 664 | w.Ctl("clean") 665 | } 666 | case <-c: 667 | w.Ctl("clean") 668 | c <- struct{}{} 669 | return 670 | } 671 | } 672 | }() 673 | return func() { 674 | c <- struct{}{} 675 | <-c 676 | } 677 | } 678 | 679 | // Sort sorts the lines in the current address range 680 | // according to the comparison function. 681 | func (w *Win) Sort(less func(x, y string) bool) error { 682 | q0, q1, err := w.ReadAddr() 683 | if err != nil { 684 | return err 685 | } 686 | data, err := w.ReadAll("xdata") 687 | if err != nil { 688 | return err 689 | } 690 | suffix := "" 691 | lines := strings.Split(string(data), "\n") 692 | if lines[len(lines)-1] == "" { 693 | suffix = "\n" 694 | lines = lines[:len(lines)-1] 695 | } 696 | sort.SliceStable(lines, func(i, j int) bool { return less(lines[i], lines[j]) }) 697 | w.Addr("#%d,#%d", q0, q1) 698 | w.Write("data", []byte(strings.Join(lines, "\n")+suffix)) 699 | return nil 700 | } 701 | 702 | // PrintTabbed prints tab-separated columnated text to body, 703 | // replacing single tabs with runs of tabs as needed to align columns. 704 | func (w *Win) PrintTabbed(text string) { 705 | tab, font, _ := w.Font() 706 | 707 | lines := strings.SplitAfter(text, "\n") 708 | var allRows [][]string 709 | for _, line := range lines { 710 | if line == "" { 711 | continue 712 | } 713 | line = strings.TrimSuffix(line, "\n") 714 | allRows = append(allRows, strings.Split(line, "\t")) 715 | } 716 | 717 | var buf bytes.Buffer 718 | for len(allRows) > 0 { 719 | if row := allRows[0]; len(row) <= 1 { 720 | if len(row) > 0 { 721 | buf.WriteString(row[0]) 722 | } 723 | buf.WriteString("\n") 724 | allRows = allRows[1:] 725 | continue 726 | } 727 | 728 | i := 0 729 | for i < len(allRows) && len(allRows[i]) > 1 { 730 | i++ 731 | } 732 | 733 | rows := allRows[:i] 734 | allRows = allRows[i:] 735 | 736 | var wid []int 737 | if font != nil { 738 | for _, row := range rows { 739 | for len(wid) < len(row) { 740 | wid = append(wid, 0) 741 | } 742 | for i, col := range row { 743 | n := font.StringWidth(col) 744 | if wid[i] < n { 745 | wid[i] = n 746 | } 747 | } 748 | } 749 | } 750 | 751 | for _, row := range rows { 752 | for i, col := range row { 753 | buf.WriteString(col) 754 | if i == len(row)-1 { 755 | break 756 | } 757 | if font == nil || tab == 0 { 758 | buf.WriteString("\t") 759 | continue 760 | } 761 | pos := font.StringWidth(col) 762 | for pos <= wid[i] { 763 | buf.WriteString("\t") 764 | pos += tab - pos%tab 765 | } 766 | } 767 | buf.WriteString("\n") 768 | } 769 | } 770 | 771 | w.Write("body", buf.Bytes()) 772 | } 773 | 774 | // Clear clears the window body. 775 | func (w *Win) Clear() { 776 | w.Addr(",") 777 | w.Write("data", nil) 778 | } 779 | 780 | type EventHandler interface { 781 | Execute(cmd string) bool 782 | Look(arg string) bool 783 | } 784 | 785 | func (w *Win) loadText(e *Event, h EventHandler) { 786 | if len(e.Text) == 0 && e.Q0 < e.Q1 { 787 | w.Addr("#%d,#%d", e.Q0, e.Q1) 788 | data, err := w.ReadAll("xdata") 789 | if err != nil { 790 | w.Err(err.Error()) 791 | } 792 | e.Text = data 793 | } 794 | } 795 | 796 | func (w *Win) EventLoop(h EventHandler) { 797 | for e := range w.EventChan() { 798 | switch e.C2 { 799 | case 'x', 'X': // execute 800 | cmd := strings.TrimSpace(string(e.Text)) 801 | if !w.execute(h, cmd) { 802 | w.WriteEvent(e) 803 | } 804 | case 'l', 'L': // look 805 | // TODO(rsc): Expand selection, especially for URLs. 806 | w.loadText(e, h) 807 | if !h.Look(string(e.Text)) { 808 | w.WriteEvent(e) 809 | } 810 | } 811 | } 812 | } 813 | 814 | func (w *Win) execute(h EventHandler, cmd string) bool { 815 | verb, arg := cmd, "" 816 | if i := strings.IndexAny(verb, " \t"); i >= 0 { 817 | verb, arg = verb[:i], strings.TrimSpace(verb[i+1:]) 818 | } 819 | 820 | // Look for specific method. 821 | m := reflect.ValueOf(h).MethodByName("Exec" + verb) 822 | if !m.IsValid() { 823 | // Fall back to general Execute. 824 | return h.Execute(cmd) 825 | } 826 | 827 | // Found method. 828 | // Committed to handling the event. 829 | // All returns below should be return true. 830 | 831 | // Check method signature. 832 | t := m.Type() 833 | switch t.NumOut() { 834 | default: 835 | w.Errf("bad method %s: too many results", cmd) 836 | return true 837 | case 0: 838 | // ok 839 | case 1: 840 | if t.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { 841 | w.Errf("bad method %s: return type %v, not error", cmd, t.Out(0)) 842 | return true 843 | } 844 | } 845 | varg := reflect.ValueOf(arg) 846 | switch t.NumIn() { 847 | default: 848 | w.Errf("bad method %s: too many arguments", cmd) 849 | return true 850 | case 0: 851 | if arg != "" { 852 | w.Errf("%s takes no arguments", cmd) 853 | return true 854 | } 855 | case 1: 856 | if t.In(0) != varg.Type() { 857 | w.Errf("bad method %s: argument type %v, not string", cmd, t.In(0)) 858 | return true 859 | } 860 | } 861 | 862 | args := []reflect.Value{} 863 | if t.NumIn() > 0 { 864 | args = append(args, varg) 865 | } 866 | out := m.Call(args) 867 | var err error 868 | if len(out) == 1 { 869 | err, _ = out[0].Interface().(error) 870 | } 871 | if err != nil { 872 | w.Errf("%v", err) 873 | } 874 | 875 | return true 876 | } 877 | 878 | func (w *Win) Selection() string { 879 | w.Ctl("addr=dot") 880 | data, err := w.ReadAll("xdata") 881 | if err != nil { 882 | w.Err(err.Error()) 883 | } 884 | return string(data) 885 | } 886 | 887 | func (w *Win) SetErrorPrefix(p string) { 888 | w.errorPrefix = p 889 | } 890 | 891 | // Err finds or creates a window appropriate for showing errors related to w 892 | // and then prints msg to that window. 893 | // It adds a final newline to msg if needed. 894 | func (w *Win) Err(msg string) { 895 | Err(w.errorPrefix, msg) 896 | } 897 | 898 | func (w *Win) Errf(format string, args ...interface{}) { 899 | w.Err(fmt.Sprintf(format, args...)) 900 | } 901 | 902 | // Err finds or creates a window appropriate for showing errors related to a window titled src 903 | // and then prints msg to that window. It adds a final newline to msg if needed. 904 | func Err(src, msg string) { 905 | if !strings.HasSuffix(msg, "\n") { 906 | msg = msg + "\n" 907 | } 908 | prefix, _ := path.Split(src) 909 | if prefix == "/" || prefix == "." { 910 | prefix = "" 911 | } 912 | name := prefix + "+Errors" 913 | w1 := Show(name) 914 | if w1 == nil { 915 | var err error 916 | w1, err = New() 917 | if err != nil { 918 | time.Sleep(100 * time.Millisecond) 919 | w1, err = New() 920 | if err != nil { 921 | log.Fatalf("cannot create +Errors window") 922 | } 923 | } 924 | w1.Name("%s", name) 925 | } 926 | w1.Addr("$") 927 | w1.Ctl("dot=addr") 928 | w1.Fprintf("body", "%s", msg) 929 | w1.Addr(".,") 930 | w1.Ctl("dot=addr") 931 | w1.Ctl("show") 932 | } 933 | 934 | // Errf is like Err but accepts a printf-style formatting. 935 | func Errf(src, format string, args ...interface{}) { 936 | Err(src, fmt.Sprintf(format, args...)) 937 | } 938 | -------------------------------------------------------------------------------- /internal/acme/acme_p9p.go: -------------------------------------------------------------------------------- 1 | //go:build !plan9 2 | // +build !plan9 3 | 4 | package acme 5 | 6 | import "github.com/fhs/9fans-go/plan9/client" 7 | 8 | func mountAcme() { 9 | if Network == "" || Address == "" { 10 | panic("network or address not set") 11 | } 12 | fsys, fsysErr = client.Mount(Network, Address) 13 | } 14 | -------------------------------------------------------------------------------- /internal/acme/acme_plan9.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "github.com/fhs/9fans-go/plan9/client" 5 | ) 6 | 7 | func mountAcme() { 8 | // Already mounted at /mnt/acme. 9 | // Ignore Network and Address. 10 | fsys = &client.Fsys{Mtpt: "/mnt/acme"} 11 | fsysErr = nil 12 | } 13 | -------------------------------------------------------------------------------- /internal/acmeutil/acme.go: -------------------------------------------------------------------------------- 1 | // Package acmeutil implements acme utility functions. 2 | package acmeutil 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strconv" 10 | 11 | "9fans.net/acme-lsp/internal/acme" 12 | ) 13 | 14 | type Win struct { 15 | *acme.Win 16 | } 17 | 18 | func NewWin() (*Win, error) { 19 | w, err := acme.New() 20 | if err != nil { 21 | return nil, err 22 | } 23 | return &Win{w}, nil 24 | } 25 | 26 | func OpenWin(id int) (*Win, error) { 27 | w, err := acme.Open(id, nil) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &Win{w}, nil 32 | } 33 | 34 | func OpenCurrentWin() (*Win, error) { 35 | id, err := strconv.Atoi(os.Getenv("winid")) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to parse $winid: %v", err) 38 | } 39 | return OpenWin(id) 40 | } 41 | 42 | func (w *Win) Filename() (string, error) { 43 | tag, err := w.ReadAll("tag") 44 | if err != nil { 45 | return "", err 46 | } 47 | i := bytes.IndexRune(tag, ' ') 48 | if i < 0 { 49 | i = len(tag) 50 | } 51 | return string(tag[:i]), nil 52 | } 53 | 54 | // CurrentAddr returns the address of current selection. 55 | func (w *Win) CurrentAddr() (q0, q1 int, err error) { 56 | _, _, err = w.ReadAddr() // open addr file 57 | if err != nil { 58 | return 0, 0, fmt.Errorf("read addr: %v", err) 59 | } 60 | err = w.Ctl("addr=dot") 61 | if err != nil { 62 | return 0, 0, fmt.Errorf("setting addr=dot: %v", err) 63 | } 64 | return w.ReadAddr() 65 | } 66 | 67 | func (w *Win) FileReadWriter(filename string) io.ReadWriter { 68 | return &winReadWriter{ 69 | w: w.Win, 70 | name: filename, 71 | } 72 | } 73 | 74 | // Reader implements text.File. 75 | func (w *Win) Reader() (io.Reader, error) { 76 | _, err := w.Seek("body", 0, 0) 77 | if err != nil { 78 | return nil, fmt.Errorf("seed failed for window %v: %v", w.ID(), err) 79 | } 80 | return w.FileReadWriter("body"), nil 81 | } 82 | 83 | // WriteAt implements text.File. 84 | func (w *Win) WriteAt(q0, q1 int, b []byte) (int, error) { 85 | err := w.Addr("#%d,#%d", q0, q1) 86 | if err != nil { 87 | return 0, fmt.Errorf("failed to write to addr for winid=%v: %v", w.ID(), err) 88 | } 89 | return w.Write("data", b) 90 | } 91 | 92 | // Mark implements text.File. 93 | func (w *Win) Mark() error { 94 | return w.Ctl("mark") 95 | } 96 | 97 | // DisableMark implements text.File. 98 | func (w *Win) DisableMark() error { 99 | return w.Ctl("nomark") 100 | } 101 | 102 | type winReadWriter struct { 103 | w *acme.Win 104 | name string 105 | } 106 | 107 | func (f *winReadWriter) Read(b []byte) (int, error) { 108 | return f.w.Read(f.name, b) 109 | } 110 | 111 | func (f *winReadWriter) Write(b []byte) (int, error) { 112 | return f.w.Write(f.name, b) 113 | } 114 | 115 | // Hijack returns the first window named name 116 | // found in the set of existing acme windows. 117 | func Hijack(name string) (*Win, error) { 118 | wins, err := acme.Windows() 119 | if err != nil { 120 | return nil, fmt.Errorf("hijack %q: %v", name, err) 121 | } 122 | for _, info := range wins { 123 | if info.Name == name { 124 | return OpenWin(info.ID) 125 | } 126 | } 127 | return nil, fmt.Errorf("hijack %q: window not found", name) 128 | } 129 | -------------------------------------------------------------------------------- /internal/gomod/gomod.go: -------------------------------------------------------------------------------- 1 | // Package gomod implements Go module related functions. 2 | package gomod 3 | 4 | import ( 5 | "encoding/json" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | type env struct { 12 | GOPATH, GOROOT string 13 | } 14 | 15 | func getEnv() (*env, error) { 16 | out, err := exec.Command("go", "env", "-json").Output() 17 | if err != nil { 18 | return nil, err 19 | } 20 | var e env 21 | err = json.Unmarshal(out, &e) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &e, nil 26 | } 27 | 28 | type module struct { 29 | Path string 30 | } 31 | 32 | func getModulePath(dir string) (string, error) { 33 | cmd := exec.Command("go", "list", "-m", "-json") 34 | cmd.Dir = dir 35 | out, err := cmd.Output() 36 | if err != nil { 37 | return "", err 38 | } 39 | var m module 40 | err = json.Unmarshal(out, &m) 41 | if err != nil { 42 | return "", err 43 | } 44 | return m.Path, nil 45 | } 46 | 47 | func isSubdirectory(parent, child string) bool { 48 | p := filepath.Clean(parent) 49 | c := filepath.Clean(child) 50 | return (p == c) || (len(c) > len(p) && strings.HasPrefix(c, p) && c[len(p)] == filepath.Separator) 51 | } 52 | 53 | // RootDir returns the module root directory for filename. 54 | func RootDir(filename string) string { 55 | defaultRoot := "/" // TODO(fhs): windows support? 56 | 57 | e, err := getEnv() 58 | if err != nil { 59 | return defaultRoot 60 | } 61 | dir := filepath.Dir(filename) 62 | if isSubdirectory(e.GOPATH, dir) || isSubdirectory(e.GOROOT, dir) { 63 | return defaultRoot 64 | } 65 | r, err := getModulePath(dir) 66 | if err != nil { 67 | return defaultRoot 68 | } 69 | return r 70 | } 71 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/acmelsp.go: -------------------------------------------------------------------------------- 1 | // Package acmelsp implements the core of acme-lsp commands. 2 | package acmelsp 3 | 4 | import ( 5 | "bufio" 6 | "context" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | 15 | "9fans.net/acme-lsp/internal/acme" 16 | "9fans.net/acme-lsp/internal/acmeutil" 17 | "9fans.net/acme-lsp/internal/lsp" 18 | "9fans.net/acme-lsp/internal/lsp/proxy" 19 | "9fans.net/acme-lsp/internal/lsp/text" 20 | "9fans.net/internal/go-lsp/lsp/protocol" 21 | "github.com/fhs/9fans-go/plan9" 22 | "github.com/fhs/9fans-go/plumb" 23 | ) 24 | 25 | func CurrentWindowRemoteCmd(ss *ServerSet, fm *FileManager) (*RemoteCmd, error) { 26 | id, err := strconv.Atoi(os.Getenv("winid")) 27 | if err != nil { 28 | return nil, fmt.Errorf("failed to parse $winid: %v", err) 29 | } 30 | return WindowRemoteCmd(ss, fm, id) 31 | } 32 | 33 | func WindowRemoteCmd(ss *ServerSet, fm *FileManager, winid int) (*RemoteCmd, error) { 34 | w, err := acmeutil.OpenWin(winid) 35 | if err != nil { 36 | return nil, fmt.Errorf("failed to to open window %v: %v", winid, err) 37 | } 38 | defer w.CloseFiles() 39 | 40 | _, fname, err := text.DocumentURI(w) 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to get text position: %v", err) 43 | } 44 | srv, found, err := ss.StartForFile(fname) 45 | if err != nil { 46 | return nil, fmt.Errorf("cound not start language server: %v", err) 47 | } 48 | if !found { 49 | return nil, fmt.Errorf("no language server for filename %q", fname) 50 | } 51 | 52 | // In case the window has unsaved changes (it's dirty), 53 | // send changes to LSP server. 54 | if err = fm.didChange(winid, fname); err != nil { 55 | return nil, fmt.Errorf("DidChange failed: %v", err) 56 | } 57 | 58 | return NewRemoteCmd(srv.Client, winid), nil 59 | } 60 | 61 | func getLine(p string, l int) string { 62 | file, err := os.Open(p) 63 | if err != nil { 64 | fmt.Println(err) 65 | return "" 66 | } 67 | defer file.Close() 68 | 69 | scanner := bufio.NewScanner(file) 70 | currentLine := 0 71 | for scanner.Scan() { 72 | currentLine++ 73 | if currentLine == l { 74 | return scanner.Text() 75 | } 76 | } 77 | 78 | return "" 79 | } 80 | 81 | func PrintLocations(w io.Writer, loc []protocol.Location) error { 82 | wd, err := os.Getwd() 83 | if err != nil { 84 | wd = "" 85 | } 86 | sort.Slice(loc, func(i, j int) bool { 87 | a := loc[i] 88 | b := loc[j] 89 | n := strings.Compare(string(a.URI), string(b.URI)) 90 | if n == 0 { 91 | m := a.Range.Start.Line - b.Range.Start.Line 92 | if m == 0 { 93 | return a.Range.Start.Character < b.Range.Start.Character 94 | } 95 | return m < 0 96 | } 97 | return n < 0 98 | }) 99 | for _, l := range loc { 100 | fmt.Fprintf(w, "%v:%s\n", lsp.LocationLink(&l, wd), getLine(text.ToPath(l.URI), int(l.Range.Start.Line+1))) 101 | } 102 | return nil 103 | } 104 | 105 | // PlumbLocations sends the locations to the plumber. 106 | func PlumbLocations(locations []protocol.Location) error { 107 | p, err := plumb.Open("send", plan9.OWRITE) 108 | if err != nil { 109 | return fmt.Errorf("failed to open plumber: %v", err) 110 | } 111 | defer p.Close() 112 | for _, loc := range locations { 113 | err := plumbLocation(&loc).Send(p) 114 | if err != nil { 115 | return fmt.Errorf("failed to plumb location: %v", err) 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | func plumbLocation(loc *protocol.Location) *plumb.Message { 122 | // LSP uses zero-based offsets. 123 | // Place the cursor *before* the location range. 124 | pos := loc.Range.Start 125 | attr := &plumb.Attribute{ 126 | Name: "addr", 127 | Value: fmt.Sprintf("%v-#0+#%v", pos.Line+1, pos.Character), 128 | } 129 | return &plumb.Message{ 130 | Src: "acme-lsp", 131 | Dst: "edit", 132 | Dir: "/", 133 | Type: "text", 134 | Attr: attr, 135 | Data: []byte(text.ToPath(loc.URI)), 136 | } 137 | } 138 | 139 | type FormatServer interface { 140 | InitializeResult(context.Context, *protocol.TextDocumentIdentifier) (*protocol.InitializeResult, error) 141 | DidChange(context.Context, *protocol.DidChangeTextDocumentParams) error 142 | Formatting(context.Context, *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) 143 | CodeAction(context.Context, *protocol.CodeActionParams) ([]protocol.CodeAction, error) 144 | ExecuteCommandOnDocument(context.Context, *proxy.ExecuteCommandOnDocumentParams) (interface{}, error) 145 | } 146 | 147 | // CodeActionAndFormat runs the given code actions and then formats the file f. 148 | func CodeActionAndFormat(ctx context.Context, server FormatServer, doc *protocol.TextDocumentIdentifier, f text.File, actions []protocol.CodeActionKind) error { 149 | initres, err := server.InitializeResult(ctx, doc) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | actions = lsp.CompatibleCodeActions(&initres.Capabilities, actions) 155 | if len(actions) > 0 { 156 | actions, err := server.CodeAction(ctx, &protocol.CodeActionParams{ 157 | TextDocument: *doc, 158 | Range: protocol.Range{}, 159 | Context: protocol.CodeActionContext{ 160 | Diagnostics: nil, 161 | Only: actions, 162 | }, 163 | }) 164 | if err != nil { 165 | return err 166 | } 167 | for _, a := range actions { 168 | if a.Edit != nil { 169 | err := editWorkspace(a.Edit) 170 | if err != nil { 171 | return err 172 | } 173 | } 174 | if a.Command != nil { 175 | _, err := server.ExecuteCommandOnDocument(ctx, &proxy.ExecuteCommandOnDocumentParams{ 176 | TextDocument: *doc, 177 | ExecuteCommandParams: protocol.ExecuteCommandParams{ 178 | Command: a.Command.Command, 179 | Arguments: a.Command.Arguments, 180 | }, 181 | }) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | } 187 | if len(actions) > 0 { 188 | // Our file may or may not be among the workspace edits for import fixes. 189 | // We assume it is among the edits. 190 | // TODO(fhs): Skip if our file didn't have import changes. 191 | rd, err := f.Reader() 192 | if err != nil { 193 | return err 194 | } 195 | b, err := ioutil.ReadAll(rd) 196 | if err != nil { 197 | return err 198 | } 199 | server.DidChange(ctx, &protocol.DidChangeTextDocumentParams{ 200 | TextDocument: protocol.VersionedTextDocumentIdentifier{ 201 | TextDocumentIdentifier: *doc, 202 | }, 203 | ContentChanges: []protocol.TextDocumentContentChangeEvent{ 204 | { 205 | Text: string(b), 206 | }, 207 | }, 208 | }) 209 | if err != nil { 210 | return err 211 | } 212 | } 213 | } 214 | edits, err := server.Formatting(ctx, &protocol.DocumentFormattingParams{ 215 | TextDocument: *doc, 216 | }) 217 | if err != nil { 218 | return err 219 | } 220 | if err := text.Edit(f, edits); err != nil { 221 | return fmt.Errorf("failed to apply edits: %v", err) 222 | } 223 | return nil 224 | } 225 | 226 | func editWorkspace(we *protocol.WorkspaceEdit) error { 227 | if we == nil { 228 | return nil // no changes to apply 229 | } 230 | if we.Changes == nil && we.DocumentChanges != nil { 231 | // gopls version >= 0.3.1 sends versioned document edits 232 | // for organizeImports code action even when we don't 233 | // support it. Convert versioned edits to non-versioned. 234 | changes := make(map[protocol.DocumentURI][]protocol.TextEdit) 235 | for _, dc := range we.DocumentChanges { 236 | if tde := dc.TextDocumentEdit; tde != nil { 237 | changes[tde.TextDocument.TextDocumentIdentifier.URI] = tde.Edits 238 | } 239 | } 240 | we.Changes = changes 241 | } 242 | if we.Changes == nil { 243 | return nil // no changes to apply 244 | } 245 | 246 | wins, err := acme.Windows() 247 | if err != nil { 248 | return fmt.Errorf("failed to read list of acme index: %v", err) 249 | } 250 | winid := make(map[string]int, len(wins)) 251 | for _, info := range wins { 252 | winid[info.Name] = info.ID 253 | } 254 | 255 | for uri := range we.Changes { 256 | fname := text.ToPath(uri) 257 | if _, ok := winid[fname]; !ok { 258 | return fmt.Errorf("%v: not open in acme", fname) 259 | } 260 | } 261 | for uri, edits := range we.Changes { 262 | fname := text.ToPath(uri) 263 | id := winid[fname] 264 | w, err := acmeutil.OpenWin(id) 265 | if err != nil { 266 | return fmt.Errorf("failed to open window %v: %v", id, err) 267 | } 268 | if err := text.Edit(w, edits); err != nil { 269 | return fmt.Errorf("failed to apply edits to window %v: %v", id, err) 270 | } 271 | w.CloseFiles() 272 | } 273 | return nil 274 | } 275 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/acmelsp_test.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "reflect" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | 11 | "9fans.net/acme-lsp/internal/lsp" 12 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 13 | "9fans.net/internal/go-lsp/lsp/protocol" 14 | "github.com/fhs/9fans-go/plumb" 15 | "github.com/google/go-cmp/cmp" 16 | ) 17 | 18 | func TestPlumbLocation(t *testing.T) { 19 | loc := protocol.Location{ 20 | URI: "file:///home/gopher/hello/main.go", 21 | Range: protocol.Range{ 22 | Start: protocol.Position{ 23 | Line: 100, 24 | Character: 25, 25 | }, 26 | End: protocol.Position{}, 27 | }, 28 | } 29 | want := plumb.Message{ 30 | Src: "acme-lsp", 31 | Dst: "edit", 32 | Dir: "/", 33 | Type: "text", 34 | Attr: &plumb.Attribute{ 35 | Name: "addr", 36 | Value: "101-#0+#25", 37 | }, 38 | Data: []byte("/home/gopher/hello/main.go"), 39 | } 40 | got := plumbLocation(&loc) 41 | if reflect.DeepEqual(got, want) { 42 | t.Errorf("plumbLocation(%v) returned %v; exptected %v", loc, got, want) 43 | } 44 | } 45 | 46 | func TestParseFlagSet(t *testing.T) { 47 | tt := []struct { 48 | name string 49 | args []string 50 | debug bool 51 | serverInfo []*ServerInfo 52 | workspaces []string 53 | err string 54 | }{ 55 | {"Debug", []string{"-debug"}, true, nil, nil, ""}, 56 | { 57 | "OneWorkspace", 58 | []string{"-workspaces", "/path/to/mod1"}, 59 | false, 60 | nil, 61 | []string{"/path/to/mod1"}, 62 | "", 63 | }, 64 | { 65 | "TwoWorkspaces", 66 | []string{"-workspaces", "/go/mod1:/go/mod2"}, 67 | false, 68 | nil, 69 | []string{"/go/mod1", "/go/mod2"}, 70 | "", 71 | }, 72 | { 73 | "ServerFlag", 74 | []string{"-server", `\.go$:gopls -rpc.trace`}, 75 | false, 76 | []*ServerInfo{ 77 | { 78 | Server: &config.Server{ 79 | Command: []string{"gopls", "-rpc.trace"}, 80 | }, 81 | Re: regexp.MustCompile(`\.go$`), 82 | }, 83 | }, 84 | nil, 85 | "", 86 | }, 87 | { 88 | "DialFlag", 89 | []string{"-dial", `\.go$:localhost:4389`}, 90 | false, 91 | []*ServerInfo{ 92 | { 93 | Server: &config.Server{ 94 | Address: "localhost:4389", 95 | }, 96 | Re: regexp.MustCompile(`\.go$`), 97 | }, 98 | }, 99 | nil, 100 | "", 101 | }, 102 | { 103 | "BadServerFlag", 104 | []string{"-server", `gopls -rpc.trace`}, 105 | false, 106 | nil, 107 | nil, 108 | "flag value must contain a colon", 109 | }, 110 | } 111 | 112 | for _, tc := range tt { 113 | t.Run(tc.name, func(t *testing.T) { 114 | f := flag.NewFlagSet("acme-lsp", flag.ContinueOnError) 115 | f.SetOutput(ioutil.Discard) 116 | 117 | cfg := config.Default() 118 | err := cfg.ParseFlags(config.LangServerFlags, f, tc.args) 119 | if len(tc.err) > 0 { 120 | if !strings.Contains(err.Error(), tc.err) { 121 | t.Fatalf("for %q, error %q does not contain %q", tc.args, err, tc.err) 122 | } 123 | return 124 | } 125 | if err != nil { 126 | t.Fatalf("failed to parse flags: %v", err) 127 | } 128 | ss, err := NewServerSet(cfg, NewDiagnosticsWriter()) 129 | if err != nil { 130 | t.Fatalf("ParseFlagSet failed: %v", err) 131 | } 132 | if cfg.Verbose != tc.debug { 133 | t.Errorf("-debug flag didn't turn on debugging") 134 | } 135 | got := ss.Workspaces() 136 | want, err := lsp.DirsToWorkspaceFolders(tc.workspaces) 137 | if err != nil { 138 | t.Fatalf("DirsToWorkspaceFolders failed: %v", err) 139 | } 140 | if !cmp.Equal(got, want) { 141 | t.Errorf("workspaces are %v; want %v", got, want) 142 | } 143 | if len(tc.serverInfo) > 0 { 144 | if got, want := len(ss.Data), len(tc.serverInfo); got != want { 145 | t.Fatalf("%v servers registered for %v; want %v", got, tc.args, want) 146 | } 147 | got := tc.serverInfo[0] 148 | want := ss.Data[0] 149 | if got, want := got.Re.String(), want.Re.String(); got != want { 150 | t.Errorf("filename pattern for %v is %v; want %v", got, tc.args, want) 151 | } 152 | if got, want := got.Command, want.Command; !cmp.Equal(got, want) { 153 | t.Errorf("lsp server args for %v is %v; want %v", tc.args, got, want) 154 | } 155 | if got, want := got.Address, want.Address; !cmp.Equal(got, want) { 156 | t.Errorf("lsp server dial address for %v is %v; want %v", tc.args, got, want) 157 | } 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/assist.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "time" 10 | "unicode" 11 | 12 | "9fans.net/acme-lsp/internal/acme" 13 | "9fans.net/acme-lsp/internal/acmeutil" 14 | "9fans.net/acme-lsp/internal/lsp/proxy" 15 | "9fans.net/acme-lsp/internal/lsp/text" 16 | "9fans.net/internal/go-lsp/lsp/protocol" 17 | ) 18 | 19 | func watchLog(ch chan<- *acme.LogEvent) { 20 | alog, err := acme.Log() 21 | if err != nil { 22 | panic(err) 23 | } 24 | defer alog.Close() 25 | for { 26 | ev, err := alog.Read() 27 | if err != nil { 28 | panic(err) 29 | } 30 | ch <- &ev 31 | } 32 | } 33 | 34 | // focusWindow represents the last focused window. 35 | // 36 | // Note that we can't cache the *acmeutil.Win for the window 37 | // because having the ctl file open prevents del event from 38 | // being delivered to acme/log file. 39 | type focusWin struct { 40 | id int 41 | q0 int 42 | name string 43 | } 44 | 45 | func newFocusWin() *focusWin { 46 | var fw focusWin 47 | fw.Reset() 48 | return &fw 49 | } 50 | 51 | func (fw *focusWin) Reset() { 52 | fw.id = -1 53 | fw.q0 = -1 54 | fw.name = "" 55 | } 56 | 57 | func (fw *focusWin) SetQ0() bool { 58 | if fw.id < 0 { 59 | return false 60 | } 61 | w, err := acmeutil.OpenWin(fw.id) 62 | if err != nil { 63 | return false 64 | } 65 | defer w.CloseFiles() 66 | 67 | q0, _, err := w.CurrentAddr() 68 | if err != nil { 69 | return false 70 | } 71 | fw.q0 = q0 72 | return true 73 | } 74 | 75 | func notifyPosChange(sm ServerMatcher, ch chan<- *focusWin) { 76 | fw := newFocusWin() 77 | logch := make(chan *acme.LogEvent) 78 | go watchLog(logch) 79 | ticker := time.NewTicker(1 * time.Second) 80 | defer ticker.Stop() 81 | 82 | pos := make(map[int]int) // winid -> q0 83 | 84 | for { 85 | select { 86 | case ev := <-logch: 87 | _, found, err := sm.ServerMatch(context.Background(), ev.Name) 88 | if found && err == nil && ev.Op == "focus" { 89 | // TODO(fhs): we should really make use of context 90 | // and cancel outstanding rpc requests on previously focused window. 91 | fw.id = ev.ID 92 | fw.name = ev.Name 93 | } else { 94 | fw.Reset() 95 | } 96 | 97 | case <-ticker.C: 98 | if fw.SetQ0() && pos[fw.id] != fw.q0 { 99 | pos[fw.id] = fw.q0 100 | ch <- &focusWin{ // send a copy 101 | id: fw.id, 102 | q0: fw.q0, 103 | name: fw.name, 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | type outputWin struct { 111 | *acmeutil.Win 112 | body io.Writer 113 | event <-chan *acme.Event 114 | sm ServerMatcher 115 | } 116 | 117 | func newOutputWin(sm ServerMatcher, name string) (*outputWin, error) { 118 | w, err := acmeutil.NewWin() 119 | if err != nil { 120 | return nil, err 121 | } 122 | w.Name(name) 123 | return &outputWin{ 124 | Win: w, 125 | body: w.FileReadWriter("body"), 126 | event: w.EventChan(), 127 | sm: sm, 128 | }, nil 129 | } 130 | 131 | func (w *outputWin) Close() { 132 | w.Del(true) 133 | w.CloseFiles() 134 | } 135 | 136 | func readLeftRight(id int, q0 int) (left, right rune, err error) { 137 | w, err := acmeutil.OpenWin(id) 138 | if err != nil { 139 | return 0, 0, err 140 | } 141 | defer w.CloseFiles() 142 | 143 | err = w.Addr("#%v,#%v", q0-1, q0+1) 144 | if err != nil { 145 | return 0, 0, err 146 | } 147 | 148 | b, err := ioutil.ReadAll(w.FileReadWriter("xdata")) 149 | if err != nil { 150 | return 0, 0, err 151 | } 152 | r := []rune(string(b)) 153 | if len(r) != 2 { 154 | // TODO(fhs): deal with EOF and beginning of file 155 | return 0, 0, fmt.Errorf("could not find rune left and right of cursor") 156 | } 157 | return r[0], r[1], nil 158 | } 159 | 160 | func winPosition(id int) (*protocol.TextDocumentPositionParams, string, error) { 161 | w, err := acmeutil.OpenWin(id) 162 | if err != nil { 163 | return nil, "", err 164 | } 165 | defer w.CloseFiles() 166 | 167 | return text.Position(w) 168 | } 169 | 170 | func isIdentifier(r rune) bool { 171 | return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' 172 | } 173 | 174 | func helpType(left, right rune) string { 175 | if unicode.IsSpace(left) && unicode.IsSpace(right) { 176 | return "" 177 | } 178 | if isIdentifier(left) && isIdentifier(right) { 179 | return "hov" 180 | } 181 | switch left { 182 | case '(', '[', '<', '{': 183 | return "sig" 184 | } 185 | return "comp" 186 | } 187 | 188 | func dprintf(format string, args ...interface{}) { 189 | if Verbose { 190 | log.Printf(format, args...) 191 | } 192 | } 193 | 194 | // Update writes result of cmd to output window. 195 | func (w *outputWin) Update(fw *focusWin, server proxy.Server, cmd string) { 196 | if cmd == "auto" { 197 | left, right, err := readLeftRight(fw.id, fw.q0) 198 | if err != nil { 199 | dprintf("read left/right rune: %v\n", err) 200 | return 201 | } 202 | cmd = helpType(left, right) 203 | if cmd == "" { 204 | return 205 | } 206 | } 207 | 208 | rc := NewRemoteCmd(server, fw.id) 209 | rc.Stdout = w.body 210 | rc.Stderr = w.body 211 | ctx := context.Background() 212 | 213 | // Assume file is already opened by file management. 214 | err := rc.DidChange(ctx) 215 | if err != nil { 216 | dprintf("DidChange failed: %v\n", err) 217 | return 218 | } 219 | 220 | w.Clear() 221 | switch cmd { 222 | case "comp": 223 | err := rc.Completion(ctx, CompleteNoEdit) 224 | if err != nil { 225 | dprintf("Completion failed: %v\n", err) 226 | } 227 | 228 | case "sig": 229 | err = rc.SignatureHelp(ctx) 230 | if err != nil { 231 | dprintf("SignatureHelp failed: %v\n", err) 232 | } 233 | case "hov": 234 | err = rc.Hover(ctx) 235 | if err != nil { 236 | dprintf("Hover failed: %v\n", err) 237 | } 238 | default: 239 | log.Fatalf("invalid command %q\n", cmd) 240 | } 241 | w.Ctl("clean") 242 | } 243 | 244 | // Assist creates an acme window where output of cmd is written after each 245 | // cursor position change in acme. Cmd is either "comp", "sig", "hov", or "auto" 246 | // for completion, signature help, hover, or auto-detection of the former three. 247 | func Assist(sm ServerMatcher, cmd string) error { 248 | name := "/LSP/assist" 249 | if cmd != "auto" { 250 | name += "/" + cmd 251 | } 252 | w, err := newOutputWin(sm, name) 253 | if err != nil { 254 | return fmt.Errorf("failed to create acme window: %v", err) 255 | } 256 | defer w.Close() 257 | 258 | fch := make(chan *focusWin) 259 | go notifyPosChange(sm, fch) 260 | 261 | loop: 262 | for { 263 | select { 264 | case fw := <-fch: 265 | server, found, err := sm.ServerMatch(context.Background(), fw.name) 266 | if err != nil { 267 | log.Printf("failed to start language server: %v\n", err) 268 | } 269 | if found { 270 | w.Update(fw, server, cmd) 271 | } 272 | 273 | case ev := <-w.event: 274 | if ev == nil { 275 | break loop 276 | } 277 | switch ev.C2 { 278 | case 'x', 'X': // execute 279 | if string(ev.Text) == "Del" { 280 | break loop 281 | } 282 | } 283 | w.WriteEvent(ev) 284 | } 285 | } 286 | return nil 287 | } 288 | 289 | // ServerMatcher represents a set of servers where it's possible to 290 | // find a matching server based on filename. 291 | type ServerMatcher interface { 292 | ServerMatch(ctx context.Context, filename string) (proxy.Server, bool, error) 293 | } 294 | 295 | // UnitServerMatcher implements ServerMatcher using only one server. 296 | type UnitServerMatcher struct { 297 | proxy.Server 298 | } 299 | 300 | func (sm *UnitServerMatcher) ServerMatch(ctx context.Context, filename string) (proxy.Server, bool, error) { 301 | _, err := sm.Server.InitializeResult(ctx, &protocol.TextDocumentIdentifier{ 302 | URI: text.ToURI(filename), 303 | }) 304 | if err != nil { 305 | return nil, false, err 306 | } 307 | return sm.Server, true, nil 308 | } 309 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/client.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "path/filepath" 9 | "sync" 10 | 11 | "9fans.net/internal/go-lsp/lsp/protocol" 12 | "github.com/sourcegraph/jsonrpc2" 13 | 14 | "9fans.net/acme-lsp/internal/lsp" 15 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 16 | "9fans.net/acme-lsp/internal/lsp/proxy" 17 | "9fans.net/acme-lsp/internal/lsp/text" 18 | ) 19 | 20 | var Verbose = false 21 | 22 | type DiagnosticsWriter interface { 23 | WriteDiagnostics(params *protocol.PublishDiagnosticsParams) 24 | } 25 | 26 | // clientHandler handles JSON-RPC requests and notifications. 27 | type clientHandler struct { 28 | cfg *ClientConfig 29 | hideDiag bool 30 | diagWriter DiagnosticsWriter 31 | diag map[protocol.DocumentURI][]protocol.Diagnostic 32 | mu sync.Mutex 33 | proxy.NotImplementedClient 34 | } 35 | 36 | func (h *clientHandler) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error { 37 | log.Printf("LSP %v: %v\n", params.Type, params.Message) 38 | return nil 39 | } 40 | 41 | func (h *clientHandler) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { 42 | if h.cfg.Logger != nil { 43 | h.cfg.Logger.Printf("%v: %v\n", params.Type, params.Message) 44 | return nil 45 | } 46 | if params.Type == protocol.Error || params.Type == protocol.Warning || Verbose { 47 | log.Printf("log: LSP %v: %v\n", params.Type, params.Message) 48 | } 49 | return nil 50 | } 51 | 52 | func (h *clientHandler) Event(context.Context, *interface{}) error { 53 | return nil 54 | } 55 | 56 | func (h *clientHandler) PublishDiagnostics(ctx context.Context, params *protocol.PublishDiagnosticsParams) error { 57 | if h.hideDiag { 58 | return nil 59 | } 60 | 61 | h.diagWriter.WriteDiagnostics(params) 62 | return nil 63 | } 64 | 65 | func (h *clientHandler) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) { 66 | return nil, nil 67 | } 68 | 69 | func (h *clientHandler) Configuration(context.Context, *protocol.ParamConfiguration) ([]interface{}, error) { 70 | return nil, nil 71 | } 72 | 73 | func (h *clientHandler) RegisterCapability(context.Context, *protocol.RegistrationParams) error { 74 | return nil 75 | } 76 | 77 | func (h *clientHandler) UnregisterCapability(context.Context, *protocol.UnregistrationParams) error { 78 | return nil 79 | } 80 | 81 | func (h *clientHandler) ShowMessageRequest(context.Context, *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { 82 | return nil, nil 83 | } 84 | 85 | func (h *clientHandler) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { 86 | err := editWorkspace(¶ms.Edit) 87 | if err != nil { 88 | return &protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil 89 | } 90 | return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil 91 | } 92 | 93 | // ClientConfig contains LSP client configuration values. 94 | type ClientConfig struct { 95 | *config.Server 96 | *config.FilenameHandler 97 | RootDirectory string // used to compute RootURI in initialization 98 | HideDiag bool // don't write diagnostics to DiagWriter 99 | RPCTrace bool // print LSP rpc trace to stderr 100 | DiagWriter DiagnosticsWriter // notification handler writes diagnostics here 101 | Workspaces []protocol.WorkspaceFolder // initial workspace folders 102 | Logger *log.Logger 103 | } 104 | 105 | // Client represents a LSP client connection. 106 | type Client struct { 107 | protocol.Server 108 | initializeResult *protocol.InitializeResult 109 | cfg *ClientConfig 110 | rpc *jsonrpc2.Conn 111 | } 112 | 113 | func NewClient(conn net.Conn, cfg *ClientConfig) (*Client, error) { 114 | c := &Client{cfg: cfg} 115 | if err := c.init(conn, cfg); err != nil { 116 | return nil, err 117 | } 118 | return c, nil 119 | } 120 | 121 | func (c *Client) Close() error { 122 | return c.rpc.Close() 123 | } 124 | 125 | func (c *Client) init(conn net.Conn, cfg *ClientConfig) error { 126 | ctx := context.Background() 127 | stream := jsonrpc2.NewBufferedStream(conn, jsonrpc2.VSCodeObjectCodec{}) 128 | handler := proxy.NewClientHandler(&clientHandler{ 129 | cfg: cfg, 130 | hideDiag: cfg.HideDiag, 131 | diagWriter: cfg.DiagWriter, 132 | diag: make(map[protocol.DocumentURI][]protocol.Diagnostic), 133 | }) 134 | var opts []jsonrpc2.ConnOpt 135 | if cfg.RPCTrace { 136 | opts = append(opts, lsp.LogMessages(log.Default())) 137 | } 138 | if c.rpc != nil { 139 | c.rpc.Close() 140 | } 141 | c.rpc = jsonrpc2.NewConn(ctx, stream, handler, opts...) 142 | server := protocol.NewServer(c.rpc) 143 | go func() { 144 | <-c.rpc.DisconnectNotify() 145 | log.Printf("jsonrpc2 client connection to LSP sever disconnected\n") 146 | }() 147 | 148 | d, err := filepath.Abs(cfg.RootDirectory) 149 | if err != nil { 150 | return err 151 | } 152 | params := &protocol.ParamInitialize{ 153 | XInitializeParams: protocol.XInitializeParams{ 154 | RootURI: text.ToURI(d), 155 | Capabilities: protocol.ClientCapabilities{ 156 | TextDocument: protocol.TextDocumentClientCapabilities{ 157 | CodeAction: protocol.CodeActionClientCapabilities{ 158 | CodeActionLiteralSupport: protocol.PCodeActionLiteralSupportPCodeAction{ 159 | CodeActionKind: protocol.FCodeActionKindPCodeActionLiteralSupport{ 160 | ValueSet: []protocol.CodeActionKind{ 161 | protocol.SourceOrganizeImports, 162 | }, 163 | }, 164 | }, 165 | }, 166 | DocumentSymbol: protocol.DocumentSymbolClientCapabilities{ 167 | HierarchicalDocumentSymbolSupport: true, 168 | }, 169 | Completion: protocol.CompletionClientCapabilities{ 170 | CompletionItem: protocol.PCompletionItemPCompletion{ 171 | TagSupport: protocol.FTagSupportPCompletionItem{ 172 | ValueSet: []protocol.CompletionItemTag{}, 173 | }, 174 | }, 175 | }, 176 | SemanticTokens: protocol.SemanticTokensClientCapabilities{ 177 | Formats: []protocol.TokenFormat{}, 178 | TokenModifiers: []string{}, 179 | TokenTypes: []string{}, 180 | }, 181 | }, 182 | Workspace: protocol.WorkspaceClientCapabilities{ 183 | WorkspaceFolders: true, 184 | ApplyEdit: true, 185 | }, 186 | }, 187 | InitializationOptions: cfg.Options, 188 | }, 189 | WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{ 190 | WorkspaceFolders: cfg.Workspaces, 191 | }, 192 | } 193 | 194 | result, err := server.Initialize(ctx, params) 195 | if err != nil { 196 | return fmt.Errorf("initialize failed: %v", err) 197 | } 198 | if err := server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 199 | return fmt.Errorf("initialized failed: %v", err) 200 | } 201 | c.Server = server 202 | c.initializeResult = result 203 | return nil 204 | } 205 | 206 | // InitializeResult implements proxy.Server. 207 | func (c *Client) InitializeResult(context.Context, *protocol.TextDocumentIdentifier) (*protocol.InitializeResult, error) { 208 | return c.initializeResult, nil 209 | } 210 | 211 | // Version exists only to implement proxy.Server. 212 | func (c *Client) Version(context.Context) (int, error) { 213 | panic("intentionally not implemented") 214 | } 215 | 216 | // WorkspaceFolders exists only to implement proxy.Server. 217 | func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) { 218 | panic("intentionally not implemented") 219 | } 220 | 221 | // ExecuteCommandOnDocument implements proxy.Server. 222 | func (s *Client) ExecuteCommandOnDocument(ctx context.Context, params *proxy.ExecuteCommandOnDocumentParams) (interface{}, error) { 223 | return s.Server.ExecuteCommand(ctx, ¶ms.ExecuteCommandParams) 224 | } 225 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/client_test.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "regexp" 12 | "runtime" 13 | "strings" 14 | "testing" 15 | 16 | "9fans.net/acme-lsp/internal/lsp" 17 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 18 | "9fans.net/acme-lsp/internal/lsp/text" 19 | "9fans.net/internal/go-lsp/lsp/protocol" 20 | ) 21 | 22 | const goSource = `package main // import "example.com/test" 23 | 24 | import "fmt" 25 | 26 | func main() { 27 | fmt.Println("Hello, 世界") 28 | } 29 | ` 30 | 31 | const goSourceUnfmt = `package main // import "example.com/test" 32 | 33 | import "fmt" 34 | 35 | func main( ){ 36 | fmt . Println ( "Hello, 世界" ) 37 | } 38 | ` 39 | 40 | const goMod = `module 9fans.net/acme-lsp/internal/lsp/acmelsp/client_test 41 | ` 42 | 43 | func testGoModule(t *testing.T, server string, src string, f func(t *testing.T, c *Client, uri protocol.DocumentURI)) { 44 | serverArgs := map[string][]string{ 45 | "gopls": {"gopls"}, 46 | } 47 | 48 | // Create the module 49 | dir, err := ioutil.TempDir("", "examplemod") 50 | if err != nil { 51 | t.Fatalf("TempDir failed: %v", err) 52 | } 53 | defer os.RemoveAll(dir) 54 | 55 | gofile := filepath.Join(dir, "main.go") 56 | if err := ioutil.WriteFile(gofile, []byte(src), 0644); err != nil { 57 | t.Fatalf("WriteFile failed: %v", err) 58 | } 59 | modfile := filepath.Join(dir, "go.mod") 60 | if err := ioutil.WriteFile(modfile, []byte(goMod), 0644); err != nil { 61 | t.Fatalf("WriteFile failed: %v", err) 62 | } 63 | 64 | // Start the server 65 | args, ok := serverArgs[server] 66 | if !ok { 67 | t.Fatalf("unknown server %q", server) 68 | } 69 | cs := &config.Server{ 70 | Command: args, 71 | } 72 | srv, err := execServer(cs, &ClientConfig{ 73 | Server: &config.Server{}, 74 | RootDirectory: dir, 75 | DiagWriter: &mockDiagosticsWriter{ioutil.Discard}, 76 | Workspaces: nil, 77 | }, false) 78 | if err != nil { 79 | t.Fatalf("startServer failed: %v", err) 80 | } 81 | defer srv.Close() 82 | 83 | ctx := context.Background() 84 | err = lsp.DidOpen(ctx, srv.Client, gofile, "go", []byte(src)) 85 | if err != nil { 86 | t.Fatalf("DidOpen failed: %v", err) 87 | } 88 | defer func() { 89 | err := lsp.DidClose(ctx, srv.Client, gofile) 90 | if err != nil { 91 | t.Fatalf("DidClose failed: %v", err) 92 | } 93 | srv.Client.Close() 94 | }() 95 | 96 | t.Run(server, func(t *testing.T) { 97 | f(t, srv.Client, text.ToURI(gofile)) 98 | }) 99 | } 100 | 101 | func TestGoFormat(t *testing.T) { 102 | ctx := context.Background() 103 | 104 | for _, server := range []string{ 105 | "gopls", 106 | } { 107 | testGoModule(t, server, goSourceUnfmt, func(t *testing.T, c *Client, uri protocol.DocumentURI) { 108 | edits, err := c.Formatting(ctx, &protocol.DocumentFormattingParams{ 109 | TextDocument: protocol.TextDocumentIdentifier{ 110 | URI: uri, 111 | }, 112 | }) 113 | if err != nil { 114 | t.Fatalf("Format failed: %v", err) 115 | } 116 | f := BytesFile([]byte(goSourceUnfmt)) 117 | err = text.Edit(&f, edits) 118 | if err != nil { 119 | t.Fatalf("failed to apply edits: %v", err) 120 | } 121 | if got := string(f); got != goSource { 122 | t.Errorf("bad format output:\n%s\nexpected:\n%s", got, goSource) 123 | } 124 | }) 125 | } 126 | } 127 | 128 | func TestGoHover(t *testing.T) { 129 | ctx := context.Background() 130 | 131 | for _, srv := range []struct { 132 | name string 133 | want string 134 | }{ 135 | {"gopls", "```go\nfunc fmt.Println(a ...any) (n int, err error)\n```\n\nPrintln formats using the default formats for its operands and writes to standard output. Spaces are always added between operands and a newline is appended. It returns the number of bytes written and any write error encountered.\n\n\n[`fmt.Println` on pkg.go.dev](https://pkg.go.dev/fmt#Println)"}, 136 | } { 137 | testGoModule(t, srv.name, goSource, func(t *testing.T, c *Client, uri protocol.DocumentURI) { 138 | pos := &protocol.TextDocumentPositionParams{ 139 | TextDocument: protocol.TextDocumentIdentifier{ 140 | URI: uri, 141 | }, 142 | Position: protocol.Position{ 143 | Line: 5, 144 | Character: 10, 145 | }, 146 | } 147 | hov, err := c.Hover(ctx, &protocol.HoverParams{ 148 | TextDocumentPositionParams: *pos, 149 | }) 150 | if err != nil { 151 | t.Fatalf("Hover failed: %v", err) 152 | } 153 | got := hov.Contents.Value 154 | // Instead of doing an exact match, we ignore extra markups 155 | // from markdown (if there are any). 156 | if !strings.Contains(got, srv.want) { 157 | t.Errorf("hover result is %q; expected %q", got, srv.want) 158 | } 159 | }) 160 | } 161 | } 162 | 163 | func TestGoDefinition(t *testing.T) { 164 | src := `package main // import "example.com/test" 165 | 166 | import "fmt" 167 | 168 | func hello() string { return "Hello" } 169 | 170 | func main() { 171 | fmt.Printf("%v\n", hello()) 172 | } 173 | ` 174 | 175 | for _, srv := range []string{ 176 | "gopls", 177 | } { 178 | testGoModule(t, srv, src, func(t *testing.T, c *Client, uri protocol.DocumentURI) { 179 | params := &protocol.DefinitionParams{ 180 | TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 181 | TextDocument: protocol.TextDocumentIdentifier{ 182 | URI: uri, 183 | }, 184 | Position: protocol.Position{ 185 | Line: 7, 186 | Character: 22, 187 | }, 188 | }, 189 | } 190 | got, err := c.Definition(context.Background(), params) 191 | if err != nil { 192 | t.Fatalf("Definition failed: %v", err) 193 | } 194 | want := []protocol.Location{ 195 | { 196 | URI: uri, 197 | Range: protocol.Range{ 198 | Start: protocol.Position{ 199 | Line: 4, 200 | Character: 5, 201 | }, 202 | End: protocol.Position{ 203 | Line: 4, 204 | Character: 10, 205 | }, 206 | }, 207 | }, 208 | } 209 | if !reflect.DeepEqual(want, got) { 210 | t.Errorf("definition result is %v; expected %v", got, want) 211 | } 212 | }) 213 | } 214 | } 215 | 216 | func TestGoTypeDefinition(t *testing.T) { 217 | ctx := context.Background() 218 | src := `package main // import "example.com/test" 219 | 220 | import "fmt" 221 | 222 | type T string 223 | 224 | func main() { 225 | foo := T("hello") 226 | fmt.Printf("%v\n", foo) 227 | } 228 | ` 229 | 230 | for _, srv := range []string{ 231 | "gopls", 232 | } { 233 | testGoModule(t, srv, src, func(t *testing.T, c *Client, uri protocol.DocumentURI) { 234 | pos := &protocol.TextDocumentPositionParams{ 235 | TextDocument: protocol.TextDocumentIdentifier{ 236 | URI: uri, 237 | }, 238 | Position: protocol.Position{ 239 | Line: 7, 240 | Character: 2, 241 | }, 242 | } 243 | got, err := c.TypeDefinition(ctx, &protocol.TypeDefinitionParams{ 244 | TextDocumentPositionParams: *pos, 245 | }) 246 | if err != nil { 247 | t.Fatalf("TypeDefinition failed: %v", err) 248 | } 249 | want := []protocol.Location{ 250 | { 251 | URI: uri, 252 | Range: protocol.Range{ 253 | Start: protocol.Position{ 254 | Line: 4, 255 | Character: 5, 256 | }, 257 | End: protocol.Position{ 258 | Line: 4, 259 | Character: 6, 260 | }, 261 | }, 262 | }, 263 | } 264 | if !reflect.DeepEqual(want, got) { 265 | t.Errorf("type definition result is %v; expected %v", got, want) 266 | } 267 | }) 268 | } 269 | } 270 | 271 | func TestGoDiagnostics(t *testing.T) { 272 | src := `package main // import "example.com/test" 273 | 274 | func main() { 275 | var s string 276 | } 277 | ` 278 | dir, err := ioutil.TempDir("", "examplemod") 279 | if err != nil { 280 | t.Fatalf("TempDir failed: %v", err) 281 | } 282 | defer os.RemoveAll(dir) 283 | 284 | gofile := filepath.Join(dir, "main.go") 285 | if err := ioutil.WriteFile(gofile, []byte(src), 0644); err != nil { 286 | t.Fatalf("WriteFile failed: %v", err) 287 | } 288 | modfile := filepath.Join(dir, "go.mod") 289 | if err := ioutil.WriteFile(modfile, []byte(goMod), 0644); err != nil { 290 | t.Fatalf("WriteFile failed: %v", err) 291 | } 292 | 293 | ch := make(chan *protocol.Diagnostic) 294 | cs := &config.Server{ 295 | Command: []string{"gopls"}, 296 | } 297 | srv, err := execServer(cs, &ClientConfig{ 298 | Server: &config.Server{}, 299 | RootDirectory: dir, 300 | DiagWriter: &chanDiagosticsWriter{ch}, 301 | Workspaces: nil, 302 | }, false) 303 | if err != nil { 304 | t.Fatalf("startServer failed: %v", err) 305 | } 306 | defer srv.Close() 307 | 308 | ctx := context.Background() 309 | err = lsp.DidOpen(ctx, srv.Client, gofile, "go", []byte(src)) 310 | if err != nil { 311 | t.Fatalf("DidOpen failed: %v", err) 312 | } 313 | 314 | diag := <-ch 315 | pat := regexp.MustCompile("^`?s'? declared (and|but) not used$") 316 | if !pat.MatchString(diag.Message) { 317 | t.Errorf("diagnostics message is %q does not match %q", diag.Message, pat) 318 | } 319 | 320 | err = lsp.DidClose(ctx, srv.Client, gofile) 321 | if err != nil { 322 | t.Fatalf("DidClose failed: %v", err) 323 | } 324 | srv.Client.Close() 325 | } 326 | 327 | const pySource = `#!/usr/bin/env python 328 | 329 | import math 330 | 331 | 332 | def main(): 333 | print(math.sqrt(42)) 334 | 335 | 336 | if __name__ == '__main__': 337 | main() 338 | ` 339 | 340 | const pySourceUnfmt = `#!/usr/bin/env python 341 | 342 | import math 343 | 344 | def main( ): 345 | print( math.sqrt ( 42 ) ) 346 | 347 | if __name__=='__main__': 348 | main( ) 349 | ` 350 | 351 | func testPython(t *testing.T, src string, f func(t *testing.T, c *Client, uri protocol.DocumentURI)) { 352 | dir, err := ioutil.TempDir("", "lspexample") 353 | if err != nil { 354 | t.Fatalf("TempDir failed: %v", err) 355 | } 356 | defer os.RemoveAll(dir) 357 | 358 | pyfile := filepath.Join(dir, "main.py") 359 | if err := ioutil.WriteFile(pyfile, []byte(src), 0644); err != nil { 360 | t.Fatalf("WriteFile failed: %v", err) 361 | } 362 | 363 | // Start the server 364 | cs := &config.Server{ 365 | Command: []string{"pyls"}, 366 | } 367 | srv, err := execServer(cs, &ClientConfig{ 368 | Server: &config.Server{}, 369 | RootDirectory: dir, 370 | DiagWriter: &mockDiagosticsWriter{ioutil.Discard}, 371 | Workspaces: nil, 372 | }, false) 373 | if err != nil { 374 | t.Fatalf("startServer failed: %v", err) 375 | } 376 | defer srv.Close() 377 | 378 | ctx := context.Background() 379 | err = lsp.DidOpen(ctx, srv.Client, pyfile, "python", []byte(src)) 380 | if err != nil { 381 | t.Fatalf("DidOpen failed: %v", err) 382 | } 383 | defer func() { 384 | err := lsp.DidClose(ctx, srv.Client, pyfile) 385 | if err != nil { 386 | t.Fatalf("DidClose failed: %v", err) 387 | } 388 | srv.Client.Close() 389 | }() 390 | 391 | f(t, srv.Client, text.ToURI(pyfile)) 392 | } 393 | 394 | func TestPythonHover(t *testing.T) { 395 | t.Skip("hangs") 396 | ctx := context.Background() 397 | 398 | testPython(t, pySource, func(t *testing.T, c *Client, uri protocol.DocumentURI) { 399 | pos := &protocol.TextDocumentPositionParams{ 400 | TextDocument: protocol.TextDocumentIdentifier{ 401 | URI: uri, 402 | }, 403 | Position: protocol.Position{ 404 | Line: 6, 405 | Character: 16, 406 | }, 407 | } 408 | hov, err := c.Hover(ctx, &protocol.HoverParams{ 409 | TextDocumentPositionParams: *pos, 410 | }) 411 | if err != nil { 412 | t.Fatalf("Hover failed: %v", err) 413 | } 414 | got := hov.Contents.Value 415 | want := "Return the square root of x." 416 | // May not be an exact match. 417 | // Perhaps depending on if it's Python 2 or 3? 418 | if !strings.Contains(got, want) { 419 | t.Errorf("hover result is %q does not contain %q", got, want) 420 | } 421 | }) 422 | } 423 | 424 | func TestPythonFormat(t *testing.T) { 425 | t.Skip("hangs") 426 | ctx := context.Background() 427 | 428 | testPython(t, pySourceUnfmt, func(t *testing.T, c *Client, uri protocol.DocumentURI) { 429 | edits, err := c.Formatting(ctx, &protocol.DocumentFormattingParams{ 430 | TextDocument: protocol.TextDocumentIdentifier{ 431 | URI: uri, 432 | }, 433 | }) 434 | if err != nil { 435 | t.Fatalf("Format failed: %v", err) 436 | } 437 | f := BytesFile([]byte(pySourceUnfmt)) 438 | err = text.Edit(&f, edits) 439 | if err != nil { 440 | t.Fatalf("failed to apply edits: %v", err) 441 | } 442 | if got := string(f); got != pySource { 443 | t.Errorf("bad format output:\n%s\nexpected:\n%s", got, pySource) 444 | } 445 | }) 446 | } 447 | 448 | func TestPythonDefinition(t *testing.T) { 449 | t.Skip("hangs") 450 | if runtime.GOOS == "windows" { 451 | t.Skip("TODO: failing on windows due to file path issues") 452 | } 453 | 454 | src := `def main(): 455 | pass 456 | 457 | if __name__ == '__main__': 458 | main() 459 | ` 460 | 461 | testPython(t, src, func(t *testing.T, c *Client, uri protocol.DocumentURI) { 462 | params := &protocol.DefinitionParams{ 463 | TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 464 | TextDocument: protocol.TextDocumentIdentifier{ 465 | URI: uri, 466 | }, 467 | Position: protocol.Position{ 468 | Line: 4, 469 | Character: 6, 470 | }, 471 | }, 472 | } 473 | got, err := c.Definition(context.Background(), params) 474 | if err != nil { 475 | t.Fatalf("Definition failed: %v", err) 476 | } 477 | want := []protocol.Location{ 478 | { 479 | URI: uri, 480 | Range: protocol.Range{ 481 | Start: protocol.Position{ 482 | Line: 0, 483 | Character: 4, 484 | }, 485 | End: protocol.Position{ 486 | Line: 0, 487 | Character: 8, 488 | }, 489 | }, 490 | }, 491 | } 492 | if !reflect.DeepEqual(want, got) { 493 | t.Errorf("definition result is %v; expected %v", got, want) 494 | } 495 | }) 496 | } 497 | 498 | func TestPythonTypeDefinition(t *testing.T) { 499 | t.Logf("pyls doesn't implement LSP textDocument/typeDefinition") 500 | } 501 | 502 | func TestFileLanguage(t *testing.T) { 503 | for _, tc := range []struct { 504 | name, lang string 505 | }{ 506 | {"/home/gopher/hello.py", "python"}, 507 | {"/home/gopher/hello.go", "go"}, 508 | {"/home/gopher/go.mod", "go.mod"}, 509 | {"/home/gopher/go.sum", "go.sum"}, 510 | {"/home/gopher/.config/acme-lsp/config.toml", "toml"}, 511 | } { 512 | lang := lsp.DetectLanguage(tc.name) 513 | if lang != tc.lang { 514 | t.Errorf("language ID of %q is %q; expected %q", tc.name, lang, tc.lang) 515 | } 516 | } 517 | } 518 | 519 | func TestLocationLink(t *testing.T) { 520 | if runtime.GOOS == "windows" { 521 | t.Skip("TODO: failing on windows due to file path issues") 522 | } 523 | 524 | l := &protocol.Location{ 525 | URI: protocol.DocumentURI("file:///home/gopher/mod1/main.go"), 526 | Range: protocol.Range{ 527 | Start: protocol.Position{ 528 | Line: 13, 529 | Character: 9, 530 | }, 531 | End: protocol.Position{ 532 | Line: 15, 533 | Character: 7, 534 | }, 535 | }, 536 | } 537 | got := lsp.LocationLink(l, "") 538 | want := "/home/gopher/mod1/main.go:14.10,16.8" 539 | if got != want { 540 | t.Errorf("LocationLink(%v) returned %q; want %q", l, got, want) 541 | } 542 | } 543 | 544 | type chanDiagosticsWriter struct { 545 | ch chan *protocol.Diagnostic 546 | } 547 | 548 | func (dw *chanDiagosticsWriter) WriteDiagnostics(params *protocol.PublishDiagnosticsParams) { 549 | for _, diag := range params.Diagnostics { 550 | dw.ch <- &diag 551 | } 552 | } 553 | 554 | var _ = text.File((*BytesFile)(nil)) 555 | 556 | type BytesFile []byte 557 | 558 | func (f *BytesFile) Reader() (io.Reader, error) { 559 | return bytes.NewReader(*f), nil 560 | } 561 | 562 | func (f *BytesFile) WriteAt(q0, q1 int, b []byte) (int, error) { 563 | r := []rune(string(*f)) 564 | 565 | rr := make([]rune, 0, len(r)+len(b)) 566 | rr = append(rr, r[:q0]...) 567 | rr = append(rr, []rune(string(b))...) 568 | rr = append(rr, r[q1:]...) 569 | *f = []byte(string(rr)) 570 | return len(b), nil 571 | } 572 | 573 | func (f *BytesFile) Mark() error { 574 | return nil 575 | } 576 | 577 | func (f *BytesFile) DisableMark() error { 578 | return nil 579 | } 580 | 581 | func TestClientProvidesCodeAction(t *testing.T) { 582 | for _, tc := range []struct { 583 | provider interface{} 584 | kind protocol.CodeActionKind 585 | want bool 586 | }{ 587 | {true, protocol.SourceOrganizeImports, true}, 588 | {false, protocol.SourceOrganizeImports, false}, 589 | {false, protocol.SourceOrganizeImports, false}, 590 | { 591 | protocol.CodeActionOptions{CodeActionKinds: []protocol.CodeActionKind{protocol.QuickFix, protocol.SourceOrganizeImports}}, 592 | protocol.SourceOrganizeImports, 593 | true, 594 | }, 595 | { 596 | protocol.CodeActionOptions{CodeActionKinds: []protocol.CodeActionKind{protocol.QuickFix}}, 597 | protocol.SourceOrganizeImports, 598 | false, 599 | }, 600 | { 601 | protocol.CodeActionOptions{CodeActionKinds: []protocol.CodeActionKind{}}, 602 | protocol.SourceOrganizeImports, 603 | false, 604 | }, 605 | { 606 | protocol.CodeActionOptions{CodeActionKinds: nil}, 607 | protocol.SourceOrganizeImports, 608 | false, 609 | }, 610 | } { 611 | c := &Client{ 612 | initializeResult: &protocol.InitializeResult{ 613 | Capabilities: protocol.ServerCapabilities{ 614 | CodeActionProvider: tc.provider, 615 | }, 616 | }, 617 | } 618 | got := lsp.ServerProvidesCodeAction(&c.initializeResult.Capabilities, tc.kind) 619 | want := tc.want 620 | if got != want { 621 | t.Errorf("got %v for provider %v and kind %v; want %v", 622 | got, tc.provider, tc.kind, want) 623 | } 624 | } 625 | } 626 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/config/config.go: -------------------------------------------------------------------------------- 1 | // Package config defines LSP tools configuration. 2 | package config 3 | 4 | import ( 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "strings" 14 | 15 | "9fans.net/internal/go-lsp/lsp/protocol" 16 | "github.com/BurntSushi/toml" 17 | "github.com/fhs/9fans-go/plan9/client" 18 | ) 19 | 20 | // Flags represent a set of command line flags. 21 | type Flags uint 22 | 23 | const ( 24 | // LangServerFlags include flags that configure LSP servers. 25 | LangServerFlags Flags = 1 << iota 26 | 27 | // ProxyFlags include flags that configure how to connect to proxy server. 28 | ProxyFlags 29 | ) 30 | 31 | // File represents user configuration file for acme-lsp and L. 32 | type File struct { 33 | // Network and address used for communication between acme-lsp and L. 34 | // Only required on systems without unix domain socket. 35 | ProxyNetwork, ProxyAddress string 36 | 37 | // Network and address where acme is serving 9P file server. 38 | // Only required on systems without unix domain socket. 39 | AcmeNetwork, AcmeAddress string 40 | 41 | // Initial set of workspace directories. 42 | WorkspaceDirectories []string 43 | 44 | // Root directory used for LSP initialization. 45 | RootDirectory string 46 | 47 | // Don't show diagnostics sent by the LSP server. 48 | HideDiagnostics bool 49 | 50 | // Format file when Put is executed in a window. 51 | FormatOnPut bool 52 | 53 | // Print to stderr the full rpc trace in lsp inspector format 54 | RPCTrace bool 55 | 56 | // LSP code actions to run when Put is executed in a window. 57 | CodeActionsOnPut []protocol.CodeActionKind 58 | 59 | // LSP servers keyed by a user provided name. 60 | Servers map[string]*Server 61 | 62 | // Servers determined by regular expression match on filename, 63 | // as supplied by -server and -dial flags. 64 | FilenameHandlers []FilenameHandler 65 | } 66 | 67 | // Config configures acme-lsp and L. 68 | type Config struct { 69 | File 70 | 71 | // Show current configuration and exit 72 | ShowConfig bool 73 | 74 | // Print more messages to stderr 75 | Verbose bool 76 | 77 | // Path to configuration file. 78 | filename string 79 | } 80 | 81 | // Server describes a LSP server. 82 | type Server struct { 83 | // Command that speaks LSP on stdin/stdout. 84 | // Can be empty if Address is given. 85 | Command []string 86 | 87 | // Dial address for LSP server. Ignored if Command is not empty. 88 | Address string 89 | 90 | // Write stderr of Command to this file. 91 | // If it's not an absolute path, it'll become relative to the cache directory. 92 | StderrFile string 93 | 94 | // Write log messages (window/logMessage notifications) sent by LSP server 95 | // to this file instead of stderr. 96 | // If it's not an absolute path, it'll become relative to the cache directory. 97 | LogFile string 98 | 99 | // Options contain server-specific settings that are passed as-is to the LSP server. 100 | Options interface{} 101 | } 102 | 103 | // FilenameHandler contains a regular expression pattern that matches a filename 104 | // and the associated server key. 105 | type FilenameHandler struct { 106 | // Pattern is a regular expression that matches filename. 107 | Pattern string 108 | 109 | // Language identifier (e.g. "go" or "python") 110 | // See list of languages here: 111 | // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentItem 112 | LanguageID string 113 | 114 | // ServerKey is the key in Config.File.Servers. 115 | ServerKey string 116 | } 117 | 118 | // Default returns the default Config. 119 | func Default() *Config { 120 | rootDir := "/" 121 | switch runtime.GOOS { 122 | case "windows": 123 | rootDir = `C:\` 124 | } 125 | return &Config{ 126 | File: File{ 127 | ProxyNetwork: "unix", 128 | ProxyAddress: filepath.Join(client.Namespace(), "acme-lsp.rpc"), 129 | AcmeNetwork: "unix", 130 | AcmeAddress: filepath.Join(client.Namespace(), "acme"), 131 | WorkspaceDirectories: nil, 132 | RootDirectory: rootDir, 133 | FormatOnPut: true, 134 | CodeActionsOnPut: []protocol.CodeActionKind{ 135 | protocol.SourceOrganizeImports, 136 | }, 137 | Servers: nil, 138 | FilenameHandlers: nil, 139 | }, 140 | } 141 | } 142 | 143 | func userConfigFilename() (string, error) { 144 | dir, err := UserConfigDir() 145 | if err != nil { 146 | return "", err 147 | } 148 | return filepath.Join(dir, "acme-lsp/config.toml"), nil 149 | } 150 | 151 | // Load loads Config from file system. It first looks at ACME_LSP_CONFIG 152 | // environment variable for location of configuration file and loads 153 | // it if it is set. If the environment variable is not set, it'll use 154 | // UserConfigDir/acme-lsp/config.toml if it exists. Otherwise, it'll 155 | // falling back to a default configuration. 156 | func Load() (*Config, error) { 157 | filename := os.Getenv("ACME_LSP_CONFIG") 158 | if filename == "" { 159 | var err error 160 | filename, err = userConfigFilename() 161 | if err != nil { 162 | return nil, err 163 | } 164 | if _, err := os.Stat(filename); os.IsNotExist(err) { 165 | return Default(), nil 166 | } 167 | } 168 | 169 | cfg, err := load(filename) 170 | if err != nil { 171 | return nil, err 172 | } 173 | def := Default() 174 | if cfg.File.ProxyNetwork == "" { 175 | cfg.File.ProxyNetwork = def.File.ProxyNetwork 176 | } 177 | if cfg.File.ProxyAddress == "" { 178 | cfg.File.ProxyAddress = def.File.ProxyAddress 179 | } 180 | if cfg.File.AcmeNetwork == "" { 181 | cfg.File.AcmeNetwork = def.File.AcmeNetwork 182 | } 183 | if cfg.File.AcmeAddress == "" { 184 | cfg.File.AcmeAddress = def.File.AcmeAddress 185 | } 186 | if cfg.File.RootDirectory == "" { 187 | cfg.File.RootDirectory = def.File.RootDirectory 188 | } 189 | cacheDir, err := os.UserCacheDir() 190 | if err != nil { 191 | return nil, err 192 | } 193 | cacheDir = filepath.Join(cacheDir, "acme-lsp") 194 | err = os.MkdirAll(cacheDir, 0700) 195 | if err != nil { 196 | return nil, err 197 | } 198 | for key := range cfg.Servers { 199 | if len(key) > 0 && key[0] == '_' { 200 | return nil, fmt.Errorf("server key %q begins with underscore", key) 201 | } 202 | s := cfg.File.Servers[key] 203 | if s.StderrFile != "" && !filepath.IsAbs(s.StderrFile) { 204 | s.StderrFile = filepath.Join(cacheDir, s.StderrFile) 205 | } 206 | if s.LogFile != "" && !filepath.IsAbs(s.LogFile) { 207 | s.LogFile = filepath.Join(cacheDir, s.LogFile) 208 | } 209 | } 210 | return cfg, nil 211 | } 212 | 213 | func load(filename string) (*Config, error) { 214 | b, err := ioutil.ReadFile(filename) 215 | if err != nil { 216 | return nil, err 217 | } 218 | var f File 219 | err = toml.Unmarshal(b, &f) 220 | if err != nil { 221 | return nil, err 222 | } 223 | return &Config{File: f, filename: filename}, nil 224 | } 225 | 226 | // Write writes Config to writer w. 227 | func Write(w io.Writer, cfg *Config) error { 228 | if cfg.filename != "" { 229 | fmt.Fprintf(w, "# Configuration file location: %v\n\n", cfg.filename) 230 | } else { 231 | fmt.Fprintf(w, "# Cound not find configuration file location.\n\n") 232 | } 233 | return toml.NewEncoder(w).Encode(cfg.File) 234 | } 235 | 236 | // ParseFlags parses command line flags and updates Config. 237 | func (cfg *Config) ParseFlags(flags Flags, f *flag.FlagSet, arguments []string) error { 238 | var ( 239 | workspaces string 240 | userServers serverFlag 241 | dialServers serverFlag 242 | ) 243 | 244 | f.StringVar(&cfg.AcmeNetwork, "acme.net", cfg.AcmeNetwork, 245 | "network where acme is serving 9P file system") 246 | f.StringVar(&cfg.AcmeAddress, "acme.addr", cfg.AcmeAddress, 247 | "address where acme is serving 9P file system") 248 | f.BoolVar(&cfg.Verbose, "v", cfg.Verbose, "Verbose output") 249 | f.BoolVar(&cfg.ShowConfig, "showconfig", false, "show configuration values and exit") 250 | 251 | if flags&ProxyFlags != 0 { 252 | f.StringVar(&cfg.ProxyNetwork, "proxy.net", cfg.ProxyNetwork, 253 | "network used for communication between acme-lsp and L") 254 | f.StringVar(&cfg.ProxyAddress, "proxy.addr", cfg.ProxyAddress, 255 | "address used for communication between acme-lsp and L") 256 | } 257 | if flags&LangServerFlags != 0 { 258 | f.BoolVar(&cfg.Verbose, "debug", cfg.Verbose, "turn on debugging prints (deprecated: use -v)") 259 | f.StringVar(&cfg.RootDirectory, "rootdir", cfg.RootDirectory, "root directory used for LSP initialization") 260 | f.BoolVar(&cfg.HideDiagnostics, "hidediag", false, "hide diagnostics sent by LSP server") 261 | f.BoolVar(&cfg.RPCTrace, "rpc.trace", false, "print the full rpc trace in lsp inspector format") 262 | f.StringVar(&workspaces, "workspaces", "", "colon-separated list of initial workspace directories") 263 | f.Var(&userServers, "server", `map filename to language server command. The format is 264 | 'handlers:cmd' where cmd is the LSP server command and handlers is 265 | a comma separated list of 'regexp[@lang]'. The regexp matches the 266 | filename and lang is a language identifier. (e.g. '\.go$:gopls' or 267 | 'go.mod$@go.mod,go.sum$@go.sum,\.go$@go:gopls')`) 268 | f.Var(&dialServers, "dial", `map filename to language server address. The format is 269 | 'handlers:host:port'. See -server flag for format of 270 | handlers. (e.g. '\.go$:localhost:4389')`) 271 | } 272 | if err := f.Parse(arguments); err != nil { 273 | return err 274 | } 275 | 276 | if flags&LangServerFlags != 0 { 277 | if len(workspaces) > 0 { 278 | cfg.WorkspaceDirectories = strings.Split(workspaces, ":") 279 | } 280 | if cfg.Servers == nil { 281 | cfg.Servers = make(map[string]*Server) 282 | } 283 | handlers := make([]FilenameHandler, 0) 284 | for i, sa := range userServers { 285 | key := fmt.Sprintf("_userCmdServer%v", i) 286 | cfg.Servers[key] = &Server{ 287 | Command: strings.Fields(sa.args), 288 | } 289 | for _, h := range sa.handlers { 290 | h.ServerKey = key 291 | handlers = append(handlers, h) 292 | } 293 | } 294 | for i, sa := range dialServers { 295 | key := fmt.Sprintf("_userDialServer%v", i) 296 | cfg.Servers[key] = &Server{ 297 | Address: sa.args, 298 | } 299 | for _, h := range sa.handlers { 300 | h.ServerKey = key 301 | handlers = append(handlers, h) 302 | } 303 | } 304 | // Prepend to give higher priority to command line flags. 305 | cfg.FilenameHandlers = append(handlers, cfg.FilenameHandlers...) 306 | } 307 | return nil 308 | } 309 | 310 | type serverArgs struct { 311 | handlers []FilenameHandler 312 | args string 313 | } 314 | 315 | type serverFlag []serverArgs 316 | 317 | func (sf *serverFlag) String() string { 318 | return fmt.Sprintf("%v", []serverArgs(*sf)) 319 | } 320 | 321 | func (sf *serverFlag) Set(val string) error { 322 | f := strings.SplitN(val, ":", 2) 323 | if len(f) != 2 { 324 | return errors.New("flag value must contain a colon") 325 | } 326 | // allow f[0] to be empty, as that's a valid regexp that matches anything 327 | if len(f[1]) == 0 { 328 | return errors.New("empty server command or addresss") 329 | } 330 | pairs := strings.Split(f[0], ",") 331 | args := f[1] 332 | 333 | var handlers []FilenameHandler 334 | for _, pp := range pairs { 335 | f := strings.SplitN(pp, "@", 2) 336 | if len(f) != 2 { 337 | handlers = append(handlers, FilenameHandler{ 338 | Pattern: pp, 339 | LanguageID: "", 340 | }) 341 | } else { 342 | handlers = append(handlers, FilenameHandler{ 343 | Pattern: f[0], 344 | LanguageID: f[1], 345 | }) 346 | } 347 | } 348 | *sf = append(*sf, serverArgs{ 349 | handlers: handlers, 350 | args: args, 351 | }) 352 | return nil 353 | } 354 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/config/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package config 6 | 7 | // TODO(fhs): Remove when we drop support for Go 1.12. 8 | 9 | // UserConfigDir returns the default root directory to use for user-specific 10 | // configuration data. Users should create their own application-specific 11 | // subdirectory within this one and use that. 12 | // 13 | // On Unix systems, it returns $XDG_CONFIG_HOME as specified by 14 | // https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html if 15 | // non-empty, else $HOME/.config. 16 | // On Darwin, it returns $HOME/Library/Application Support. 17 | // On Windows, it returns %AppData%. 18 | // On Plan 9, it returns $home/lib. 19 | // 20 | // If the location cannot be determined (for example, $HOME is not defined), 21 | // then it will return an error. 22 | func UserConfigDir() (string, error) { 23 | return userConfigDir() 24 | } 25 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/config/file_go1.12.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build !go1.13 6 | // +build !go1.13 7 | 8 | package config 9 | 10 | import ( 11 | "errors" 12 | "os" 13 | "runtime" 14 | ) 15 | 16 | func userConfigDir() (string, error) { 17 | var dir string 18 | 19 | switch runtime.GOOS { 20 | case "windows": 21 | dir = os.Getenv("AppData") 22 | if dir == "" { 23 | return "", errors.New("%AppData% is not defined") 24 | } 25 | 26 | case "darwin": 27 | dir = os.Getenv("HOME") 28 | if dir == "" { 29 | return "", errors.New("$HOME is not defined") 30 | } 31 | dir += "/Library/Application Support" 32 | 33 | case "plan9": 34 | dir = os.Getenv("home") 35 | if dir == "" { 36 | return "", errors.New("$home is not defined") 37 | } 38 | dir += "/lib" 39 | 40 | default: // Unix 41 | dir = os.Getenv("XDG_CONFIG_HOME") 42 | if dir == "" { 43 | dir = os.Getenv("HOME") 44 | if dir == "" { 45 | return "", errors.New("neither $XDG_CONFIG_HOME nor $HOME are defined") 46 | } 47 | dir += "/.config" 48 | } 49 | } 50 | 51 | return dir, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/config/file_go1.13.go: -------------------------------------------------------------------------------- 1 | //go:build go1.13 2 | // +build go1.13 3 | 4 | package config 5 | 6 | import "os" 7 | 8 | var userConfigDir = os.UserConfigDir 9 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/diagnostics.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sync" 8 | "syscall" 9 | "time" 10 | 11 | "9fans.net/acme-lsp/internal/acmeutil" 12 | "9fans.net/acme-lsp/internal/lsp" 13 | "9fans.net/internal/go-lsp/lsp/protocol" 14 | ) 15 | 16 | // diagWin implements client.DiagnosticsWriter. 17 | // It writes diagnostics to an acme window. 18 | // It will create the diagnostics window on-demand, recreating it if necessary. 19 | type diagWin struct { 20 | name string // window name 21 | *acmeutil.Win 22 | paramsChan chan *protocol.PublishDiagnosticsParams 23 | updateChan chan struct{} 24 | 25 | dead bool // window has been closed 26 | mu sync.Mutex 27 | } 28 | 29 | func newDiagWin(name string) *diagWin { 30 | return &diagWin{ 31 | name: name, 32 | updateChan: make(chan struct{}), 33 | paramsChan: make(chan *protocol.PublishDiagnosticsParams, 100), 34 | dead: true, 35 | } 36 | } 37 | 38 | func (dw *diagWin) restart() error { 39 | dw.mu.Lock() 40 | defer dw.mu.Unlock() 41 | 42 | if !dw.dead { 43 | return nil 44 | } 45 | w, err := acmeutil.Hijack(dw.name) 46 | if err != nil { 47 | w, err = acmeutil.NewWin() 48 | if err != nil { 49 | return err 50 | } 51 | w.Name(dw.name) 52 | w.Write("tag", []byte("Reload ")) 53 | } 54 | dw.Win = w 55 | dw.dead = false 56 | 57 | go func() { 58 | defer func() { 59 | dw.mu.Lock() 60 | dw.Del(true) 61 | dw.CloseFiles() 62 | dw.dead = true 63 | dw.mu.Unlock() 64 | }() 65 | 66 | for ev := range dw.EventChan() { 67 | if ev == nil { 68 | return 69 | } 70 | switch ev.C2 { 71 | case 'x', 'X': // execute 72 | switch string(ev.Text) { 73 | case "Del": 74 | return 75 | case "Reload": 76 | dw.updateChan <- struct{}{} 77 | continue 78 | 79 | case "Restart": 80 | restart() 81 | } 82 | } 83 | dw.WriteEvent(ev) 84 | } 85 | }() 86 | return nil 87 | } 88 | 89 | func (dw *diagWin) update(diags map[protocol.DocumentURI][]protocol.Diagnostic) error { 90 | if err := dw.restart(); err != nil { 91 | return err 92 | } 93 | 94 | dw.Clear() 95 | body := dw.FileReadWriter("body") 96 | basedir := "" // TODO 97 | for uri, uriDiag := range diags { 98 | for _, diag := range uriDiag { 99 | loc := &protocol.Location{ 100 | URI: uri, 101 | Range: diag.Range, 102 | } 103 | fmt.Fprintf(body, "%v: %v\n", lsp.LocationLink(loc, basedir), diag.Message) 104 | } 105 | } 106 | return dw.Ctl("clean") 107 | } 108 | 109 | func (dw *diagWin) WriteDiagnostics(params *protocol.PublishDiagnosticsParams) { 110 | dw.paramsChan <- params 111 | } 112 | 113 | func NewDiagnosticsWriter() DiagnosticsWriter { 114 | dw := newDiagWin("/LSP/Diagnostics") 115 | 116 | // Collect stream of diagnostics updates and write them all 117 | // after certain interval if they need to be updated. 118 | go func() { 119 | diags := make(map[protocol.DocumentURI][]protocol.Diagnostic) 120 | ticker := time.NewTicker(time.Second) 121 | needsUpdate := false 122 | for { 123 | select { 124 | case <-ticker.C: 125 | if needsUpdate { 126 | dw.update(diags) 127 | needsUpdate = false 128 | } 129 | 130 | case <-dw.updateChan: // user request 131 | dw.update(diags) 132 | needsUpdate = false 133 | 134 | case params := <-dw.paramsChan: 135 | if len(diags[params.URI]) == 0 && len(params.Diagnostics) == 0 { 136 | continue 137 | } 138 | diags[params.URI] = params.Diagnostics 139 | needsUpdate = true 140 | } 141 | } 142 | }() 143 | return dw 144 | } 145 | 146 | func restart() { 147 | exe, err := os.Executable() 148 | if err != nil { 149 | exe = os.Args[0] 150 | } 151 | err = syscall.Exec(exe, os.Args, os.Environ()) 152 | log.Fatalf("exec: %v", err) 153 | } 154 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/exec.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "regexp" 13 | "sort" 14 | "strings" 15 | 16 | "9fans.net/internal/go-lsp/lsp/protocol" 17 | 18 | "9fans.net/acme-lsp/internal/lsp" 19 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 20 | "9fans.net/acme-lsp/internal/lsp/proxy" 21 | ) 22 | 23 | type Server struct { 24 | conn net.Conn 25 | Client *Client 26 | } 27 | 28 | func (s *Server) Close() { 29 | if s != nil { 30 | s.conn.Close() 31 | } 32 | } 33 | 34 | func execServer(cs *config.Server, cfg *ClientConfig, restartOnExit bool) (*Server, error) { 35 | args := cs.Command 36 | 37 | stderr := os.Stderr 38 | if cs.StderrFile != "" { 39 | f, err := os.Create(cs.StderrFile) 40 | if err != nil { 41 | return nil, fmt.Errorf("could not create server StderrFile: %v", err) 42 | } 43 | stderr = f 44 | } 45 | 46 | startCommand := func() (*exec.Cmd, net.Conn, error) { 47 | p0, p1 := net.Pipe() 48 | // TODO(fhs): use CommandContext? 49 | cmd := exec.Command(args[0], args[1:]...) 50 | cmd.Stdin = p0 51 | cmd.Stdout = p0 52 | if Verbose || cs.StderrFile != "" { 53 | cmd.Stderr = stderr 54 | } 55 | if err := cmd.Start(); err != nil { 56 | return nil, nil, fmt.Errorf("failed to execute language server: %v", err) 57 | } 58 | return cmd, p1, nil 59 | } 60 | cmd, p1, err := startCommand() 61 | if err != nil { 62 | return nil, err 63 | } 64 | srv := &Server{ 65 | conn: p1, 66 | } 67 | 68 | // Restart server if it dies. 69 | go func() { 70 | for { 71 | err := cmd.Wait() 72 | log.Printf("language server %v exited: %v", args[0], err) 73 | 74 | if !restartOnExit { 75 | break 76 | } 77 | 78 | // TODO(fhs): cancel using context? 79 | srv.conn.Close() 80 | log.Printf("restarting language server %v after exit", args[0]) 81 | cmd, p1, err = startCommand() 82 | if err != nil { 83 | log.Printf("%v", err) 84 | return 85 | } 86 | srv.conn = p1 87 | 88 | go func() { 89 | // Reinitialize existing client instead of creating a new one 90 | // because it's still being used. 91 | if err := srv.Client.init(p1, cfg); err != nil { 92 | log.Printf("initialize after server restart failed: %v", err) 93 | cmd.Process.Kill() 94 | srv.conn.Close() 95 | } 96 | }() 97 | } 98 | }() 99 | 100 | srv.Client, err = NewClient(p1, cfg) 101 | if err != nil { 102 | cmd.Process.Kill() 103 | return nil, fmt.Errorf("failed to connect to language server %q: %v", args, err) 104 | } 105 | return srv, nil 106 | } 107 | 108 | func dialServer(cs *config.Server, cfg *ClientConfig) (*Server, error) { 109 | conn, err := net.Dial("tcp", cs.Address) 110 | if err != nil { 111 | return nil, err 112 | } 113 | c, err := NewClient(conn, cfg) 114 | if err != nil { 115 | return nil, fmt.Errorf("failed to connect to language server at %v: %v", cs.Address, err) 116 | } 117 | return &Server{ 118 | conn: conn, 119 | Client: c, 120 | }, nil 121 | } 122 | 123 | // ServerInfo holds information about a LSP server and optionally a connection to it. 124 | type ServerInfo struct { 125 | *config.Server 126 | *config.FilenameHandler 127 | 128 | Re *regexp.Regexp // filename regular expression 129 | Logger *log.Logger // Logger for config.Server.LogFile 130 | srv *Server // running server instance 131 | } 132 | 133 | func (info *ServerInfo) start(cfg *ClientConfig) (*Server, error) { 134 | if info.srv != nil { 135 | return info.srv, nil 136 | } 137 | 138 | if len(info.Address) > 0 { 139 | srv, err := dialServer(info.Server, cfg) 140 | if err != nil { 141 | return nil, err 142 | } 143 | info.srv = srv 144 | } else { 145 | srv, err := execServer(info.Server, cfg, true) 146 | if err != nil { 147 | return nil, err 148 | } 149 | info.srv = srv 150 | } 151 | return info.srv, nil 152 | } 153 | 154 | // ServerSet holds information about a set of LSP servers and connection to them, 155 | // which are created on-demand. 156 | type ServerSet struct { 157 | Data []*ServerInfo 158 | diagWriter DiagnosticsWriter 159 | workspaces map[string]*protocol.WorkspaceFolder // set of workspace folders 160 | cfg *config.Config 161 | } 162 | 163 | // NewServerSet creates a new server set from config. 164 | func NewServerSet(cfg *config.Config, diagWriter DiagnosticsWriter) (*ServerSet, error) { 165 | workspaces := make(map[string]*protocol.WorkspaceFolder) 166 | if len(cfg.WorkspaceDirectories) > 0 { 167 | folders, err := lsp.DirsToWorkspaceFolders(cfg.WorkspaceDirectories) 168 | if err != nil { 169 | return nil, err 170 | } 171 | for i := range folders { 172 | d := &folders[i] 173 | workspaces[d.URI] = d 174 | } 175 | } 176 | 177 | var data []*ServerInfo 178 | for i, h := range cfg.FilenameHandlers { 179 | cs, ok := cfg.Servers[h.ServerKey] 180 | if !ok { 181 | return nil, fmt.Errorf("server not found for key %q", h.ServerKey) 182 | } 183 | if len(cs.Command) == 0 && len(cs.Address) == 0 { 184 | return nil, fmt.Errorf("invalid server for key %q", h.ServerKey) 185 | } 186 | re, err := regexp.Compile(h.Pattern) 187 | if err != nil { 188 | return nil, err 189 | } 190 | var logger *log.Logger 191 | if cs.LogFile != "" { 192 | f, err := os.Create(cs.LogFile) 193 | if err != nil { 194 | return nil, fmt.Errorf("could not create server %v LogFile: %v", h.ServerKey, err) 195 | } 196 | logger = log.New(f, "", log.LstdFlags) 197 | } 198 | data = append(data, &ServerInfo{ 199 | Server: cs, 200 | FilenameHandler: &cfg.FilenameHandlers[i], 201 | Re: re, 202 | Logger: logger, 203 | }) 204 | } 205 | return &ServerSet{ 206 | Data: data, 207 | diagWriter: diagWriter, 208 | workspaces: workspaces, 209 | cfg: cfg, 210 | }, nil 211 | } 212 | 213 | func (ss *ServerSet) MatchFile(filename string) *ServerInfo { 214 | for i, info := range ss.Data { 215 | if info.Re.MatchString(filename) { 216 | return ss.Data[i] 217 | } 218 | } 219 | return nil 220 | } 221 | 222 | func (ss *ServerSet) ClientConfig(info *ServerInfo) *ClientConfig { 223 | return &ClientConfig{ 224 | Server: info.Server, 225 | FilenameHandler: info.FilenameHandler, 226 | RootDirectory: ss.cfg.RootDirectory, 227 | HideDiag: ss.cfg.HideDiagnostics, 228 | RPCTrace: ss.cfg.RPCTrace, 229 | DiagWriter: ss.diagWriter, 230 | Workspaces: ss.Workspaces(), 231 | Logger: info.Logger, 232 | } 233 | } 234 | 235 | func (ss *ServerSet) StartForFile(filename string) (*Server, bool, error) { 236 | info := ss.MatchFile(filename) 237 | if info == nil { 238 | return nil, false, nil // unknown language server 239 | } 240 | srv, err := info.start(ss.ClientConfig(info)) 241 | if err != nil { 242 | return nil, false, err 243 | } 244 | return srv, true, err 245 | } 246 | 247 | func (ss *ServerSet) ServerMatch(ctx context.Context, filename string) (proxy.Server, bool, error) { 248 | srv, found, err := ss.StartForFile(filename) 249 | if err != nil || !found { 250 | return nil, found, err 251 | } 252 | return srv.Client, found, err 253 | } 254 | 255 | func (ss *ServerSet) CloseAll() { 256 | for _, info := range ss.Data { 257 | info.srv.Close() 258 | } 259 | } 260 | 261 | func (ss *ServerSet) PrintTo(w io.Writer) { 262 | for _, info := range ss.Data { 263 | if len(info.Address) > 0 { 264 | fmt.Fprintf(w, "%v %v\n", info.Re, info.Address) 265 | } else { 266 | fmt.Fprintf(w, "%v %v\n", info.Re, strings.Join(info.Command, " ")) 267 | } 268 | } 269 | } 270 | 271 | func (ss *ServerSet) forEach(f func(*Client) error) error { 272 | for _, info := range ss.Data { 273 | srv, err := info.start(ss.ClientConfig(info)) 274 | if err != nil { 275 | return err 276 | } 277 | err = f(srv.Client) 278 | if err != nil { 279 | return err 280 | } 281 | } 282 | return nil 283 | } 284 | 285 | // Workspaces returns a sorted list of current workspace directories. 286 | func (ss *ServerSet) Workspaces() []protocol.WorkspaceFolder { 287 | var folders []protocol.WorkspaceFolder 288 | for i := range ss.workspaces { 289 | folders = append(folders, *ss.workspaces[i]) 290 | } 291 | sort.Slice(folders, func(i, j int) bool { 292 | return folders[i].URI < folders[j].URI 293 | }) 294 | return folders 295 | } 296 | 297 | // DidChangeWorkspaceFolders adds and removes given workspace folders. 298 | func (ss *ServerSet) DidChangeWorkspaceFolders(ctx context.Context, added, removed []protocol.WorkspaceFolder) error { 299 | err := ss.forEach(func(c *Client) error { 300 | return c.DidChangeWorkspaceFolders(ctx, &protocol.DidChangeWorkspaceFoldersParams{ 301 | Event: protocol.WorkspaceFoldersChangeEvent{ 302 | Added: added, 303 | Removed: removed, 304 | }, 305 | }) 306 | }) 307 | if err != nil { 308 | return err 309 | } 310 | for i := range added { 311 | d := &added[i] 312 | ss.workspaces[d.URI] = d 313 | } 314 | for _, d := range removed { 315 | delete(ss.workspaces, d.URI) 316 | } 317 | return nil 318 | } 319 | 320 | // AbsDirs returns the absolute representation of directories dirs. 321 | func AbsDirs(dirs []string) ([]string, error) { 322 | a := make([]string, len(dirs)) 323 | for i, d := range dirs { 324 | d, err := filepath.Abs(d) 325 | if err != nil { 326 | return nil, err 327 | } 328 | a[i] = d 329 | } 330 | return a, nil 331 | } 332 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/exec_test.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "testing" 12 | 13 | "9fans.net/acme-lsp/internal/lsp" 14 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 15 | "9fans.net/internal/go-lsp/lsp/protocol" 16 | "github.com/google/go-cmp/cmp" 17 | ) 18 | 19 | func TestAbsDirs(t *testing.T) { 20 | if runtime.GOOS == "windows" { 21 | t.Skip("TODO: failing on windows due to file path issues") 22 | } 23 | 24 | cwd, err := os.Getwd() 25 | if err != nil { 26 | t.Fatalf("Getwd: %v", err) 27 | } 28 | dirs := []string{ 29 | "/path/to/mod1", 30 | "./mod1", 31 | "../stuff/mod1", 32 | } 33 | got, err := AbsDirs(dirs) 34 | if err != nil { 35 | t.Fatalf("AbsDirs: %v", err) 36 | } 37 | want := []string{ 38 | "/path/to/mod1", 39 | filepath.Join(cwd, "mod1"), 40 | filepath.Join(cwd, "../stuff/mod1"), 41 | } 42 | if !cmp.Equal(got, want) { 43 | t.Errorf("AbsDirs of %v is %v; want %v", dirs, got, want) 44 | } 45 | } 46 | 47 | func TestServerSetWorkspaces(t *testing.T) { 48 | cfg := &config.Config{ 49 | File: config.File{ 50 | RootDirectory: "/", 51 | WorkspaceDirectories: []string{"/path/to/mod1", "/path/to/mod2"}, 52 | Servers: map[string]*config.Server{ 53 | "gopls": { 54 | Command: []string{"gopls"}, 55 | }, 56 | }, 57 | FilenameHandlers: []config.FilenameHandler{ 58 | { 59 | Pattern: `\.go$`, 60 | ServerKey: "gopls", 61 | }, 62 | }, 63 | }, 64 | } 65 | ss, err := NewServerSet(cfg, &mockDiagosticsWriter{ioutil.Discard}) 66 | if err != nil { 67 | t.Fatalf("failed to create server set: %v", err) 68 | } 69 | defer ss.CloseAll() 70 | 71 | want, err := lsp.DirsToWorkspaceFolders(cfg.WorkspaceDirectories) 72 | if err != nil { 73 | t.Fatalf("DirsToWorkspaceFolders failed: %v", err) 74 | } 75 | got := ss.Workspaces() 76 | if !cmp.Equal(got, want) { 77 | t.Errorf("initial workspaces are %v; want %v", got, want) 78 | } 79 | 80 | added, err := lsp.DirsToWorkspaceFolders([]string{"/path/to/mod3"}) 81 | if err != nil { 82 | t.Fatalf("DirsToWorkspaceFolders failed: %v", err) 83 | } 84 | want = append(want, added...) 85 | err = ss.DidChangeWorkspaceFolders(context.Background(), added, nil) 86 | if err != nil { 87 | t.Fatalf("ServerSet.AddWorkspaces: %v", err) 88 | } 89 | got = ss.Workspaces() 90 | if !cmp.Equal(got, want) { 91 | t.Errorf("after adding %v, workspaces are %v; want %v", added, got, want) 92 | } 93 | 94 | removed := want[:1] 95 | want = want[1:] 96 | err = ss.DidChangeWorkspaceFolders(context.Background(), nil, removed) 97 | if err != nil { 98 | t.Fatalf("ServerSet.RemoveWorkspaces: %v", err) 99 | } 100 | got = ss.Workspaces() 101 | if !cmp.Equal(got, want) { 102 | t.Errorf("after removing %v, workspaces are %v; want %v", removed, got, want) 103 | } 104 | } 105 | 106 | type mockDiagosticsWriter struct { 107 | io.Writer 108 | } 109 | 110 | func (dw *mockDiagosticsWriter) WriteDiagnostics(params *protocol.PublishDiagnosticsParams) { 111 | for _, diag := range params.Diagnostics { 112 | loc := &protocol.Location{ 113 | URI: params.URI, 114 | Range: diag.Range, 115 | } 116 | fmt.Fprintf(dw, "%v: %v\n", lsp.LocationLink(loc, ""), diag.Message) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/files.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | 9 | "9fans.net/acme-lsp/internal/acme" 10 | "9fans.net/acme-lsp/internal/acmeutil" 11 | "9fans.net/acme-lsp/internal/lsp" 12 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 13 | "9fans.net/acme-lsp/internal/lsp/text" 14 | "9fans.net/internal/go-lsp/lsp/protocol" 15 | ) 16 | 17 | // FileManager keeps track of open files in acme. 18 | // It is used to synchronize text with LSP server. 19 | // 20 | // Note that we can't cache the *acmeutil.Win for the windows 21 | // because having the ctl file open prevents del event from 22 | // being delivered to acme/log file. 23 | type FileManager struct { 24 | ss *ServerSet 25 | wins map[string]struct{} // set of open files 26 | mu sync.Mutex 27 | 28 | cfg *config.Config 29 | } 30 | 31 | // NewFileManager creates a new file manager, initialized with files currently open in acme. 32 | func NewFileManager(ss *ServerSet, cfg *config.Config) (*FileManager, error) { 33 | fm := &FileManager{ 34 | ss: ss, 35 | wins: make(map[string]struct{}), 36 | cfg: cfg, 37 | } 38 | 39 | wins, err := acme.Windows() 40 | if err != nil { 41 | return nil, fmt.Errorf("failed to read list of acme index: %v", err) 42 | } 43 | for _, info := range wins { 44 | err := fm.didOpen(info.ID, info.Name) 45 | if err != nil { 46 | return nil, err 47 | } 48 | } 49 | return fm, nil 50 | } 51 | 52 | // Run watches for files opened, closed, saved, or refreshed in acme 53 | // and tells LSP server about it. It also formats files when it's saved. 54 | func (fm *FileManager) Run() { 55 | alog, err := acme.Log() 56 | if err != nil { 57 | log.Printf("file manager opening acme/log: %v", err) 58 | return 59 | } 60 | defer alog.Close() 61 | 62 | for { 63 | ev, err := alog.Read() 64 | if err != nil { 65 | log.Printf("file manager reading acme/log: %v", err) 66 | return 67 | } 68 | switch ev.Op { 69 | case "new": 70 | if err := fm.didOpen(ev.ID, ev.Name); err != nil { 71 | log.Printf("didOpen failed in file manager: %v", err) 72 | } 73 | case "del": 74 | if err := fm.didClose(ev.Name); err != nil { 75 | log.Printf("didClose failed in file manager: %v", err) 76 | } 77 | case "get": 78 | if err := fm.didChange(ev.ID, ev.Name); err != nil { 79 | log.Printf("didChange failed in file manager: %v", err) 80 | } 81 | case "put": 82 | if err := fm.didSave(ev.ID, ev.Name); err != nil { 83 | log.Printf("didSave failed in file manager: %v", err) 84 | } 85 | if fm.cfg.FormatOnPut { 86 | if err := fm.format(ev.ID, ev.Name); err != nil && Verbose { 87 | log.Printf("Format failed in file manager: %v", err) 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | func (fm *FileManager) withClient(winid int, name string, f func(*Client, *acmeutil.Win) error) error { 95 | s, found, err := fm.ss.StartForFile(name) 96 | if err != nil { 97 | return err 98 | } 99 | if !found { 100 | return nil // Unknown language server. 101 | } 102 | 103 | var win *acmeutil.Win 104 | if winid >= 0 { 105 | w, err := acmeutil.OpenWin(winid) 106 | if err != nil { 107 | return err 108 | } 109 | defer w.CloseFiles() 110 | win = w 111 | } 112 | return f(s.Client, win) 113 | } 114 | 115 | func (fm *FileManager) didOpen(winid int, name string) error { 116 | return fm.withClient(winid, name, func(c *Client, w *acmeutil.Win) error { 117 | fm.mu.Lock() 118 | defer fm.mu.Unlock() 119 | 120 | if _, ok := fm.wins[name]; ok { 121 | return fmt.Errorf("file already open in file manager: %v", name) 122 | } 123 | fm.wins[name] = struct{}{} 124 | 125 | b, err := w.ReadAll("body") 126 | if err != nil { 127 | return err 128 | } 129 | return lsp.DidOpen(context.Background(), c, name, c.cfg.FilenameHandler.LanguageID, b) 130 | }) 131 | } 132 | 133 | func (fm *FileManager) didClose(name string) error { 134 | fm.mu.Lock() 135 | defer fm.mu.Unlock() 136 | 137 | if _, ok := fm.wins[name]; !ok { 138 | return nil // Unknown language server. 139 | } 140 | delete(fm.wins, name) 141 | 142 | return fm.withClient(-1, name, func(c *Client, _ *acmeutil.Win) error { 143 | return lsp.DidClose(context.Background(), c, name) 144 | }) 145 | } 146 | 147 | func (fm *FileManager) didChange(winid int, name string) error { 148 | fm.mu.Lock() 149 | defer fm.mu.Unlock() 150 | 151 | if _, ok := fm.wins[name]; !ok { 152 | return nil // Unknown language server. 153 | } 154 | return fm.withClient(winid, name, func(c *Client, w *acmeutil.Win) error { 155 | b, err := w.ReadAll("body") 156 | if err != nil { 157 | return err 158 | } 159 | return lsp.DidChange(context.Background(), c, name, b) 160 | }) 161 | } 162 | 163 | func (fm *FileManager) DidChange(winid int) error { 164 | w, err := acmeutil.OpenWin(winid) 165 | if err != nil { 166 | return err 167 | } 168 | defer w.CloseFiles() 169 | 170 | name, err := w.Filename() 171 | if err != nil { 172 | return fmt.Errorf("could not get filename for window %v: %v", winid, err) 173 | } 174 | // TODO(fhs): we are opening the window again in didChange. 175 | return fm.didChange(winid, name) 176 | } 177 | 178 | func (fm *FileManager) didSave(winid int, name string) error { 179 | fm.mu.Lock() 180 | defer fm.mu.Unlock() 181 | 182 | if _, ok := fm.wins[name]; !ok { 183 | return nil // Unknown language server. 184 | } 185 | return fm.withClient(winid, name, func(c *Client, w *acmeutil.Win) error { 186 | b, err := w.ReadAll("body") 187 | if err != nil { 188 | return err 189 | } 190 | 191 | // TODO(fhs): Maybe DidChange is not needed with includeText option to DidSave? 192 | err = lsp.DidChange(context.Background(), c, name, b) 193 | if err != nil { 194 | return err 195 | } 196 | return lsp.DidSave(context.Background(), c, name) 197 | }) 198 | } 199 | 200 | func (fm *FileManager) format(winid int, name string) error { 201 | fm.mu.Lock() 202 | defer fm.mu.Unlock() 203 | 204 | if _, ok := fm.wins[name]; !ok { 205 | return nil // Unknown language server. 206 | } 207 | return fm.withClient(winid, name, func(c *Client, w *acmeutil.Win) error { 208 | doc := &protocol.TextDocumentIdentifier{ 209 | URI: text.ToURI(name), 210 | } 211 | return CodeActionAndFormat(context.Background(), c, doc, w, fm.cfg.CodeActionsOnPut) 212 | }) 213 | } 214 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/proxy.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "9fans.net/acme-lsp/internal/lsp" 9 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 10 | "9fans.net/acme-lsp/internal/lsp/proxy" 11 | "9fans.net/acme-lsp/internal/lsp/text" 12 | "9fans.net/acme-lsp/internal/p9service" 13 | "9fans.net/internal/go-lsp/lsp/protocol" 14 | "github.com/sourcegraph/jsonrpc2" 15 | ) 16 | 17 | type proxyServer struct { 18 | ss *ServerSet // client connections to upstream LSP server (e.g. gopls) 19 | fm *FileManager 20 | proxy.NotImplementedServer 21 | } 22 | 23 | func (s *proxyServer) Version(ctx context.Context) (int, error) { 24 | return proxy.Version, nil 25 | } 26 | 27 | func (s *proxyServer) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) { 28 | return s.ss.Workspaces(), nil 29 | } 30 | 31 | func (s *proxyServer) InitializeResult(ctx context.Context, params *protocol.TextDocumentIdentifier) (*protocol.InitializeResult, error) { 32 | srv, err := serverForURI(s.ss, params.URI) 33 | if err != nil { 34 | return nil, fmt.Errorf("InitializeResult: %v", err) 35 | } 36 | return srv.Client.InitializeResult(ctx, params) 37 | } 38 | 39 | func (s *proxyServer) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { 40 | srv, err := serverForURI(s.ss, params.TextDocument.URI) 41 | if err != nil { 42 | return fmt.Errorf("DidChange: %v", err) 43 | } 44 | return srv.Client.DidChange(ctx, params) 45 | } 46 | 47 | func (s *proxyServer) DidChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error { 48 | return s.ss.DidChangeWorkspaceFolders(ctx, params.Event.Added, params.Event.Removed) 49 | } 50 | 51 | func (s *proxyServer) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { 52 | srv, err := serverForURI(s.ss, params.TextDocumentPositionParams.TextDocument.URI) 53 | if err != nil { 54 | return nil, fmt.Errorf("Completion: %v", err) 55 | } 56 | return srv.Client.Completion(ctx, params) 57 | } 58 | 59 | func (s *proxyServer) Definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { 60 | srv, err := serverForURI(s.ss, params.TextDocumentPositionParams.TextDocument.URI) 61 | if err != nil { 62 | return nil, fmt.Errorf("Definition: %v", err) 63 | } 64 | return srv.Client.Definition(ctx, params) 65 | } 66 | 67 | func (s *proxyServer) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { 68 | srv, err := serverForURI(s.ss, params.TextDocument.URI) 69 | if err != nil { 70 | return nil, fmt.Errorf("Formatting: %v", err) 71 | } 72 | return srv.Client.Formatting(ctx, params) 73 | } 74 | 75 | func (s *proxyServer) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { 76 | srv, err := serverForURI(s.ss, params.TextDocument.URI) 77 | if err != nil { 78 | return nil, fmt.Errorf("CodeAction: %v", err) 79 | } 80 | return srv.Client.CodeAction(ctx, params) 81 | } 82 | 83 | func (s *proxyServer) ExecuteCommandOnDocument(ctx context.Context, params *proxy.ExecuteCommandOnDocumentParams) (interface{}, error) { 84 | srv, err := serverForURI(s.ss, params.TextDocument.URI) 85 | if err != nil { 86 | return nil, fmt.Errorf("ExecuteCommandOnDocument: %v", err) 87 | } 88 | return srv.Client.ExecuteCommand(ctx, ¶ms.ExecuteCommandParams) 89 | } 90 | 91 | func (s *proxyServer) Hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { 92 | srv, err := serverForURI(s.ss, params.TextDocumentPositionParams.TextDocument.URI) 93 | if err != nil { 94 | return nil, fmt.Errorf("Hover: %v", err) 95 | } 96 | return srv.Client.Hover(ctx, params) 97 | } 98 | 99 | func (s *proxyServer) Implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) { 100 | srv, err := serverForURI(s.ss, params.TextDocumentPositionParams.TextDocument.URI) 101 | if err != nil { 102 | return nil, fmt.Errorf("Implementation: %v", err) 103 | } 104 | return srv.Client.Implementation(ctx, params) 105 | } 106 | 107 | func (s *proxyServer) References(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) { 108 | srv, err := serverForURI(s.ss, params.TextDocumentPositionParams.TextDocument.URI) 109 | if err != nil { 110 | return nil, fmt.Errorf("References: %v", err) 111 | } 112 | return srv.Client.References(ctx, params) 113 | } 114 | 115 | func (s *proxyServer) Rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { 116 | srv, err := serverForURI(s.ss, params.TextDocument.URI) 117 | if err != nil { 118 | return nil, fmt.Errorf("Rename: %v", err) 119 | } 120 | return srv.Client.Rename(ctx, params) 121 | } 122 | 123 | func (s *proxyServer) SignatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { 124 | srv, err := serverForURI(s.ss, params.TextDocumentPositionParams.TextDocument.URI) 125 | if err != nil { 126 | return nil, fmt.Errorf("SignatureHelp: %v", err) 127 | } 128 | return srv.Client.SignatureHelp(ctx, params) 129 | } 130 | 131 | func (s *proxyServer) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { 132 | srv, err := serverForURI(s.ss, params.TextDocument.URI) 133 | if err != nil { 134 | return nil, fmt.Errorf("DocumentSymbol: %v", err) 135 | } 136 | return srv.Client.DocumentSymbol(ctx, params) 137 | } 138 | 139 | func (s *proxyServer) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) { 140 | srv, err := serverForURI(s.ss, params.TextDocumentPositionParams.TextDocument.URI) 141 | if err != nil { 142 | return nil, fmt.Errorf("TypeDefinition: %v", err) 143 | } 144 | return srv.Client.TypeDefinition(ctx, params) 145 | } 146 | 147 | func serverForURI(ss *ServerSet, uri protocol.DocumentURI) (*Server, error) { 148 | filename := text.ToPath(uri) 149 | srv, found, err := ss.StartForFile(filename) 150 | if !found { 151 | return nil, fmt.Errorf("unknown language server for URI %q", uri) 152 | } 153 | if err != nil { 154 | return nil, fmt.Errorf("cound not start language server: %v", err) 155 | } 156 | return srv, nil 157 | } 158 | 159 | func ListenAndServeProxy(ctx context.Context, cfg *config.Config, ss *ServerSet, fm *FileManager) error { 160 | ln, err := p9service.Listen(ctx, cfg.ProxyNetwork, cfg.ProxyAddress) 161 | if err != nil { 162 | return err 163 | } 164 | // The context doesn't affect Accept below, 165 | // so roll our own cancellation/timeout. 166 | // See https://github.com/golang/go/issues/28120#issuecomment-428978461 167 | go func() { 168 | <-ctx.Done() 169 | ln.Close() 170 | }() 171 | for { 172 | conn, err := ln.Accept() 173 | if err != nil { 174 | return err 175 | } 176 | stream := jsonrpc2.NewBufferedStream(conn, jsonrpc2.VSCodeObjectCodec{}) 177 | handler := proxy.NewServerHandler(&proxyServer{ 178 | ss: ss, 179 | fm: fm, 180 | }) 181 | var opts []jsonrpc2.ConnOpt 182 | if cfg.RPCTrace { 183 | opts = append(opts, lsp.LogMessages(log.Default())) 184 | 185 | } 186 | rpc := jsonrpc2.NewConn(ctx, stream, handler, opts...) 187 | go func() { 188 | <-rpc.DisconnectNotify() 189 | if cfg.Verbose { 190 | log.Printf("proxy: jsonrpc2 connection disconnected\n") 191 | } 192 | }() 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /internal/lsp/acmelsp/remote.go: -------------------------------------------------------------------------------- 1 | package acmelsp 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "9fans.net/acme-lsp/internal/acmeutil" 13 | "9fans.net/acme-lsp/internal/lsp" 14 | "9fans.net/acme-lsp/internal/lsp/proxy" 15 | "9fans.net/acme-lsp/internal/lsp/text" 16 | "9fans.net/internal/go-lsp/lsp/protocol" 17 | ) 18 | 19 | // RemoteCmd executes LSP commands in an acme window using the proxy server. 20 | type RemoteCmd struct { 21 | server proxy.Server 22 | winid int 23 | Stdout io.Writer 24 | Stderr io.Writer 25 | } 26 | 27 | func NewRemoteCmd(server proxy.Server, winid int) *RemoteCmd { 28 | return &RemoteCmd{ 29 | server: server, 30 | winid: winid, 31 | Stdout: os.Stdout, 32 | Stderr: os.Stderr, 33 | } 34 | } 35 | 36 | func (rc *RemoteCmd) getPosition() (pos *protocol.TextDocumentPositionParams, filename string, err error) { 37 | w, err := acmeutil.OpenWin(rc.winid) 38 | if err != nil { 39 | return nil, "", fmt.Errorf("failed to to open window %v: %v", rc.winid, err) 40 | } 41 | defer w.CloseFiles() 42 | 43 | return text.Position(w) 44 | } 45 | 46 | func (rc *RemoteCmd) DidChange(ctx context.Context) error { 47 | w, err := acmeutil.OpenWin(rc.winid) 48 | if err != nil { 49 | return fmt.Errorf("failed to to open window %v: %v", rc.winid, err) 50 | } 51 | defer w.CloseFiles() 52 | 53 | uri, _, err := text.DocumentURI(w) 54 | if err != nil { 55 | return err 56 | } 57 | body, err := w.ReadAll("body") 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return rc.server.DidChange(ctx, &protocol.DidChangeTextDocumentParams{ 63 | TextDocument: protocol.VersionedTextDocumentIdentifier{ 64 | TextDocumentIdentifier: protocol.TextDocumentIdentifier{ 65 | URI: uri, 66 | }, 67 | }, 68 | ContentChanges: []protocol.TextDocumentContentChangeEvent{ 69 | { 70 | Text: string(body), 71 | }, 72 | }, 73 | }) 74 | } 75 | 76 | type CompletionKind int 77 | 78 | const ( 79 | CompleteNoEdit CompletionKind = iota 80 | CompleteInsertOnlyMatch 81 | CompleteInsertFirstMatch 82 | ) 83 | 84 | func (rc *RemoteCmd) Completion(ctx context.Context, kind CompletionKind) error { 85 | w, err := acmeutil.OpenWin(rc.winid) 86 | if err != nil { 87 | return err 88 | } 89 | defer w.CloseFiles() 90 | 91 | pos, _, err := text.Position(w) 92 | if err != nil { 93 | return err 94 | } 95 | result, err := rc.server.Completion(ctx, &protocol.CompletionParams{ 96 | TextDocumentPositionParams: *pos, 97 | }) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if (kind == CompleteInsertFirstMatch && len(result.Items) >= 1) || (kind == CompleteInsertOnlyMatch && len(result.Items) == 1) { 103 | textEdit := result.Items[0].TextEdit 104 | if textEdit == nil { 105 | // TODO(fhs): Use insertText or label instead. 106 | return fmt.Errorf("nil TextEdit in completion item") 107 | } 108 | if err := text.Edit(w, []protocol.TextEdit{*textEdit}); err != nil { 109 | return fmt.Errorf("failed to apply completion edit: %v", err) 110 | } 111 | 112 | if len(result.Items) == 1 { 113 | return nil 114 | } 115 | } 116 | 117 | var sb strings.Builder 118 | 119 | if len(result.Items) == 0 { 120 | fmt.Fprintf(&sb, "no completion\n") 121 | } 122 | 123 | for _, item := range result.Items { 124 | fmt.Fprintf(&sb, "%v\t%v\n", item.Label, item.Detail) 125 | } 126 | 127 | if kind == CompleteInsertFirstMatch { 128 | cw, err := acmeutil.Hijack("/LSP/Completions") 129 | if err != nil { 130 | cw, err = acmeutil.NewWin() 131 | if err != nil { 132 | return err 133 | } 134 | 135 | cw.Name("/LSP/Completions") 136 | } 137 | 138 | defer cw.Win.Ctl("clean") 139 | 140 | cw.Clear() 141 | cw.PrintTabbed(sb.String()) 142 | } else { 143 | fmt.Fprintln(rc.Stdout, sb.String()) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func (rc *RemoteCmd) Definition(ctx context.Context, print bool) error { 150 | pos, _, err := rc.getPosition() 151 | if err != nil { 152 | return fmt.Errorf("failed to get position: %v", err) 153 | } 154 | locations, err := rc.server.Definition(ctx, &protocol.DefinitionParams{ 155 | TextDocumentPositionParams: *pos, 156 | }) 157 | if err != nil { 158 | return fmt.Errorf("bad server response: %v", err) 159 | } 160 | if print { 161 | return PrintLocations(rc.Stdout, locations) 162 | } 163 | return PlumbLocations(locations) 164 | } 165 | 166 | func (rc *RemoteCmd) OrganizeImportsAndFormat(ctx context.Context) error { 167 | win, err := acmeutil.OpenWin(rc.winid) 168 | if err != nil { 169 | return err 170 | } 171 | defer win.CloseFiles() 172 | 173 | uri, _, err := text.DocumentURI(win) 174 | if err != nil { 175 | return err 176 | } 177 | 178 | doc := &protocol.TextDocumentIdentifier{ 179 | URI: uri, 180 | } 181 | return CodeActionAndFormat(ctx, rc.server, doc, win, []protocol.CodeActionKind{ 182 | protocol.SourceOrganizeImports, 183 | }) 184 | } 185 | 186 | func (rc *RemoteCmd) Hover(ctx context.Context) error { 187 | pos, _, err := rc.getPosition() 188 | if err != nil { 189 | return err 190 | } 191 | 192 | hov, err := rc.server.Hover(ctx, &protocol.HoverParams{ 193 | TextDocumentPositionParams: *pos, 194 | }) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | if hov == nil { 200 | fmt.Fprintln(rc.Stdout, "No hover help available.") 201 | return nil 202 | } 203 | 204 | fmt.Fprintf(rc.Stdout, "%v\n", hov.Contents.Value) 205 | 206 | return nil 207 | } 208 | 209 | func (rc *RemoteCmd) Implementation(ctx context.Context, print bool) error { 210 | pos, _, err := rc.getPosition() 211 | if err != nil { 212 | return err 213 | } 214 | loc, err := rc.server.Implementation(ctx, &protocol.ImplementationParams{ 215 | TextDocumentPositionParams: *pos, 216 | }) 217 | if err != nil { 218 | return err 219 | } 220 | if len(loc) == 0 { 221 | fmt.Fprintf(rc.Stderr, "No implementations found.\n") 222 | return nil 223 | } 224 | return PrintLocations(rc.Stdout, loc) 225 | } 226 | 227 | func (rc *RemoteCmd) References(ctx context.Context) error { 228 | pos, _, err := rc.getPosition() 229 | if err != nil { 230 | return err 231 | } 232 | loc, err := rc.server.References(ctx, &protocol.ReferenceParams{ 233 | TextDocumentPositionParams: *pos, 234 | Context: protocol.ReferenceContext{ 235 | IncludeDeclaration: true, 236 | }, 237 | }) 238 | if err != nil { 239 | return err 240 | } 241 | if len(loc) == 0 { 242 | fmt.Fprintf(rc.Stderr, "No references found.\n") 243 | return nil 244 | } 245 | return PrintLocations(rc.Stdout, loc) 246 | } 247 | 248 | // Rename renames the identifier at cursor position to newname. 249 | func (rc *RemoteCmd) Rename(ctx context.Context, newname string) error { 250 | pos, _, err := rc.getPosition() 251 | if err != nil { 252 | return err 253 | } 254 | we, err := rc.server.Rename(ctx, &protocol.RenameParams{ 255 | TextDocument: pos.TextDocument, 256 | Position: pos.Position, 257 | NewName: newname, 258 | }) 259 | if err != nil { 260 | return err 261 | } 262 | return editWorkspace(we) 263 | } 264 | 265 | func (rc *RemoteCmd) SignatureHelp(ctx context.Context) error { 266 | pos, _, err := rc.getPosition() 267 | if err != nil { 268 | return err 269 | } 270 | sh, err := rc.server.SignatureHelp(ctx, &protocol.SignatureHelpParams{ 271 | TextDocumentPositionParams: *pos, 272 | }) 273 | if err != nil { 274 | return err 275 | } 276 | if sh != nil { 277 | for _, sig := range sh.Signatures { 278 | fmt.Fprintf(rc.Stdout, "%v\n", sig.Label) 279 | fmt.Fprintf(rc.Stdout, "%v\n", sig.Documentation) 280 | } 281 | } 282 | return nil 283 | } 284 | 285 | func (rc *RemoteCmd) DocumentSymbol(ctx context.Context) error { 286 | win, err := acmeutil.OpenWin(rc.winid) 287 | if err != nil { 288 | return err 289 | } 290 | defer win.CloseFiles() 291 | 292 | basedir := "" // TODO 293 | 294 | uri, _, err := text.DocumentURI(win) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | // TODO(fhs): DocumentSymbol request can return either a 300 | // []DocumentSymbol (hierarchical) or []SymbolInformation (flat). 301 | // We only handle the hierarchical type below. 302 | 303 | // TODO(fhs): Make use of DocumentSymbol.Range to optionally filter out 304 | // symbols that aren't within current cursor position? 305 | 306 | syms, err := rc.server.DocumentSymbol(ctx, &protocol.DocumentSymbolParams{ 307 | TextDocument: protocol.TextDocumentIdentifier{ 308 | URI: uri, 309 | }, 310 | }) 311 | if err != nil { 312 | return err 313 | } 314 | if len(syms) == 0 { 315 | fmt.Fprintf(rc.Stderr, "No symbols found.\n") 316 | return nil 317 | } 318 | walkDocumentSymbols(syms, 0, func(s *protocol.DocumentSymbol, depth int) { 319 | loc := &protocol.Location{ 320 | URI: uri, 321 | Range: s.SelectionRange, 322 | } 323 | indent := strings.Repeat(" ", depth) 324 | fmt.Fprintf(rc.Stdout, "%v%v %v\n", indent, s.Name, s.Detail) 325 | fmt.Fprintf(rc.Stdout, "%v %v\n", indent, lsp.LocationLink(loc, basedir)) 326 | }) 327 | return nil 328 | } 329 | 330 | func (rc *RemoteCmd) TypeDefinition(ctx context.Context, print bool) error { 331 | pos, _, err := rc.getPosition() 332 | if err != nil { 333 | return err 334 | } 335 | locations, err := rc.server.TypeDefinition(ctx, &protocol.TypeDefinitionParams{ 336 | TextDocumentPositionParams: *pos, 337 | }) 338 | if err != nil { 339 | return err 340 | } 341 | if print { 342 | return PrintLocations(rc.Stdout, locations) 343 | } 344 | return PlumbLocations(locations) 345 | } 346 | 347 | func walkDocumentSymbols1(syms []protocol.DocumentSymbol, depth int, f func(s *protocol.DocumentSymbol, depth int)) { 348 | for _, s := range syms { 349 | f(&s, depth) 350 | walkDocumentSymbols1(s.Children, depth+1, f) 351 | } 352 | } 353 | 354 | func walkDocumentSymbols(syms []interface{}, depth int, f func(s *protocol.DocumentSymbol, depth int)) { 355 | for _, s := range syms { 356 | switch val := s.(type) { 357 | default: 358 | log.Printf("unknown symbol type %T", val) 359 | 360 | case protocol.DocumentSymbol: 361 | f(&val, depth) 362 | walkDocumentSymbols1(val.Children, depth+1, f) 363 | 364 | // Workaround for the DocumentSymbol not being parsed by the auto-generated LSP definitions. 365 | case map[string]interface{}: 366 | ds, err := parseDocumentSymbol(val) 367 | if err != nil { 368 | log.Printf("failed to parse DocumentSymbols: %v\n", err) 369 | } else { 370 | f(ds, depth) 371 | walkDocumentSymbols1(ds.Children, depth+1, f) 372 | } 373 | } 374 | } 375 | } 376 | 377 | func parseDocumentSymbol(data map[string]interface{}) (*protocol.DocumentSymbol, error) { 378 | b, err := json.Marshal(data) 379 | if err != nil { 380 | return nil, err 381 | } 382 | var ds protocol.DocumentSymbol 383 | if err := json.Unmarshal(b, &ds); err != nil { 384 | return nil, err 385 | } 386 | return &ds, nil 387 | } 388 | -------------------------------------------------------------------------------- /internal/lsp/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Pakcage cmd contains utlity functions that help implement lsp related commands. 2 | package cmd 3 | 4 | import ( 5 | "flag" 6 | "log" 7 | "os" 8 | 9 | "9fans.net/acme-lsp/internal/acme" 10 | "9fans.net/acme-lsp/internal/lsp/acmelsp" 11 | "9fans.net/acme-lsp/internal/lsp/acmelsp/config" 12 | ) 13 | 14 | func Setup(flags config.Flags) *config.Config { 15 | cfg, err := config.Load() 16 | if err != nil { 17 | log.Fatalf("failed to load config file: %v", err) 18 | } 19 | err = cfg.ParseFlags(flags, flag.CommandLine, os.Args[1:]) 20 | if err != nil { 21 | // Unreached since flag.CommandLine uses flag.ExitOnError. 22 | log.Fatalf("failed to parse flags: %v", err) 23 | } 24 | 25 | if cfg.ShowConfig { 26 | config.Write(os.Stdout, cfg) 27 | os.Exit(0) 28 | } 29 | 30 | // Setup custom acme package 31 | acme.Network = cfg.AcmeNetwork 32 | acme.Address = cfg.AcmeAddress 33 | 34 | if cfg.Verbose { 35 | acmelsp.Verbose = true 36 | } 37 | return cfg 38 | } 39 | -------------------------------------------------------------------------------- /internal/lsp/proxy/client.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "9fans.net/internal/go-lsp/lsp/protocol" 8 | "github.com/sourcegraph/jsonrpc2" 9 | ) 10 | 11 | const Debug = false 12 | 13 | type Client interface { 14 | protocol.Client 15 | } 16 | 17 | func clientDispatch(ctx context.Context, client Client, conn *jsonrpc2.Conn, r *jsonrpc2.Request) (bool, error) { 18 | return false, nil 19 | } 20 | 21 | var _ Client = (*clientDispatcher)(nil) 22 | 23 | type clientDispatcher struct { 24 | *jsonrpc2.Conn 25 | protocol.Client 26 | } 27 | 28 | var _ protocol.Client = (*NotImplementedClient)(nil) 29 | 30 | type NotImplementedClient struct{} 31 | 32 | // $/logTrace 33 | func (c *NotImplementedClient) LogTrace(context.Context, *protocol.LogTraceParams) error { 34 | return fmt.Errorf("$/logTrace not implemented") 35 | } 36 | 37 | // $/progress 38 | func (c *NotImplementedClient) Progress(context.Context, *protocol.ProgressParams) error { 39 | return fmt.Errorf("$/progress not implemented") 40 | } 41 | 42 | func (c *NotImplementedClient) ShowMessage(context.Context, *protocol.ShowMessageParams) error { 43 | return fmt.Errorf("ShowMessage not implemented") 44 | } 45 | 46 | func (c *NotImplementedClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { 47 | return fmt.Errorf("LogMessage not implemented") 48 | } 49 | 50 | // window/showDocument 51 | func (c *NotImplementedClient) ShowDocument(context.Context, *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { 52 | return nil, fmt.Errorf("window/showDocument not implemented") 53 | } 54 | 55 | func (c *NotImplementedClient) Event(context.Context, *interface{}) error { 56 | return fmt.Errorf("Event not implemented") 57 | } 58 | 59 | func (c *NotImplementedClient) PublishDiagnostics(context.Context, *protocol.PublishDiagnosticsParams) error { 60 | return fmt.Errorf("PublishDiagnostics not implemented") 61 | } 62 | 63 | func (c *NotImplementedClient) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) { 64 | return nil, fmt.Errorf("WorkspaceFolders not implemented") 65 | } 66 | 67 | func (c *NotImplementedClient) Configuration(context.Context, *protocol.ParamConfiguration) ([]interface{}, error) { 68 | return nil, fmt.Errorf("Configuration not implemented") 69 | } 70 | 71 | func (c *NotImplementedClient) RegisterCapability(context.Context, *protocol.RegistrationParams) error { 72 | return fmt.Errorf("RegisterCapability not implemented") 73 | } 74 | 75 | func (c *NotImplementedClient) UnregisterCapability(context.Context, *protocol.UnregistrationParams) error { 76 | return fmt.Errorf("UnregisterCapability not implemented") 77 | } 78 | 79 | func (c *NotImplementedClient) ShowMessageRequest(context.Context, *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { 80 | return nil, fmt.Errorf("ShowMessageRequest not implemented") 81 | } 82 | 83 | // window/workDoneProgress/create 84 | func (c *NotImplementedClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error { 85 | return fmt.Errorf("window/workDoneProgress/create not implemented") 86 | } 87 | 88 | func (c *NotImplementedClient) ApplyEdit(context.Context, *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { 89 | return nil, fmt.Errorf("ApplyEdit not implemented") 90 | } 91 | 92 | // workspace/codeLens/refresh 93 | func (c *NotImplementedClient) CodeLensRefresh(context.Context) error { 94 | return fmt.Errorf("workspace/codeLens/refresh not implemented") 95 | } 96 | 97 | // workspace/diagnostic/refresh 98 | func (c *NotImplementedClient) DiagnosticRefresh(context.Context) error { 99 | return fmt.Errorf("workspace/diagnostic/refresh not implemented") 100 | } 101 | 102 | // workspace/inlayHint/refresh 103 | func (c *NotImplementedClient) InlayHintRefresh(context.Context) error { 104 | return fmt.Errorf("workspace/inlayHint/refresh not implemented") 105 | } 106 | 107 | // workspace/inlineValue/refresh 108 | func (c *NotImplementedClient) InlineValueRefresh(context.Context) error { 109 | return fmt.Errorf("workspace/inlineValue/refresh not implemented") 110 | } 111 | 112 | // workspace/semanticTokens/refresh 113 | func (c *NotImplementedClient) SemanticTokensRefresh(context.Context) error { 114 | return fmt.Errorf("workspace/semanticTokens/refresh not implemented") 115 | } 116 | -------------------------------------------------------------------------------- /internal/lsp/proxy/context.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type contextKey int 8 | 9 | const ( 10 | clientKey = contextKey(iota) 11 | ) 12 | 13 | func WithClient(ctx context.Context, client Client) context.Context { 14 | return context.WithValue(ctx, clientKey, client) 15 | } 16 | -------------------------------------------------------------------------------- /internal/lsp/proxy/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package proxy implements the protocol between acme-lsp and L commands. 6 | package proxy 7 | -------------------------------------------------------------------------------- /internal/lsp/proxy/protocol.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package proxy 6 | 7 | import ( 8 | "context" 9 | "log" 10 | 11 | "9fans.net/internal/go-lsp/lsp/protocol" 12 | "github.com/sourcegraph/jsonrpc2" 13 | ) 14 | 15 | type DocumentUri = string 16 | 17 | type clientHandler struct { 18 | client Client 19 | } 20 | 21 | func (h *clientHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Request) { 22 | if Debug { 23 | log.Printf("proxy: client handler %v\n", r.Method) 24 | } 25 | ok, err := clientDispatch(ctx, h.client, conn, r) 26 | if !ok { 27 | ok, err = protocol.ClientDispatch(ctx, h.client, conn, r) 28 | } 29 | if !ok { 30 | rpcerr := &jsonrpc2.Error{ 31 | Code: jsonrpc2.CodeMethodNotFound, 32 | Message: "method not implemented", 33 | } 34 | err = conn.Reply(ctx, r.ID, rpcerr) 35 | } 36 | if err != nil { 37 | log.Printf("proxy: client rpc reply failed for %v: %v", r.Method, err) 38 | } 39 | } 40 | 41 | func NewClientHandler(client Client) jsonrpc2.Handler { 42 | return &clientHandler{ 43 | client: client, 44 | } 45 | } 46 | 47 | type serverHandler struct { 48 | server Server 49 | } 50 | 51 | func (h *serverHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Request) { 52 | if Debug { 53 | log.Printf("proxy: server handler %v\n", r.Method) 54 | } 55 | ok, err := serverDispatch(ctx, h.server, conn, r) 56 | if !ok { 57 | ok, err = protocol.ServerDispatch(ctx, h.server, conn, r) 58 | } 59 | if !ok { 60 | rpcerr := &jsonrpc2.Error{ 61 | Code: jsonrpc2.CodeMethodNotFound, 62 | Message: "method not implemented", 63 | } 64 | err = conn.Reply(ctx, r.ID, rpcerr) 65 | } 66 | if err != nil { 67 | log.Printf("proxy: server rpc reply failed for %v: %v", r.Method, err) 68 | } 69 | } 70 | 71 | func NewServerHandler(server Server) jsonrpc2.Handler { 72 | return &serverHandler{ 73 | server: server, 74 | } 75 | } 76 | 77 | func NewClient(conn *jsonrpc2.Conn) Client { 78 | return &clientDispatcher{ 79 | Conn: conn, 80 | Client: protocol.NewClient(conn), 81 | } 82 | } 83 | 84 | func NewServer(conn *jsonrpc2.Conn) Server { 85 | return &serverDispatcher{ 86 | Conn: conn, 87 | Server: protocol.NewServer(conn), 88 | } 89 | } 90 | 91 | func reply(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID, result interface{}, err error) error { 92 | if err != nil { 93 | rpcerr := &jsonrpc2.Error{ 94 | Code: jsonrpc2.CodeInternalError, 95 | Message: err.Error(), 96 | } 97 | return conn.ReplyWithError(ctx, id, rpcerr) 98 | } 99 | return conn.Reply(ctx, id, result) 100 | } 101 | 102 | func sendParseError(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID, err error) error { 103 | rpcerr := &jsonrpc2.Error{ 104 | Code: jsonrpc2.CodeParseError, 105 | Message: err.Error(), 106 | } 107 | return conn.ReplyWithError(ctx, id, rpcerr) 108 | } 109 | 110 | type ExecuteCommandOnDocumentParams struct { 111 | TextDocument protocol.TextDocumentIdentifier 112 | ExecuteCommandParams protocol.ExecuteCommandParams 113 | } 114 | -------------------------------------------------------------------------------- /internal/lsp/proxy/server.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "9fans.net/internal/go-lsp/lsp/protocol" 9 | "github.com/sourcegraph/jsonrpc2" 10 | ) 11 | 12 | // Version is used to detect if acme-lsp and L are speaking the same protocol. 13 | const Version = 1 14 | 15 | // Server implements a subset of an LSP protocol server as defined by protocol.Server and 16 | // some custom acme-lsp specific methods. 17 | type Server interface { 18 | // Version returns the protocol version. 19 | Version(context.Context) (int, error) 20 | 21 | // WorkspaceFolders returns the workspace folders currently being managed by acme-lsp. 22 | // In LSP, this method is implemented by the client, but in our case acme-lsp is managing 23 | // the workspace folders, so this has to be implemented by the acme-lsp proxy server. 24 | WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) 25 | 26 | // InitializeResult returns the initialize response from the LSP server. 27 | // This is useful for L command to get initialization results (e.g. server capabilities) 28 | // of an already initialized LSP server. 29 | InitializeResult(context.Context, *protocol.TextDocumentIdentifier) (*protocol.InitializeResult, error) 30 | 31 | // ExecuteCommandOnDocument is the same as ExecuteCommand, but 32 | // params contain the TextDocumentIdentifier of the original 33 | // CodeAction so that the server implemention can multiplex 34 | // ExecuteCommand request to the right server. 35 | ExecuteCommandOnDocument(context.Context, *ExecuteCommandOnDocumentParams) (interface{}, error) 36 | 37 | protocol.Server 38 | //DidChange(context.Context, *protocol.DidChangeTextDocumentParams) error 39 | //DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error 40 | //Completion(context.Context, *protocol.CompletionParams) (*protocol.CompletionList, error) 41 | //Definition(context.Context, *protocol.DefinitionParams) ([]protocol.Location, error) 42 | //Formatting(context.Context, *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) 43 | //CodeAction(context.Context, *protocol.CodeActionParams) ([]protocol.CodeAction, error) 44 | //Hover(context.Context, *protocol.HoverParams) (*protocol.Hover, error) 45 | //Implementation(context.Context, *protocol.ImplementationParams) ([]protocol.Location, error) 46 | //References(context.Context, *protocol.ReferenceParams) ([]protocol.Location, error) 47 | //Rename(context.Context, *protocol.RenameParams) (*protocol.WorkspaceEdit, error) 48 | //SignatureHelp(context.Context, *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) 49 | //DocumentSymbol(context.Context, *protocol.DocumentSymbolParams) ([]interface{}, error) 50 | //TypeDefinition(context.Context, *protocol.TypeDefinitionParams) ([]protocol.Location, error) 51 | } 52 | 53 | func serverDispatch(ctx context.Context, server Server, conn *jsonrpc2.Conn, r *jsonrpc2.Request) (bool, error) { 54 | switch r.Method { 55 | case "acme-lsp/version": // req 56 | resp, err := server.Version(ctx) 57 | return true, reply(ctx, conn, r.ID, resp, err) 58 | 59 | case "acme-lsp/workspaceFolders": // req 60 | resp, err := server.WorkspaceFolders(ctx) 61 | return true, reply(ctx, conn, r.ID, resp, err) 62 | 63 | case "acme-lsp/initializeResult": // req 64 | var params protocol.TextDocumentIdentifier 65 | if err := json.Unmarshal(*r.Params, ¶ms); err != nil { 66 | return true, sendParseError(ctx, conn, r.ID, err) 67 | } 68 | resp, err := server.InitializeResult(ctx, ¶ms) 69 | return true, reply(ctx, conn, r.ID, resp, err) 70 | 71 | case "acme-lsp/executeCommandOnDocument": // req 72 | var params ExecuteCommandOnDocumentParams 73 | if err := json.Unmarshal(*r.Params, ¶ms); err != nil { 74 | return true, sendParseError(ctx, conn, r.ID, err) 75 | } 76 | resp, err := server.ExecuteCommandOnDocument(ctx, ¶ms) 77 | return true, reply(ctx, conn, r.ID, resp, err) 78 | 79 | default: 80 | return false, nil 81 | } 82 | } 83 | 84 | var _ Server = (*serverDispatcher)(nil) 85 | 86 | // serverDispatcher extends a protocol.Server with our custom messages. 87 | type serverDispatcher struct { 88 | *jsonrpc2.Conn 89 | protocol.Server 90 | } 91 | 92 | func (s *serverDispatcher) Version(ctx context.Context) (int, error) { 93 | var result int 94 | if err := s.Conn.Call(ctx, "acme-lsp/version", nil, &result); err != nil { 95 | return 0, err 96 | } 97 | return result, nil 98 | } 99 | 100 | func (s *serverDispatcher) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) { 101 | var result []protocol.WorkspaceFolder 102 | if err := s.Conn.Call(ctx, "acme-lsp/workspaceFolders", nil, &result); err != nil { 103 | return nil, err 104 | } 105 | return result, nil 106 | } 107 | 108 | func (s *serverDispatcher) InitializeResult(ctx context.Context, params *protocol.TextDocumentIdentifier) (*protocol.InitializeResult, error) { 109 | var result protocol.InitializeResult 110 | if err := s.Conn.Call(ctx, "acme-lsp/initializeResult", params, &result); err != nil { 111 | return nil, err 112 | } 113 | return &result, nil 114 | } 115 | 116 | func (s *serverDispatcher) ExecuteCommandOnDocument(ctx context.Context, params *ExecuteCommandOnDocumentParams) (interface{}, error) { 117 | var result interface{} 118 | if err := s.Conn.Call(ctx, "acme-lsp/executeCommandOnDocument", params, &result); err != nil { 119 | return nil, err 120 | } 121 | return result, nil 122 | } 123 | 124 | var _ protocol.Server = (*NotImplementedServer)(nil) 125 | 126 | // NotImplementedServer is a stub implementation of protocol.Server. 127 | type NotImplementedServer struct{} 128 | 129 | func (s *NotImplementedServer) Progress(context.Context, *protocol.ProgressParams) error { 130 | return fmt.Errorf("not implemented") 131 | } 132 | func (s *NotImplementedServer) SetTrace(context.Context, *protocol.SetTraceParams) error { 133 | return fmt.Errorf("not implemented") 134 | } 135 | func (s *NotImplementedServer) IncomingCalls(context.Context, *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) { 136 | return nil, fmt.Errorf("not implemented") 137 | } 138 | func (s *NotImplementedServer) OutgoingCalls(context.Context, *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) { 139 | return nil, fmt.Errorf("not implemented") 140 | } 141 | func (s *NotImplementedServer) ResolveCodeAction(context.Context, *protocol.CodeAction) (*protocol.CodeAction, error) { 142 | return nil, fmt.Errorf("not implemented") 143 | } 144 | func (s *NotImplementedServer) ResolveCodeLens(context.Context, *protocol.CodeLens) (*protocol.CodeLens, error) { 145 | return nil, fmt.Errorf("not implemented") 146 | } 147 | func (s *NotImplementedServer) ResolveCompletionItem(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) { 148 | return nil, fmt.Errorf("not implemented") 149 | } 150 | func (s *NotImplementedServer) ResolveDocumentLink(context.Context, *protocol.DocumentLink) (*protocol.DocumentLink, error) { 151 | return nil, fmt.Errorf("not implemented") 152 | } 153 | func (s *NotImplementedServer) Exit(context.Context) error { return fmt.Errorf("not implemented") } 154 | func (s *NotImplementedServer) Initialize(context.Context, *protocol.ParamInitialize) (*protocol.InitializeResult, error) { 155 | return nil, fmt.Errorf("not implemented") 156 | } 157 | func (s *NotImplementedServer) Initialized(context.Context, *protocol.InitializedParams) error { 158 | return fmt.Errorf("not implemented") 159 | } 160 | func (s *NotImplementedServer) Resolve(context.Context, *protocol.InlayHint) (*protocol.InlayHint, error) { 161 | return nil, fmt.Errorf("not implemented") 162 | } 163 | func (s *NotImplementedServer) DidChangeNotebookDocument(context.Context, *protocol.DidChangeNotebookDocumentParams) error { 164 | return fmt.Errorf("not implemented") 165 | } 166 | func (s *NotImplementedServer) DidCloseNotebookDocument(context.Context, *protocol.DidCloseNotebookDocumentParams) error { 167 | return fmt.Errorf("not implemented") 168 | } 169 | func (s *NotImplementedServer) DidOpenNotebookDocument(context.Context, *protocol.DidOpenNotebookDocumentParams) error { 170 | return fmt.Errorf("not implemented") 171 | } 172 | func (s *NotImplementedServer) DidSaveNotebookDocument(context.Context, *protocol.DidSaveNotebookDocumentParams) error { 173 | return fmt.Errorf("not implemented") 174 | } 175 | func (s *NotImplementedServer) Shutdown(context.Context) error { return fmt.Errorf("not implemented") } 176 | func (s *NotImplementedServer) CodeAction(context.Context, *protocol.CodeActionParams) ([]protocol.CodeAction, error) { 177 | return nil, fmt.Errorf("not implemented") 178 | } 179 | func (s *NotImplementedServer) CodeLens(context.Context, *protocol.CodeLensParams) ([]protocol.CodeLens, error) { 180 | return nil, fmt.Errorf("not implemented") 181 | } 182 | func (s *NotImplementedServer) ColorPresentation(context.Context, *protocol.ColorPresentationParams) ([]protocol.ColorPresentation, error) { 183 | return nil, fmt.Errorf("not implemented") 184 | } 185 | func (s *NotImplementedServer) Completion(context.Context, *protocol.CompletionParams) (*protocol.CompletionList, error) { 186 | return nil, fmt.Errorf("not implemented") 187 | } 188 | func (s *NotImplementedServer) Declaration(context.Context, *protocol.DeclarationParams) (*protocol.Or_textDocument_declaration, error) { 189 | return nil, fmt.Errorf("not implemented") 190 | } 191 | func (s *NotImplementedServer) Definition(context.Context, *protocol.DefinitionParams) ([]protocol.Location, error) { 192 | return nil, fmt.Errorf("not implemented") 193 | } 194 | func (s *NotImplementedServer) Diagnostic(context.Context, *string) (*string, error) { 195 | return nil, fmt.Errorf("not implemented") 196 | } 197 | func (s *NotImplementedServer) DidChange(context.Context, *protocol.DidChangeTextDocumentParams) error { 198 | return fmt.Errorf("not implemented") 199 | } 200 | func (s *NotImplementedServer) DidClose(context.Context, *protocol.DidCloseTextDocumentParams) error { 201 | return fmt.Errorf("not implemented") 202 | } 203 | func (s *NotImplementedServer) DidOpen(context.Context, *protocol.DidOpenTextDocumentParams) error { 204 | return fmt.Errorf("not implemented") 205 | } 206 | func (s *NotImplementedServer) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) error { 207 | return fmt.Errorf("not implemented") 208 | } 209 | func (s *NotImplementedServer) DocumentColor(context.Context, *protocol.DocumentColorParams) ([]protocol.ColorInformation, error) { 210 | return nil, fmt.Errorf("not implemented") 211 | } 212 | func (s *NotImplementedServer) DocumentHighlight(context.Context, *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { 213 | return nil, fmt.Errorf("not implemented") 214 | } 215 | func (s *NotImplementedServer) DocumentLink(context.Context, *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) { 216 | return nil, fmt.Errorf("not implemented") 217 | } 218 | func (s *NotImplementedServer) DocumentSymbol(context.Context, *protocol.DocumentSymbolParams) ([]interface{}, error) { 219 | return nil, fmt.Errorf("not implemented") 220 | } 221 | func (s *NotImplementedServer) FoldingRange(context.Context, *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { 222 | return nil, fmt.Errorf("not implemented") 223 | } 224 | func (s *NotImplementedServer) Formatting(context.Context, *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { 225 | return nil, fmt.Errorf("not implemented") 226 | } 227 | func (s *NotImplementedServer) Hover(context.Context, *protocol.HoverParams) (*protocol.Hover, error) { 228 | return nil, fmt.Errorf("not implemented") 229 | } 230 | func (s *NotImplementedServer) Implementation(context.Context, *protocol.ImplementationParams) ([]protocol.Location, error) { 231 | return nil, fmt.Errorf("not implemented") 232 | } 233 | func (s *NotImplementedServer) InlayHint(context.Context, *protocol.InlayHintParams) ([]protocol.InlayHint, error) { 234 | return nil, fmt.Errorf("not implemented") 235 | } 236 | func (s *NotImplementedServer) InlineValue(context.Context, *protocol.InlineValueParams) ([]protocol.InlineValue, error) { 237 | return nil, fmt.Errorf("not implemented") 238 | } 239 | func (s *NotImplementedServer) LinkedEditingRange(context.Context, *protocol.LinkedEditingRangeParams) (*protocol.LinkedEditingRanges, error) { 240 | return nil, fmt.Errorf("not implemented") 241 | } 242 | func (s *NotImplementedServer) Moniker(context.Context, *protocol.MonikerParams) ([]protocol.Moniker, error) { 243 | return nil, fmt.Errorf("not implemented") 244 | } 245 | func (s *NotImplementedServer) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) { 246 | return nil, fmt.Errorf("not implemented") 247 | } 248 | func (s *NotImplementedServer) PrepareCallHierarchy(context.Context, *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) { 249 | return nil, fmt.Errorf("not implemented") 250 | } 251 | func (s *NotImplementedServer) PrepareRename(context.Context, *protocol.PrepareRenameParams) (*protocol.PrepareRename2Gn, error) { 252 | return nil, fmt.Errorf("not implemented") 253 | } 254 | func (s *NotImplementedServer) PrepareTypeHierarchy(context.Context, *protocol.TypeHierarchyPrepareParams) ([]protocol.TypeHierarchyItem, error) { 255 | return nil, fmt.Errorf("not implemented") 256 | } 257 | func (s *NotImplementedServer) RangeFormatting(context.Context, *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { 258 | return nil, fmt.Errorf("not implemented") 259 | } 260 | func (s *NotImplementedServer) References(context.Context, *protocol.ReferenceParams) ([]protocol.Location, error) { 261 | return nil, fmt.Errorf("not implemented") 262 | } 263 | func (s *NotImplementedServer) Rename(context.Context, *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { 264 | return nil, fmt.Errorf("not implemented") 265 | } 266 | func (s *NotImplementedServer) SelectionRange(context.Context, *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { 267 | return nil, fmt.Errorf("not implemented") 268 | } 269 | func (s *NotImplementedServer) SemanticTokensFull(context.Context, *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { 270 | return nil, fmt.Errorf("not implemented") 271 | } 272 | func (s *NotImplementedServer) SemanticTokensFullDelta(context.Context, *protocol.SemanticTokensDeltaParams) (interface{}, error) { 273 | return nil, fmt.Errorf("not implemented") 274 | } 275 | func (s *NotImplementedServer) SemanticTokensRange(context.Context, *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { 276 | return nil, fmt.Errorf("not implemented") 277 | } 278 | func (s *NotImplementedServer) SignatureHelp(context.Context, *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { 279 | return nil, fmt.Errorf("not implemented") 280 | } 281 | func (s *NotImplementedServer) TypeDefinition(context.Context, *protocol.TypeDefinitionParams) ([]protocol.Location, error) { 282 | return nil, fmt.Errorf("not implemented") 283 | } 284 | func (s *NotImplementedServer) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error { 285 | return fmt.Errorf("not implemented") 286 | } 287 | func (s *NotImplementedServer) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocumentParams) ([]protocol.TextEdit, error) { 288 | return nil, fmt.Errorf("not implemented") 289 | } 290 | func (s *NotImplementedServer) Subtypes(context.Context, *protocol.TypeHierarchySubtypesParams) ([]protocol.TypeHierarchyItem, error) { 291 | return nil, fmt.Errorf("not implemented") 292 | } 293 | func (s *NotImplementedServer) Supertypes(context.Context, *protocol.TypeHierarchySupertypesParams) ([]protocol.TypeHierarchyItem, error) { 294 | return nil, fmt.Errorf("not implemented") 295 | } 296 | func (s *NotImplementedServer) WorkDoneProgressCancel(context.Context, *protocol.WorkDoneProgressCancelParams) error { 297 | return fmt.Errorf("not implemented") 298 | } 299 | func (s *NotImplementedServer) DiagnosticWorkspace(context.Context, *protocol.WorkspaceDiagnosticParams) (*protocol.WorkspaceDiagnosticReport, error) { 300 | return nil, fmt.Errorf("not implemented") 301 | } 302 | func (s *NotImplementedServer) DidChangeConfiguration(context.Context, *protocol.DidChangeConfigurationParams) error { 303 | return fmt.Errorf("not implemented") 304 | } 305 | func (s *NotImplementedServer) DidChangeWatchedFiles(context.Context, *protocol.DidChangeWatchedFilesParams) error { 306 | return fmt.Errorf("not implemented") 307 | } 308 | func (s *NotImplementedServer) DidChangeWorkspaceFolders(context.Context, *protocol.DidChangeWorkspaceFoldersParams) error { 309 | return fmt.Errorf("not implemented") 310 | } 311 | func (s *NotImplementedServer) DidCreateFiles(context.Context, *protocol.CreateFilesParams) error { 312 | return fmt.Errorf("not implemented") 313 | } 314 | func (s *NotImplementedServer) DidDeleteFiles(context.Context, *protocol.DeleteFilesParams) error { 315 | return fmt.Errorf("not implemented") 316 | } 317 | func (s *NotImplementedServer) DidRenameFiles(context.Context, *protocol.RenameFilesParams) error { 318 | return fmt.Errorf("not implemented") 319 | } 320 | func (s *NotImplementedServer) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) (interface{}, error) { 321 | return nil, fmt.Errorf("not implemented") 322 | } 323 | func (s *NotImplementedServer) Symbol(context.Context, *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { 324 | return nil, fmt.Errorf("not implemented") 325 | } 326 | func (s *NotImplementedServer) WillCreateFiles(context.Context, *protocol.CreateFilesParams) (*protocol.WorkspaceEdit, error) { 327 | return nil, fmt.Errorf("not implemented") 328 | } 329 | func (s *NotImplementedServer) WillDeleteFiles(context.Context, *protocol.DeleteFilesParams) (*protocol.WorkspaceEdit, error) { 330 | return nil, fmt.Errorf("not implemented") 331 | } 332 | func (s *NotImplementedServer) WillRenameFiles(context.Context, *protocol.RenameFilesParams) (*protocol.WorkspaceEdit, error) { 333 | return nil, fmt.Errorf("not implemented") 334 | } 335 | func (s *NotImplementedServer) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymbol) (*protocol.WorkspaceSymbol, error) { 336 | return nil, fmt.Errorf("not implemented") 337 | } 338 | func (s *NotImplementedServer) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { 339 | return nil, fmt.Errorf("not implemented") 340 | } 341 | -------------------------------------------------------------------------------- /internal/lsp/text/edit.go: -------------------------------------------------------------------------------- 1 | // Package text implements text editing helper routines for LSP. 2 | package text 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strings" 9 | 10 | "9fans.net/internal/go-lsp/lsp/protocol" 11 | ) 12 | 13 | // File represents an open file in text editor. 14 | type File interface { 15 | // Reader returns a reader for the entire file text buffer ("body" in acme). 16 | Reader() (io.Reader, error) 17 | 18 | // WriteAt replaces the text in rune range [q0, q1) with bytes b. 19 | WriteAt(q0, q1 int, b []byte) (int, error) 20 | 21 | // Mark marks the file for later undo. 22 | Mark() error 23 | 24 | // DisableMark turns off automatic marking (e.g. generated by WriteAt). 25 | DisableMark() error 26 | } 27 | 28 | type EditList []protocol.TextEdit 29 | 30 | func (l EditList) Len() int { return len(l) } 31 | func (l EditList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 32 | func (l EditList) Less(i, j int) bool { 33 | if l[i].Range.Start.Line == l[j].Range.Start.Line { 34 | return l[i].Range.Start.Character < l[j].Range.Start.Character 35 | } 36 | return l[i].Range.Start.Line < l[j].Range.Start.Line 37 | } 38 | 39 | // Edit applied edits to file f. 40 | func Edit(f File, edits []protocol.TextEdit) error { 41 | if len(edits) == 0 { 42 | return nil 43 | } 44 | reader, err := f.Reader() 45 | if err != nil { 46 | return err 47 | } 48 | off, err := getNewlineOffsets(reader) 49 | if err != nil { 50 | return fmt.Errorf("failed to obtain newline offsets: %v", err) 51 | } 52 | 53 | f.DisableMark() 54 | f.Mark() 55 | 56 | // Make sure edits follow the sequence from beginning to end 57 | // See comments below. 58 | sort.Sort(EditList(edits)) 59 | 60 | // Applying the edits in reverse order gets the job done. 61 | // See https://github.com/golang/go/wiki/gopls#textdocumentformatting-response 62 | for i := len(edits) - 1; i >= 0; i-- { 63 | e := edits[i] 64 | q0 := off.LineToOffset(int(e.Range.Start.Line), int(e.Range.Start.Character)) 65 | q1 := off.LineToOffset(int(e.Range.End.Line), int(e.Range.End.Character)) 66 | f.WriteAt(q0, q1, []byte(e.NewText)) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // AddressableFile represents an open file in text editor which has a current adddress. 73 | type AddressableFile interface { 74 | File 75 | 76 | // Filename returns the filesystem path to the file. 77 | Filename() (string, error) 78 | 79 | // CurrentAddr returns the address of current selection. 80 | CurrentAddr() (q0, q1 int, err error) 81 | } 82 | 83 | // DocumentURI returns the URI and filename of a file being edited. 84 | func DocumentURI(f AddressableFile) (uri protocol.DocumentURI, filename string, err error) { 85 | name, err := f.Filename() 86 | if err != nil { 87 | return "", "", err 88 | } 89 | return ToURI(name), name, nil 90 | } 91 | 92 | // Position returns the current position within a file being edited. 93 | func Position(f AddressableFile) (pos *protocol.TextDocumentPositionParams, filename string, err error) { 94 | name, err := f.Filename() 95 | if err != nil { 96 | return nil, "", fmt.Errorf("could not get window filename: %v", err) 97 | } 98 | q0, _, err := f.CurrentAddr() 99 | if err != nil { 100 | return nil, "", fmt.Errorf("could not get current address: %v", err) 101 | } 102 | reader, err := f.Reader() 103 | if err != nil { 104 | return nil, "", fmt.Errorf("could not get window body reader: %v", err) 105 | } 106 | off, err := getNewlineOffsets(reader) 107 | if err != nil { 108 | return nil, "", fmt.Errorf("failed to get newline offset: %v", err) 109 | } 110 | line, col := off.OffsetToLine(q0) 111 | return &protocol.TextDocumentPositionParams{ 112 | TextDocument: protocol.TextDocumentIdentifier{ 113 | URI: ToURI(name), 114 | }, 115 | Position: protocol.Position{ 116 | Line: uint32(line), 117 | Character: uint32(col), 118 | }, 119 | }, name, nil 120 | } 121 | 122 | // ToURI converts filename to URI. 123 | func ToURI(filename string) protocol.DocumentURI { 124 | return protocol.DocumentURI("file://" + filename) 125 | } 126 | 127 | // ToPath converts URI to filename. 128 | func ToPath(uri protocol.DocumentURI) string { 129 | filename, _ := CutPrefix(string(uri), "file://") 130 | return filename 131 | } 132 | 133 | // CutPrefix returns s without the provided leading prefix string 134 | // and reports whether it found the prefix. 135 | // If s doesn't start with prefix, CutPrefix returns s, false. 136 | // If prefix is the empty string, CutPrefix returns s, true. 137 | // 138 | // TODO(fhs): remove and use strings.CutPrefix in Go >= 1.20 139 | func CutPrefix(s, prefix string) (after string, found bool) { 140 | if !strings.HasPrefix(s, prefix) { 141 | return s, false 142 | } 143 | return s[len(prefix):], true 144 | } 145 | -------------------------------------------------------------------------------- /internal/lsp/text/edit_test.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "9fans.net/internal/go-lsp/lsp/protocol" 8 | ) 9 | 10 | func TestURI(t *testing.T) { 11 | if runtime.GOOS == "windows" { 12 | t.Skip("TODO: failing on windows due to file path issues") 13 | } 14 | 15 | for _, tc := range []struct { 16 | name string 17 | uri protocol.DocumentURI 18 | }{ 19 | {"/home/gopher/hello.go", "file:///home/gopher/hello.go"}, 20 | } { 21 | uri := ToURI(tc.name) 22 | if uri != tc.uri { 23 | t.Errorf("ToURI(%q) is %q; expected %q", tc.name, uri, tc.uri) 24 | } 25 | name := ToPath(tc.uri) 26 | if name != tc.name { 27 | t.Errorf("ToPath(%q) is %q; expected %q", tc.uri, name, tc.name) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/lsp/text/line.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "unicode/utf8" 7 | ) 8 | 9 | // TODO(fhs): Maybe replace this with https://godoc.org/golang.org/x/tools/internal/span ? 10 | // LSP deals with UTF-16, but acme deals with runes, so out implementation here 11 | // may not be entirely accurate. 12 | 13 | type nlOffsets struct { 14 | nl []int // rune offsets of '\n' 15 | leftover int // runes leftover after last '\n' 16 | } 17 | 18 | func getNewlineOffsets(r io.Reader) (*nlOffsets, error) { 19 | br := bufio.NewReader(r) 20 | o := 0 21 | nl := []int{0} 22 | leftover := 0 23 | for { 24 | line, err := br.ReadString('\n') 25 | if err != nil && err != io.EOF { 26 | return nil, err 27 | } 28 | if err == io.EOF { 29 | leftover = len(line) 30 | break 31 | } 32 | o += utf8.RuneCountInString(line) 33 | nl = append(nl, o) 34 | } 35 | return &nlOffsets{ 36 | nl: nl, 37 | leftover: leftover, 38 | }, nil 39 | } 40 | 41 | // LineToOffset returns the rune offset within the file given the 42 | // line number and rune offset within the line. 43 | func (off *nlOffsets) LineToOffset(line, col int) int { 44 | eof := off.nl[len(off.nl)-1] + off.leftover 45 | if line >= len(off.nl) { 46 | // beyond EOF, so just return the highest offset 47 | return eof 48 | } 49 | o := off.nl[line] + col 50 | if o > eof { 51 | o = eof 52 | } 53 | return o 54 | } 55 | 56 | // OffsetToLine returns the line number and rune offset within the line 57 | // given rune offset within the file. 58 | func (off *nlOffsets) OffsetToLine(offset int) (line, col int) { 59 | for i, o := range off.nl { 60 | if o > offset { 61 | return i - 1, offset - off.nl[i-1] 62 | } 63 | } 64 | if i := len(off.nl) - 1; offset >= off.nl[i] { 65 | return i, offset - off.nl[i] 66 | } 67 | panic("unreachable") 68 | } 69 | -------------------------------------------------------------------------------- /internal/lsp/text/line_test.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | const testFile1 = `123 9 | 56αβ9 10 | 11 | CDE 12 | ` 13 | 14 | // testFile2 describes a file that does not end with '\n' 15 | const testFile2 = `12345 16 | 678` 17 | 18 | const testFile3 = `hello` 19 | 20 | func TestLineOffsets(t *testing.T) { 21 | var testCases = []struct { 22 | file string 23 | offset, line, col int 24 | }{ 25 | {testFile1, 0x0, 0, 0}, 26 | {testFile1, 0x1, 0, 1}, 27 | {testFile1, 0x2, 0, 2}, 28 | {testFile1, 0x3, 0, 3}, 29 | {testFile1, 0x4, 1, 0}, 30 | {testFile1, 0x5, 1, 1}, 31 | {testFile1, 0x6, 1, 2}, 32 | {testFile1, 0x7, 1, 3}, 33 | {testFile1, 0x8, 1, 4}, 34 | {testFile1, 0x9, 1, 5}, 35 | {testFile1, 0xA, 2, 0}, 36 | {testFile1, 0xB, 3, 0}, 37 | {testFile1, 0xC, 3, 1}, 38 | {testFile1, 0xD, 3, 2}, 39 | {testFile1, 0xE, 3, 3}, 40 | {testFile1, 0xF, 4, 0}, 41 | {testFile2, 0x5, 0, 5}, 42 | {testFile2, 0x6, 1, 0}, 43 | {testFile2, 0x7, 1, 1}, 44 | {testFile2, 0x8, 1, 2}, 45 | {testFile2, 0x9, 1, 3}, 46 | {testFile3, 0x0, 0, 0}, 47 | {testFile3, 0x1, 0, 1}, 48 | {testFile3, 0x5, 0, 5}, 49 | } 50 | 51 | for _, tc := range testCases { 52 | off, err := getNewlineOffsets(bytes.NewBufferString(tc.file)) 53 | if err != nil { 54 | t.Errorf("failed to compute file offsets: %v", err) 55 | continue 56 | } 57 | if o := off.LineToOffset(tc.line, tc.col); o != tc.offset { 58 | t.Errorf("LineToOffset(%v, %v) = %v for off=%v; expected %v\n", 59 | tc.line, tc.col, o, off, tc.offset) 60 | } 61 | if line, col := off.OffsetToLine(tc.offset); line != tc.line || col != tc.col { 62 | t.Errorf("OffsetToLine(%v) = %v, %v for off=%v; expected %v, %v\n", 63 | tc.offset, line, col, off, tc.line, tc.col) 64 | } 65 | } 66 | } 67 | 68 | func TestLineOffsetsLeftover(t *testing.T) { 69 | var testCases = []struct { 70 | file string 71 | offset, line, col int 72 | }{ 73 | {testFile2, 0x9, 1, 4}, 74 | {testFile2, 0x9, 2, 0}, 75 | {testFile2, 0x9, 2, 1}, 76 | {testFile2, 0x9, 2, 2}, 77 | {testFile2, 0x9, 3, 0}, 78 | {testFile2, 0x9, 3, 1}, 79 | {testFile2, 0x9, 3, 2}, 80 | {testFile2, 0x9, 4, 0}, 81 | {testFile2, 0x9, 4, 1}, 82 | {testFile2, 0x9, 4, 2}, 83 | {testFile3, 0x5, 0, 6}, 84 | {testFile3, 0x5, 0, 7}, 85 | {testFile3, 0x5, 1, 0}, 86 | {testFile3, 0x5, 1, 2}, 87 | } 88 | 89 | for _, tc := range testCases { 90 | off, err := getNewlineOffsets(bytes.NewBufferString(tc.file)) 91 | if err != nil { 92 | t.Errorf("failed to compute file offsets: %v", err) 93 | continue 94 | } 95 | if o := off.LineToOffset(tc.line, tc.col); o != tc.offset { 96 | t.Errorf("LineToOffset(%v, %v) = %v for off=%v; expected %v\n", 97 | tc.line, tc.col, o, off, tc.offset) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/lsp/utils.go: -------------------------------------------------------------------------------- 1 | // Package lsp contains Language Server Protocol utility functions. 2 | package lsp 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "path/filepath" 10 | "sync" 11 | 12 | "9fans.net/acme-lsp/internal/lsp/text" 13 | "9fans.net/internal/go-lsp/lsp/protocol" 14 | "github.com/sourcegraph/jsonrpc2" 15 | ) 16 | 17 | func ServerProvidesCodeAction(cap *protocol.ServerCapabilities, kind protocol.CodeActionKind) bool { 18 | switch ap := cap.CodeActionProvider.(type) { 19 | case bool: 20 | return ap 21 | case protocol.CodeActionOptions: 22 | for _, k := range ap.CodeActionKinds { 23 | if k == kind { 24 | return true 25 | } 26 | } 27 | } 28 | return false 29 | } 30 | 31 | func CompatibleCodeActions(cap *protocol.ServerCapabilities, kinds []protocol.CodeActionKind) []protocol.CodeActionKind { 32 | var allowed []protocol.CodeActionKind 33 | switch ap := cap.CodeActionProvider.(type) { 34 | default: 35 | log.Printf("CompatibleCodeActions: unexpected CodeActionProvider type %T", ap) 36 | case bool: 37 | if ap { 38 | return kinds 39 | } 40 | return nil 41 | case protocol.CodeActionOptions: 42 | allowed = ap.CodeActionKinds 43 | case map[string]any: 44 | as, ok := ap["codeActionKinds"].([]any) 45 | if !ok { 46 | log.Printf("codeActionKinds is %T", ap["codeActionKinds"]) 47 | break 48 | } 49 | for i, a := range as { 50 | b, ok := a.(string) 51 | if !ok { 52 | log.Printf("codeActionKinds[%d] is %T", i, b) 53 | } 54 | allowed = append(allowed, protocol.CodeActionKind(b)) 55 | } 56 | } 57 | 58 | var compat []protocol.CodeActionKind 59 | Kinds: 60 | for _, k := range kinds { 61 | for _, allow := range allowed { 62 | if k == allow { 63 | compat = append(compat, k) 64 | continue Kinds 65 | } 66 | } 67 | log.Printf("code action %v is not compatible with server kinds %v", k, allowed) 68 | } 69 | return compat 70 | 71 | } 72 | 73 | func LocationLink(l *protocol.Location, basedir string) string { 74 | p := text.ToPath(l.URI) 75 | rel, err := filepath.Rel(basedir, p) 76 | if err == nil && len(rel) < len(p) { 77 | p = rel 78 | } 79 | return fmt.Sprintf("%s:%v.%v,%v.%v", p, 80 | l.Range.Start.Line+1, l.Range.Start.Character+1, 81 | l.Range.End.Line+1, l.Range.End.Character+1) 82 | } 83 | 84 | func DidOpen(ctx context.Context, server protocol.Server, filename string, lang string, body []byte) error { 85 | if lang == "" { 86 | lang = DetectLanguage(filename) 87 | } 88 | return server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ 89 | TextDocument: protocol.TextDocumentItem{ 90 | URI: text.ToURI(filename), 91 | LanguageID: lang, 92 | Version: 0, 93 | Text: string(body), 94 | }, 95 | }) 96 | } 97 | 98 | func DidClose(ctx context.Context, server protocol.Server, filename string) error { 99 | return server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ 100 | TextDocument: protocol.TextDocumentIdentifier{ 101 | URI: text.ToURI(filename), 102 | }, 103 | }) 104 | } 105 | 106 | func DidSave(ctx context.Context, server protocol.Server, filename string) error { 107 | return server.DidSave(ctx, &protocol.DidSaveTextDocumentParams{ 108 | TextDocument: protocol.TextDocumentIdentifier{ 109 | URI: text.ToURI(filename), 110 | // TODO(fhs): add text field for includeText option 111 | }, 112 | }) 113 | } 114 | 115 | func DidChange(ctx context.Context, server protocol.Server, filename string, body []byte) error { 116 | return server.DidChange(ctx, &protocol.DidChangeTextDocumentParams{ 117 | TextDocument: protocol.VersionedTextDocumentIdentifier{ 118 | TextDocumentIdentifier: protocol.TextDocumentIdentifier{ 119 | URI: text.ToURI(filename), 120 | }, 121 | }, 122 | ContentChanges: []protocol.TextDocumentContentChangeEvent{ 123 | { 124 | Text: string(body), 125 | }, 126 | }, 127 | }) 128 | } 129 | 130 | func DetectLanguage(filename string) string { 131 | switch base := filepath.Base(filename); base { 132 | case "go.mod", "go.sum": 133 | return base 134 | } 135 | lang := filepath.Ext(filename) 136 | if len(lang) == 0 { 137 | return lang 138 | } 139 | if lang[0] == '.' { 140 | lang = lang[1:] 141 | } 142 | switch lang { 143 | case "py": 144 | lang = "python" 145 | } 146 | return lang 147 | } 148 | 149 | func DirsToWorkspaceFolders(dirs []string) ([]protocol.WorkspaceFolder, error) { 150 | var workspaces []protocol.WorkspaceFolder 151 | for _, d := range dirs { 152 | d, err := filepath.Abs(d) 153 | if err != nil { 154 | return nil, err 155 | } 156 | workspaces = append(workspaces, protocol.WorkspaceFolder{ 157 | URI: string(text.ToURI(d)), 158 | Name: d, 159 | }) 160 | } 161 | return workspaces, nil 162 | } 163 | 164 | // LogMessages causes all messages sent and received on conn to be 165 | // logged using the provided logger. 166 | // 167 | // This works around a bug in jsonrpc2. 168 | // Upstream PR: https://github.com/sourcegraph/jsonrpc2/pull/71 169 | func LogMessages(logger jsonrpc2.Logger) jsonrpc2.ConnOpt { 170 | return func(c *jsonrpc2.Conn) { 171 | // Remember reqs we have received so we can helpfully show the 172 | // request method in OnSend for responses. 173 | var ( 174 | mu sync.Mutex 175 | reqMethods = map[jsonrpc2.ID]string{} 176 | ) 177 | 178 | jsonrpc2.OnRecv(func(req *jsonrpc2.Request, resp *jsonrpc2.Response) { 179 | switch { 180 | case resp != nil: 181 | var method string 182 | if req != nil { 183 | method = req.Method 184 | } else { 185 | method = "(no matching request)" 186 | } 187 | switch { 188 | case resp.Result != nil: 189 | result, _ := json.Marshal(resp.Result) 190 | logger.Printf("jsonrpc2: --> result #%s: %s: %s\n", resp.ID, method, result) 191 | case resp.Error != nil: 192 | err, _ := json.Marshal(resp.Error) 193 | logger.Printf("jsonrpc2: --> error #%s: %s: %s\n", resp.ID, method, err) 194 | } 195 | 196 | case req != nil: 197 | mu.Lock() 198 | reqMethods[req.ID] = req.Method 199 | mu.Unlock() 200 | 201 | params, _ := json.Marshal(req.Params) 202 | if req.Notif { 203 | logger.Printf("jsonrpc2: --> notif: %s: %s\n", req.Method, params) 204 | } else { 205 | logger.Printf("jsonrpc2: --> request #%s: %s: %s\n", req.ID, req.Method, params) 206 | } 207 | } 208 | })(c) 209 | jsonrpc2.OnSend(func(req *jsonrpc2.Request, resp *jsonrpc2.Response) { 210 | switch { 211 | case resp != nil: 212 | mu.Lock() 213 | method := reqMethods[resp.ID] 214 | delete(reqMethods, resp.ID) 215 | mu.Unlock() 216 | if method == "" { 217 | method = "(no previous request)" 218 | } 219 | 220 | if resp.Result != nil { 221 | result, _ := json.Marshal(resp.Result) 222 | logger.Printf("jsonrpc2: <-- result #%s: %s: %s\n", resp.ID, method, result) 223 | } else { 224 | err, _ := json.Marshal(resp.Error) 225 | logger.Printf("jsonrpc2: <-- error #%s: %s: %s\n", resp.ID, method, err) 226 | } 227 | 228 | case req != nil: 229 | params, _ := json.Marshal(req.Params) 230 | if req.Notif { 231 | logger.Printf("jsonrpc2: <-- notif: %s: %s\n", req.Method, params) 232 | } else { 233 | logger.Printf("jsonrpc2: <-- request #%s: %s: %s\n", req.ID, req.Method, params) 234 | } 235 | } 236 | })(c) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /internal/lsp/utils_test.go: -------------------------------------------------------------------------------- 1 | package lsp 2 | 3 | import ( 4 | "testing" 5 | 6 | "9fans.net/internal/go-lsp/lsp/protocol" 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestCompatibleCodeActions(t *testing.T) { 11 | for _, tc := range []struct { 12 | name string 13 | cap protocol.ServerCapabilities 14 | kinds, want []protocol.CodeActionKind 15 | }{ 16 | { 17 | "True", 18 | protocol.ServerCapabilities{CodeActionProvider: true}, 19 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 20 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 21 | }, 22 | { 23 | "False", 24 | protocol.ServerCapabilities{CodeActionProvider: false}, 25 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 26 | nil, 27 | }, 28 | { 29 | "AllFound", 30 | protocol.ServerCapabilities{ 31 | CodeActionProvider: protocol.CodeActionOptions{ 32 | CodeActionKinds: []protocol.CodeActionKind{ 33 | protocol.QuickFix, 34 | protocol.SourceOrganizeImports, 35 | }, 36 | }, 37 | }, 38 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 39 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 40 | }, 41 | { 42 | "NoneFound", 43 | protocol.ServerCapabilities{ 44 | CodeActionProvider: protocol.CodeActionOptions{ 45 | CodeActionKinds: []protocol.CodeActionKind{ 46 | protocol.QuickFix, 47 | }, 48 | }, 49 | }, 50 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 51 | nil, 52 | }, 53 | { 54 | "OneFound", 55 | protocol.ServerCapabilities{ 56 | CodeActionProvider: protocol.CodeActionOptions{ 57 | CodeActionKinds: []protocol.CodeActionKind{ 58 | protocol.QuickFix, 59 | protocol.SourceOrganizeImports, 60 | }, 61 | }, 62 | }, 63 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 64 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 65 | }, 66 | { 67 | "OneFoundMap", 68 | protocol.ServerCapabilities{ 69 | CodeActionProvider: map[string]any{ 70 | "codeActionKinds": []any{ 71 | "quickfix", 72 | "source.organizeImports", 73 | }, 74 | }, 75 | }, 76 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 77 | []protocol.CodeActionKind{protocol.SourceOrganizeImports}, 78 | }, 79 | } { 80 | t.Run(tc.name, func(t *testing.T) { 81 | got := CompatibleCodeActions(&tc.cap, tc.kinds) 82 | want := tc.want 83 | if !cmp.Equal(got, want) { 84 | t.Errorf("got %v; want %v", got, want) 85 | } 86 | }) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/p9service/p9service.go: -------------------------------------------------------------------------------- 1 | // Pakcage p9service help implement a plan9 service. 2 | package p9service 3 | 4 | import ( 5 | "context" 6 | "net" 7 | "os" 8 | ) 9 | 10 | // Listen is like net.Listen but it removes dead unix sockets. 11 | func Listen(ctx context.Context, network, address string) (net.Listener, error) { 12 | var lc net.ListenConfig 13 | ln, err := lc.Listen(ctx, network, address) 14 | if err != nil && network == "unix" && isAddrInUse(err) { 15 | if _, err1 := net.Dial(network, address); !isConnRefused(err1) { 16 | return nil, err // Listen error 17 | } 18 | // Dead socket, so remove it. 19 | err = os.Remove(address) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return net.Listen(network, address) 24 | } 25 | return ln, err 26 | } 27 | -------------------------------------------------------------------------------- /internal/p9service/p9service_plan9.go: -------------------------------------------------------------------------------- 1 | //go:build plan9 2 | // +build plan9 3 | 4 | package p9service 5 | 6 | func isAddrInUse(err error) bool { 7 | return false 8 | } 9 | 10 | func isConnRefused(err error) bool { 11 | return false 12 | } 13 | -------------------------------------------------------------------------------- /internal/p9service/p9service_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !plan9 2 | // +build !plan9 3 | 4 | package p9service 5 | 6 | import ( 7 | "net" 8 | "os" 9 | "syscall" 10 | ) 11 | 12 | func isAddrInUse(err error) bool { 13 | if err, ok := err.(*net.OpError); ok { 14 | if err, ok := err.Err.(*os.SyscallError); ok { 15 | return err.Err == syscall.EADDRINUSE 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func isConnRefused(err error) bool { 22 | if err, ok := err.(*net.OpError); ok { 23 | if err, ok := err.Err.(*os.SyscallError); ok { 24 | return err.Err == syscall.ECONNREFUSED 25 | } 26 | } 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /scripts/mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2012 The Go Authors. All rights reserved. 3 | # Use of this source code is governed by a BSD-style 4 | # license that can be found in the LICENSE file. 5 | 6 | # Generate command docs from usage help. 7 | # Based on https://golang.org/src/cmd/go/mkalldocs.sh 8 | 9 | go build -o main.latest 10 | ( 11 | echo '/*' 12 | ./main.latest -help 2>&1 13 | echo '*/' 14 | echo 'package main' 15 | ) >doc.go 16 | gofmt -w doc.go 17 | rm main.latest 18 | --------------------------------------------------------------------------------