Skip to content

Commit

Permalink
feat: go to definition and find references for document refs
Browse files Browse the repository at this point in the history
  • Loading branch information
armsnyder committed Jun 23, 2024
1 parent 58a97fb commit 2f0afee
Show file tree
Hide file tree
Showing 31 changed files with 3,286 additions and 0 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow

on:
push:
branches:
- main
pull_request: {}

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: |
go test -cover ./...
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- uses: golangci/golangci-lint-action@v4
with:
version: v1.59.1

install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: go install .
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.out
/openapiv3-lsp
125 changes: 125 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# See https://golangci-lint.run/usage/configuration/

linters:
disable-all: true
enable:
# See https://golangci-lint.run/usage/linters/
- asasalint # Check for pass []any as any in variadic func(...any).
- bodyclose # Checks whether HTTP response body is closed successfully.
- contextcheck # Check whether the function uses a non-inherited context.
- durationcheck # Check for two durations multiplied together.
- errcheck # Checks whether Rows.Err of rows is checked successfully.
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted.
- errorlint # Errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
- forbidigo # Forbids identifiers.
- gci # Gci controls Go package import order and makes it always deterministic.
- gocritic # Provides diagnostics that check for bugs, performance and style issues. Extensible without recompilation through dynamic rules. Dynamic rules are written declaratively with AST patterns, filters, report message and optional suggestion.
- godot # Check if comments end in a period.
- gosec # Inspects source code for security problems.
- gosimple # Linter for Go source code that specializes in simplifying code.
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string.
- inamedparam # Reports interfaces with unnamed method parameters.
- ineffassign # Detects when assignments to existing variables are not used.
- mirror # Reports wrong mirror patterns of bytes/strings usage.
- musttag # Enforce field tags in (un)marshaled structs.
- nilerr # Finds the code that returns nil even if it checks that the error is not nil.
- nilnil # Checks that there is no simultaneous return of nil error and an invalid value.
- noctx # Finds sending http request without context.Context.
- nolintlint # Reports ill-formed or insufficient nolint directives.
- nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL.
- perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative.
- protogetter # Reports direct reads from proto message fields when getters should be used.
- reassign # Checks that package variables are not reassigned.
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
- staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint.
- tenv # # Tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17.
- unconvert # Remove unnecessary type conversions.
- unused # Checks Go code for unused constants, variables, functions and types.

linters-settings:
# See https://golangci-lint.run/usage/linters/#linters-configuration
forbidigo:
forbid:
- 'fmt\.Print.*' # Should be using a logger
gci:
sections:
- standard
- default
- prefix(github.com/armsnyder)
gocritic:
enabled-tags:
- performance
- opinionated
- experimental
disabled-checks:
- whyNoLint # False positives, use nolintlint instead
govet:
enable-all: true
disable:
- fieldalignment # Too struct
nolintlint:
require-specific: true
revive:
enable-all-rules: true
rules:
# See https://revive.run/r
- name: add-constant # too strict
disabled: true
- name: argument-limit # too strict
disabled: true
- name: cognitive-complexity
arguments:
- 30
- name: cyclomatic
arguments:
- 30
- name: file-header # too strict
disabled: true
- name: function-length
arguments:
- 50 # statements
- 0 # lines (0 to disable)
- name: function-result-limit # too strict
disabled: true
- name: import-shadowing # too strict, results in uglier code
disabled: true
- name: line-length-limit # too strict
disabled: true
- name: max-public-structs # too strict
disabled: true
- name: modifies-parameter # too strict
disabled: true
- name: modifies-value-receiver # too strict
disabled: true
- name: nested-structs # too strict
disabled: true
- name: package-comments # too strict
disabled: true
- name: unhandled-error
disabled: true # not as good as errcheck

issues:
exclude-rules:
- path: _test\.go$
linters:
- gosec # too strict
- noctx # too strict
- path: _test\.go$
text: (cognitive-complexity|function-length|dot-imports|import-alias-naming) # too strict
linters:
- revive
# main.go is allowed to contain early bootstrapping print statements.
# TestMain is allowed to log.
- path: \/main(_test)?\.go$
text: fmt.Print
linters:
- forbidigo
# Shadowing err is common.
- text: 'shadow: declaration of "err"'
linters:
- govet
- text: "^exported:.+stutters" # too strict and gets in the way of combining types like handlers
linters:
- revive
- path: _test\.go$
text: "unused-parameter" # too strict
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# OpenAPI Language Server

