Skip to content

Commit

Permalink
language server
Browse files Browse the repository at this point in the history
  • Loading branch information
aljazerzen committed May 9, 2024
1 parent 12b3d2a commit f3e30f3
Show file tree
Hide file tree
Showing 6 changed files with 429 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
8 changes: 8 additions & 0 deletions edb/errors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ def line(self):
def col(self):
return int(self._attrs.get(FIELD_COLUMN_START, -1))

@property
def line_end(self):
return int(self._attrs.get(FIELD_LINE_END, -1))

@property
def col_end(self):
return int(self._attrs.get(FIELD_COLUMN_END, -1))

@property
def position(self):
return int(self._attrs.get(FIELD_POSITION_START, -1))
Expand Down
17 changes: 17 additions & 0 deletions edb/language_server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
87 changes: 87 additions & 0 deletions edb/language_server/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from lsprotocol import types as lsp_types


from edb.edgeql import parser as qlparser

from . import parsing as ls_parsing
from . import server as ls_server


def main():
ls = ls_server.EdgeDBLanguageServer()

@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):
document_updated(ls, params.text_document.uri)

@ls.feature(lsp_types.TEXT_DOCUMENT_DID_CHANGE)
def text_document_did_change(params: lsp_types.DidChangeTextDocumentParams):
document_updated(ls, params.text_document.uri)

@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 := ls_parsing.parse_and_suggest(document, params.position):
items.append(item)

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

ls.start_io()


def document_updated(ls: ls_server.EdgeDBLanguageServer, doc_uri: str):
# each call to this function should yield in exactly one publish_diagnostics
# for this document

document = ls.workspace.get_text_document(doc_uri)
ql_ast = ls_parsing.parse(document, ls)
if diagnostics := ql_ast.error:
ls.publish_diagnostics(document.uri, diagnostics, document.version)
return
assert ql_ast.ok

try:
if isinstance(ql_ast.ok, list):
diagnostics = ls_server.compile(ls, ql_ast.ok)
ls.publish_diagnostics(document.uri, diagnostics, document.version)
else:
ls.publish_diagnostics(document.uri, [], document.version)
except BaseException as e:
ls.show_message_log(f'Internal error: {e}')
ls.publish_diagnostics(document.uri, [], document.version)


if __name__ == '__main__':
main()
149 changes: 149 additions & 0 deletions edb/language_server/parsing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2008-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from typing import Any, List, Tuple, Optional, TypeVar, Generic
from dataclasses import dataclass

from pygls.server import LanguageServer
from pygls.workspace import TextDocument
from lsprotocol import types as lsp_types


from edb.edgeql import ast as qlast
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


T = TypeVar('T', covariant=True)
E = TypeVar('E', covariant=True)


@dataclass(kw_only=True, slots=True)
class Result(Generic[T, E]):
ok: Optional[T] = None
error: Optional[E] = None


def parse(
doc: TextDocument, ls: LanguageServer
) -> Result[List[qlast.Base] | qlast.Schema, List[lsp_types.Diagnostic]]:
sdl = doc.filename.endswith('.esdl') if doc.filename else False

source, result, productions = _parse_inner(doc.source, sdl)

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)
assert end

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,
)
)

return Result(error=diagnostics)

# parsing successful
assert isinstance(result.out, rust_parser.CSTNode)

ast = qlparser._cst_to_ast(
result.out, productions, source, doc.filename
).val
if sdl:
assert isinstance(ast, qlast.Schema), ast
else:
assert isinstance(ast, list), ast
return Result(ok=ast)


def parse_and_suggest(
doc: TextDocument, position: lsp_types.Position
) -> Optional[lsp_types.CompletionItem]:
sdl = doc.filename.endswith('.esdl') if doc.filename else False

source, result, _productions = _parse_inner(doc.source, sdl)
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


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_inner(
source_str: str, sdl: bool
) -> Tuple[tokenizer.Source, rust_parser.ParserResult, Any]:
try:
source = tokenizer.Source.from_string(source_str)
except Exception as e:
# TODO
print(e)
raise AssertionError(e)

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
Loading

0 comments on commit f3e30f3

Please sign in to comment.