Skip to content

Commit

Permalink
lsp demo
Browse files Browse the repository at this point in the history
  • Loading branch information
aljazerzen committed May 6, 2024
1 parent 7968f3d commit 7552383
Show file tree
Hide file tree
Showing 24 changed files with 4,822 additions and 2 deletions.
4 changes: 2 additions & 2 deletions edb/edgeql-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,9 +558,9 @@ fn injection_cost(kind: &Kind) -> u16 {

// Manual keyword tweaks to encourage some error messages and discourage others.
Keyword(keywords::Keyword(
"delete" | "update" | "migration" | "role" | "global" | "administer",
"delete" | "update" | "migration" | "role" | "global" | "administer" | "future" | "database",
)) => 100,
Keyword(keywords::Keyword("insert")) => 20,
Keyword(keywords::Keyword("insert" | "module" | "extension" | "branch")) => 20,
Keyword(keywords::Keyword("select" | "property" | "type")) => 10,
Keyword(_) => 15,

Expand Down
1 change: 1 addition & 0 deletions edb/tools/edb.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,6 @@ def server(version=False, **kwargs):
from . import gen_rust_ast # noqa
from . import ast_inheritance_graph # noqa
from . import parser_demo # noqa
from . import lsp # noqa
from .profiling import cli as prof_cli # noqa
from .experimental_interpreter import edb_entry # noqa
149 changes: 149 additions & 0 deletions edb/tools/lsp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from typing import Any, List, Tuple
import click
from pygls.server import LanguageServer
from pygls.workspace import TextDocument
from lsprotocol import types as lsp_types

from edb.tools.edb import edbcommands

from edb.edgeql import tokenizer
from edb.edgeql import parser as qlparser
from edb.edgeql.parser.grammar import tokens as qltokens

import edb._edgeql_parser as rust_parser


@edbcommands.command("lsp")
@click.option('--stdio', default=False, is_flag=True)
def main(stdio: bool):
ls = LanguageServer('EdgeDB Language Server', 'v0.1')

@ls.feature(
lsp_types.INITIALIZE,
)
def init(_params: lsp_types.InitializeParams):
ls.show_message_log('Starting')
qlparser.preload_spec()
ls.show_message_log('Started')

@ls.feature(lsp_types.TEXT_DOCUMENT_DID_OPEN)
def text_document_did_open(params: lsp_types.DidOpenTextDocumentParams):
ls.show_message_log(f'did open: {params.text_document.uri}')

document = ls.workspace.get_text_document(params.text_document.uri)
parse_and_report_diagnostics(document, ls)

@ls.feature(lsp_types.TEXT_DOCUMENT_DID_CHANGE)
def text_document_did_change(params: lsp_types.DidChangeTextDocumentParams):
ls.show_message_log(f'did change: {params.text_document.uri}')

document = ls.workspace.get_text_document(params.text_document.uri)
parse_and_report_diagnostics(document, ls)

@ls.feature(
lsp_types.TEXT_DOCUMENT_COMPLETION,
lsp_types.CompletionOptions(trigger_characters=[',']),
)
def completions(params: lsp_types.CompletionParams):
items = []

document = ls.workspace.get_text_document(params.text_document.uri)

if item := parse_and_suggest_keyword(document, params.position):
items.append(item)

return lsp_types.CompletionList(is_incomplete=False, items=items)

if stdio:
ls.start_io()


def position_in_span(pos: lsp_types.Position, span: Tuple[Any, Any]):
start, end = span

if pos.line < start.line - 1:
return False
if pos.line > end.line - 1:
return False
if pos.line == start.line - 1 and pos.character < start.column - 1:
return False
if pos.line == end.line - 1 and pos.character > end.column - 1:
return False
return True


def parse(doc: TextDocument) -> Tuple[tokenizer.Source, List[Any], Any]:
sdl = False

try:
source = tokenizer.Source.from_string(doc.source)
except Exception as e:
# TODO
print(e)
return

start_t = qltokens.T_STARTSDLDOCUMENT if sdl else qltokens.T_STARTBLOCK
start_t_name = start_t.__name__[2:]
tokens = source.tokens()

result, productions = rust_parser.parse(start_t_name, tokens)
return source, result, productions


def parse_and_report_diagnostics(doc: TextDocument, ls: LanguageServer) -> None:
source, result, _productions = parse(doc)

if result.errors:
diagnostics = []
for error in result.errors:
message, span, hint, details = error

if details:
message += f"\n{details}"
if hint:
message += f"\nHint: {hint}"
(start, end) = tokenizer.inflate_span(source.text(), span)

diagnostics.append(
lsp_types.Diagnostic(
range=lsp_types.Range(
start=lsp_types.Position(
line=start.line - 1,
character=start.column - 1,
),
end=lsp_types.Position(
line=end.line - 1,
character=end.column - 1,
),
),
severity=lsp_types.DiagnosticSeverity.Error,
message=message,
)
)

ls.publish_diagnostics(doc.uri, diagnostics, doc.version)
return

ls.publish_diagnostics(doc.uri, [], doc.version)
# parsing successful


def parse_and_suggest_keyword(document, position) -> lsp_types.CompletionItem:
source, result, _productions = parse(document)
for error in result.errors:
message: str
message, span, hint, details = error
if not message.startswith('Missing keyword '):
continue
(start, end) = tokenizer.inflate_span(source.text(), span)

if not position_in_span(position, (start, end)):
continue

keyword = message.removeprefix('Missing keyword \'')[:-1]

return lsp_types.CompletionItem(
label=keyword,
kind=lsp_types.CompletionItemKind.Keyword,
)
return None
5 changes: 5 additions & 0 deletions vscode-lsp-client/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/**
client/node_modules/**
client/out/**
server/node_modules/**
server/out/**
20 changes: 20 additions & 0 deletions vscode-lsp-client/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**@type {import('eslint').Linter.Config} */
// eslint-disable-next-line no-undef
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'semi': [2, "always"],
'@typescript-eslint/no-unused-vars': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-non-null-assertion': 0,
}
};
4 changes: 4 additions & 0 deletions vscode-lsp-client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
out
node_modules
client/server
.vscode-test
15 changes: 15 additions & 0 deletions vscode-lsp-client/.vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.vscode/**
**/*.ts
**/*.map
.gitignore
**/tsconfig.json
**/tsconfig.base.json
contributing.md
.travis.yml
client/node_modules/**
!client/node_modules/vscode-jsonrpc/**
!client/node_modules/vscode-languageclient/**
!client/node_modules/vscode-languageserver-protocol/**
!client/node_modules/vscode-languageserver-types/**
!client/node_modules/{minimatch,brace-expansion,concat-map,balanced-match}/**
!client/node_modules/{semver,lru-cache,yallist}/**
37 changes: 37 additions & 0 deletions vscode-lsp-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# LSP Example

Heavily documented sample code for https://code.visualstudio.com/api/language-extensions/language-server-extension-guide

## Functionality

This Language Server works for plain text file. It has the following language features:
- Completions
- Diagnostics regenerated on each file change or configuration change

It also includes an End-to-End test.

## Structure

```
.
├── client // Language Client
│ ├── src
│ │ ├── test // End to End tests for Language Client / Server
│ │ └── extension.ts // Language Client entry point
├── package.json // The extension manifest.
└── server // Language Server
└── src
└── server.ts // Language Server entry point
```

## Running the Sample

- Run `npm install` in this folder. This installs all necessary npm modules in both the client and server folder
- Open VS Code on this folder.
- Press Ctrl+Shift+B to start compiling the client and server in [watch mode](https://code.visualstudio.com/docs/editor/tasks#:~:text=The%20first%20entry%20executes,the%20HelloWorld.js%20file.).
- Switch to the Run and Debug View in the Sidebar (Ctrl+Shift+D).
- Select `Launch Client` from the drop down (if it is not already).
- Press ▷ to run the launch config (F5).
- In the [Extension Development Host](https://code.visualstudio.com/api/get-started/your-first-extension#:~:text=Then%2C%20inside%20the%20editor%2C%20press%20F5.%20This%20will%20compile%20and%20run%20the%20extension%20in%20a%20new%20Extension%20Development%20Host%20window.) instance of VSCode, open a document in 'plain text' language mode.
- Type `j` or `t` to see `Javascript` and `TypeScript` completion.
- Enter text content such as `AAA aaa BBB`. The extension will emit diagnostics for all words in all-uppercase.
Loading

0 comments on commit 7552383

Please sign in to comment.