This is a language server for OpenAPI v3.0.0. It is based on the [Language
Server Protocol](https://microsoft.github.io/language-server-protocol/).

I created this language server because I do a lot of manual OpenAPI/Swagger
file editing, and I wanted a quick way to jump to definitions and find
references of schema definitions.

I personally use
[yaml-language-server](https://github.com/redhat-developer/yaml-language-server)
for schema validation and code completion, so these features are not a priority
for me to implement in this language server.

## Features

### Language Features

- [x] Jump to definition
- [x] Find references
- [ ] Code completion
- [ ] Diagnostics
- [ ] Hover
- [ ] Rename
- [ ] Document symbols
- [ ] Code actions

### Other Features

- [x] YAML filetype support
- [ ] JSON filetype support
- [ ] VSCode extension

## Installation

```bash
go install github.com/armsnyder/openapiv3-lsp@latest
```

### Neovim Configuration Example

Assuming you are using Neovim and have the installed openapiv3-lsp binary in
your PATH, you can use the following Lua code to your Neovim configuration:

```lua
vim.api.nvim_create_autocmd('FileType', {
pattern = 'yaml',
callback = function()
vim.lsp.start {
cmd = { 'openapiv3-lsp' },
filetypes = { 'yaml' },
}
end,
})
```

This is just a basic working example. You will probably want to further
customize the configuration to your needs.
3 changes: 3 additions & 0 deletions generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package main

//go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source internal/lsp/handler.go -destination internal/lsp/testutil/handler.go -package testutil
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/armsnyder/openapiv3-lsp

go 1.22.4

require go.uber.org/mock v0.4.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
2 changes: 2 additions & 0 deletions internal/analysis/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package anaylsis contains the OpenAPI Lanuage Server business logic.
package analysis
134 changes: 134 additions & 0 deletions internal/analysis/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package analysis

import (
"bytes"
"fmt"

"github.com/armsnyder/openapiv3-lsp/internal/analysis/yaml"
"github.com/armsnyder/openapiv3-lsp/internal/lsp"
"github.com/armsnyder/openapiv3-lsp/internal/lsp/types"
)

// Handler implements the LSP handler for the OpenAPI Language Server. It
// contains the business logic for the server.
type Handler struct {
lsp.NopHandler

files map[string]*annotatedFile
}

type annotatedFile struct {
file lsp.File
document yaml.Document
}

func (h *Handler) getDocument(uri string) (yaml.Document, error) {
f := h.files[uri]
if f == nil {
return yaml.Document{}, fmt.Errorf("unknown file: %s", uri)
}

if f.document.Lines == nil {
document, err := yaml.Parse(bytes.NewReader(f.file.Bytes()))
if err != nil {
return yaml.Document{}, err
}
f.document = document
}

return f.document, nil
}

func (*Handler) Capabilities() types.ServerCapabilities {
return types.ServerCapabilities{
TextDocumentSync: types.TextDocumentSyncOptions{
OpenClose: true,
Change: types.SyncIncremental,
},
DefinitionProvider: true,
ReferencesProvider: true,
}
}

func (h *Handler) HandleOpen(params types.DidOpenTextDocumentParams) error {
if h.files == nil {
h.files = make(map[string]*annotatedFile)
}

var f annotatedFile

f.file.Reset([]byte(params.TextDocument.Text))
h.files[params.TextDocument.URI] = &f

return nil
}

func (h *Handler) HandleClose(params types.DidCloseTextDocumentParams) error {
delete(h.files, params.TextDocument.URI)
return nil
}

func (h *Handler) HandleChange(params types.DidChangeTextDocumentParams) error {
f, ok := h.files[params.TextDocument.URI]
if !ok {
return fmt.Errorf("unknown file: %s", params.TextDocument.URI)
}

for _, change := range params.ContentChanges {
if err := f.file.ApplyChange(change); err != nil {
return err
}
}

return nil
}

func (h *Handler) HandleDefinition(params types.DefinitionParams) ([]types.Location, error) {
document, err := h.getDocument(params.TextDocument.URI)
if err != nil {
return nil, err
}

if params.Position.Line >= len(document.Lines) {
return nil, nil
}

ref := document.Lines[params.Position.Line].Value

referencedLine := document.Locate(ref)
if referencedLine == nil {
return nil, nil
}

return []types.Location{{
URI: params.TextDocument.URI,
Range: referencedLine.KeyRange,
}}, nil
}

func (h *Handler) HandleReferences(params types.ReferenceParams) ([]types.Location, error) {
document, err := h.getDocument(params.TextDocument.URI)
if err != nil {
return nil, err
}

if params.Position.Line >= len(document.Lines) {
return nil, nil
}

ref := document.Lines[params.Position.Line].KeyRef()

var locations []types.Location

for _, line := range document.Lines {
if line.Value == ref {
locations = append(locations, types.Location{
URI: params.TextDocument.URI,
Range: line.ValueRange,
})
}
}
return locations, nil
}

var _ lsp.Handler = (*Handler)(nil)
Loading

0 comments on commit 2f0afee

Please sign in to comment.