Skip to content

Commit

Permalink
test: add end-to-end tests for the various formatting requests
Browse files Browse the repository at this point in the history
  • Loading branch information
alcarney committed Apr 26, 2024
1 parent 0d51815 commit 5831c1f
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 353 deletions.
18 changes: 11 additions & 7 deletions examples/servers/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
{
"[plaintext]": {
// Uncomment to enable `textDocument/onTypeFormatting` requests
//"editor.formatOnType": true
},
// Uncomment to override Python interpreter used.
// "pygls.server.pythonPath": "/path/to/python",
"pygls.server.debug": false,
// "pygls.server.debugHost": "localhost",
// "pygls.server.debugPort": 5678,
"pygls.server.launchScript": "json_server.py",
"pygls.server.launchScript": "formatting.py",
"pygls.trace.server": "off",
"pygls.client.documentSelector": [
{
"scheme": "file",
"language": "json"
}
// Uncomment to use code_actions or inlay_hints servers
// {
// "scheme": "file",
// "language": "plaintext"
// "language": "json"
// }
// Uncomment to use code_actions or inlay_hints servers
{
"scheme": "file",
"language": "plaintext"
}
],
// "pygls.jsonServer.exampleConfiguration": "some value here",
}
6 changes: 6 additions & 0 deletions examples/servers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
| `code_actions.py` | `sums.txt` | Evaluate sums via a code action |
| `code_lens.py` | `sums.txt` | Evaluate sums via a code lens |
| `colors.py` | `colors.txt` | Provides a visual representation of color values and even a color picker in supported clients |
| `formatting.py`| `table.txt`| Implements whole document, selection only and as-you-type formatting for markdown like tables [^1] [^2] |
| `goto.py` | `code.txt` | Implements the various "Goto X" requests in the specification |
| `hover.py` | `dates.txt` | Opens a popup showing the date underneath the cursor in multiple formats |
| `inlay_hints.py` | `sums.txt` | Use inlay hints to show the binary representation of numbers in the file |
| `publish_diagnostics.py` | `sums.txt` | Use "push-model" diagnostics to highlight missing or incorrect answers |
| `pull_diagnostics.py` | `sums.txt` | Use "pull-model" diagnostics to highlight missing or incorrect answers |


[^1]: To enable as-you-type formatting, be sure to uncomment the `editor.formatOnType` option in `.vscode/settings.json`

[^2]: This server is enough to demonstrate the bare minimum required to implement these methods be sure to check the contents of the `params` object for all the additional options you shoud be considering!
178 changes: 178 additions & 0 deletions examples/servers/formatting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
############################################################################
# Copyright(c) Open Law Library. All rights reserved. #
# See ThirdPartyNotices.txt in the project root for additional notices. #
# #
# 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. #
############################################################################
import logging
from typing import Dict
from typing import List
from typing import Optional

import attrs
from lsprotocol import types

from pygls.server import LanguageServer
from pygls.workspace import TextDocument


@attrs.define
class Row:
"""Represents a row in the table"""

cells: List[str]
cell_widths: List[int]
line_number: int


server = LanguageServer("formatting-server", "v1")


@server.feature(types.TEXT_DOCUMENT_FORMATTING)
def format_document(ls: LanguageServer, params: types.DocumentFormattingParams):
"""Format the entire document"""
logging.debug("%s", params)

doc = ls.workspace.get_text_document(params.text_document.uri)
rows = parse_document(doc)
return format_table(rows)


@server.feature(types.TEXT_DOCUMENT_RANGE_FORMATTING)
def format_range(ls: LanguageServer, params: types.DocumentRangeFormattingParams):
"""Format the given range within a document"""
logging.debug("%s", params)

doc = ls.workspace.get_text_document(params.text_document.uri)
rows = parse_document(doc, params.range)
return format_table(rows, params.range)


@server.feature(
types.TEXT_DOCUMENT_ON_TYPE_FORMATTING,
types.DocumentOnTypeFormattingOptions(first_trigger_character="|"),
)
def format_on_type(ls: LanguageServer, params: types.DocumentOnTypeFormattingParams):
"""Format the document while the user is typing"""
logging.debug("%s", params)

doc = ls.workspace.get_text_document(params.text_document.uri)
rows = parse_document(doc)
return format_table(rows)


def format_table(
rows: List[Row], range_: Optional[types.Range] = None
) -> List[types.TextEdit]:
"""Format the given table, returning the list of edits to make to the document.
If range is given, this method will only modify the document within the specified
range.
"""
edits: List[types.TextEdit] = []

# Determine max widths
columns: Dict[int, int] = {}
for row in rows:
for idx, cell in enumerate(row.cells):
columns[idx] = max(len(cell), columns.get(idx, 0))

# Format the table.
cell_padding = 2
for row in rows:
# Only process the lines within the specified range.
if skip_line(row.line_number, range_):
continue

if len(row.cells) == 0:
# If there are no cells on the row, then this must be a separator row
cells: List[str] = []
empty_cells = [
"-" * (columns[i] + cell_padding) for i in range(len(columns))
]
else:
# Otherwise ensure that each row has a consistent number of cells
empty_cells = [" " for _ in range(len(columns) - len(row.cells))]
cells = [
c.center(columns[i] + cell_padding) for i, c in enumerate(row.cells)
]

line = f"|{'|'.join([*cells, *empty_cells])}|\n"
edits.append(
types.TextEdit(
range=types.Range(
start=types.Position(line=row.line_number, character=0),
end=types.Position(line=row.line_number + 1, character=0),
),
new_text=line,
)
)

return edits


def parse_document(
document: TextDocument, range_: Optional[types.Range] = None
) -> List[Row]:
"""Parse the given document into a list of table rows.
If range_ is given, only consider lines within the range part of the table.
"""
rows: List[Row] = []
for linum, line in enumerate(document.lines):
if skip_line(linum, range_):
continue

line = line.strip()
cells = [c.strip() for c in line.split("|")]

if line.startswith("|"):
cells.pop(0)

if line.endswith("|"):
cells.pop(-1)

chars = set()
for c in cells:
chars.update(set(c))

logging.debug("%s: %s", chars, cells)

if chars == {"-"}:
# Check for a separator row, use an empty list to represent it.
cells = []

elif len(cells) == 0:
continue

row = Row(cells=cells, line_number=linum, cell_widths=[len(c) for c in cells])

logging.debug("%s", row)
rows.append(row)

return rows


def skip_line(line: int, range_: Optional[types.Range]) -> bool:
"""Given a range, determine if we should skip the given line number."""

if range_ is None:
return False

return any([line < range_.start.line, line > range_.end.line])


if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")

server.start_io()
3 changes: 3 additions & 0 deletions examples/servers/workspace/table.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
|a|b|
|-|-|
|apple|banana|
Loading

0 comments on commit 5831c1f

Please sign in to comment.