Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create extra regions for diagnostics with tags #1588

Merged
merged 23 commits into from
Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/src/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,35 @@ The following tables give an overview about the scope names used by LSP.
!!! note
If `diagnostics_highlight_style` is set to "fill" in the LSP settings, the highlighting color can be controlled via the "background" color from a color scheme rule for the listed scopes.

Diagnostics will also optionally include the following scopes:

| scope | diagnostic tag name | description |
| ------------------------ | ------------------- | --------------------------- |
| `markup.unnecessary.lsp` | Unnecessary | Unused or unnecessary code |
| `markup.deprecated.lsp` | Deprecated | Deprecated or obsolete code |

!!! note
Regions created for those scopes don't follow the `diagnostics_highlight_style` setting and instead always use the "fill" style.

Those scopes can be used to, for example, gray-out the text color of unused code, if the server supports that.

For example, to add a custom rule for `Mariana` color scheme, select `UI: Customize Color Scheme` from the Command Palette and add the following rule:

```json
{
"rules": [
{
"scope": "markup.unnecessary.lsp",
"foreground": "color(rgb(255, 255, 255) alpha(0.4))",
"background": "color(var(blue3) alpha(0.9))"
}
]
}
```

The color scheme rule only works if the "background" color is different from the global background of the scheme. So for other color schemes, ideally pick a background color that is as close as possible, but marginally different from the original background.


#### Signature Help

| scope | description |
Expand Down
12 changes: 12 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class DiagnosticSeverity:
Hint = 4


class DiagnosticTag:
Unnecessary = 1
Deprecated = 2


class CompletionItemTag:
Deprecated = 1

Expand Down Expand Up @@ -174,6 +179,13 @@ class SignatureHelpTriggerKind:
}, total=True)


PublishDiagnosticsParams = TypedDict('PublishDiagnosticsParams', {
'uri': DocumentUri,
'version': Optional[int],
'diagnostics': List[Diagnostic],
}, total=False)


class Request:

__slots__ = ('method', 'params', 'view', 'progress')
Expand Down
5 changes: 5 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from .protocol import CodeAction
from .protocol import Command
from .protocol import CompletionItemTag
from .protocol import DiagnosticTag
from .protocol import Diagnostic
from .protocol import Error
from .protocol import ErrorCode
Expand Down Expand Up @@ -115,6 +116,7 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
config: ClientConfig) -> dict:
completion_kinds = list(range(1, len(COMPLETION_KINDS) + 1))
symbol_kinds = list(range(1, len(SYMBOL_KINDS) + 1))
diagnostic_tag_value_set = [v for k, v in DiagnosticTag.__dict__.items() if not k.startswith('_')]
completion_tag_value_set = [v for k, v in CompletionItemTag.__dict__.items() if not k.startswith('_')]
symbol_tag_value_set = [v for k, v in SymbolTag.__dict__.items() if not k.startswith('_')]
first_folder = workspace_folders[0] if workspace_folders else None
Expand Down Expand Up @@ -236,6 +238,9 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
},
"publishDiagnostics": {
"relatedInformation": True,
"tagSupport": {
"valueSet": diagnostic_tag_value_set
},
"versionSupport": True,
"codeDescriptionSupport": True,
"dataSupport": True
Expand Down
15 changes: 11 additions & 4 deletions plugin/session_buffer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from .core.protocol import Diagnostic, Range
from .core.protocol import Diagnostic
from .core.protocol import DiagnosticSeverity
from .core.protocol import TextDocumentSyncKindFull
from .core.protocol import TextDocumentSyncKindNone
from .core.protocol import Range
from .core.sessions import SessionViewProtocol
from .core.settings import userprefs
from .core.types import Capabilities
Expand Down Expand Up @@ -38,13 +39,14 @@ def update(self, version: int, changes: Iterable[sublime.TextChange]) -> None:

class DiagnosticSeverityData:

__slots__ = ('regions', 'annotations', 'panel_contribution', 'scope', 'icon')
__slots__ = ('regions', 'regions_with_tag', 'annotations', 'panel_contribution', 'scope', 'icon')

def __init__(self, severity: int) -> None:
self.regions = [] # type: List[sublime.Region]
self.regions_with_tag = {} # type: Dict[int, List[sublime.Region]]
self.annotations = [] # type: List[str]
self.panel_contribution = [] # type: List[Tuple[str, Optional[int], Optional[str], Optional[str]]]
_, __, self.scope, self.icon = DIAGNOSTIC_SEVERITY[severity - 1]
_, _, self.scope, self.icon = DIAGNOSTIC_SEVERITY[severity - 1]
if userprefs().diagnostics_gutter_marker != "sign":
self.icon = userprefs().diagnostics_gutter_marker

Expand Down Expand Up @@ -263,7 +265,12 @@ def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optio
data = DiagnosticSeverityData(severity)
data_per_severity[severity] = data
region = range_to_region(Range.from_lsp(diagnostic["range"]), view)
data.regions.append(region)
tags = diagnostic.get('tags', [])
if tags:
for tag in tags:
data.regions_with_tag.setdefault(tag, []).append(region)
else:
data.regions.append(region)
diagnostics.append((diagnostic, region))
if severity == DiagnosticSeverity.Error:
total_errors += 1
Expand Down
26 changes: 23 additions & 3 deletions plugin/session_view.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .core.progress import ViewProgressReporter
from .core.protocol import DiagnosticTag
from .core.protocol import Notification
from .core.protocol import Request
from .core.sessions import Session
Expand All @@ -13,6 +14,8 @@
import sublime
import functools

DIAGNOSTIC_TAG_VALUES = [v for (k, v) in DiagnosticTag.__dict__.items() if not k.startswith('_')]


class SessionView:
"""
Expand Down Expand Up @@ -173,20 +176,37 @@ def shutdown_async(self) -> None:
def diagnostics_key(self, severity: int) -> str:
return "lsp{}d{}".format(self.session.config.name, severity)

def diagnostics_tag_scope(self, tag: int) -> Optional[str]:
for k, v in DiagnosticTag.__dict__.items():
if v == tag:
return 'markup.{}.lsp'.format(k.lower())
return None

def present_diagnostics_async(self, flags: int) -> None:
data_per_severity = self.session_buffer.data_per_severity
for severity in reversed(range(1, len(DIAGNOSTIC_SEVERITY) + 1)):
key = self.diagnostics_key(severity)
key_tags = {tag: '{}_tags_{}'.format(key, tag) for tag in DIAGNOSTIC_TAG_VALUES}
data = data_per_severity.get(severity)
if data is None:
self.view.erase_regions(key)
for key_tag in key_tags.values():
self.view.erase_regions(key_tag)
elif ((severity <= userprefs().show_diagnostics_severity_level) and
(data.icon or flags != (sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE))):
# allow showing diagnostics with same begin and end range in the view
flags |= sublime.DRAW_EMPTY
self.view.add_regions(key, data.regions, data.scope, data.icon, flags)
non_tag_regions = data.regions
for tag, regions in data.regions_with_tag.items():
tag_scope = self.diagnostics_tag_scope(tag)
# Trick to only add tag regions if there is a corresponding color scheme scope defined.
if tag_scope and 'background' in self.view.style_for_scope(tag_scope):
self.view.add_regions(key_tags[tag], regions, tag_scope, flags=sublime.DRAW_NO_OUTLINE)
else:
non_tag_regions.extend(regions)
self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags | sublime.DRAW_EMPTY)
else:
self.view.erase_regions(key)
for key_tag in key_tags.values():
self.view.erase_regions(key_tag)
listener = self.listener()
if listener:
listener.on_diagnostics_updated_async()
Expand Down
18 changes: 13 additions & 5 deletions tests/test_server_notifications.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from LSP.plugin.core.protocol import DiagnosticSeverity
from LSP.plugin.core.protocol import DiagnosticTag
from LSP.plugin.core.protocol import PublishDiagnosticsParams
from LSP.plugin.core.typing import Generator
from LSP.plugin.core.url import filename_to_uri
from LSP.plugin.core.protocol import DiagnosticSeverity
from setup import TextDocumentTestCase
import sublime

Expand All @@ -9,8 +11,8 @@ class ServerNotifications(TextDocumentTestCase):

def test_publish_diagnostics(self) -> Generator:
self.insert_characters("a b c\n")
yield from self.await_client_notification("textDocument/publishDiagnostics", {
'uri': filename_to_uri(self.view.file_name()),
params = {
'uri': filename_to_uri(self.view.file_name() or ''),
'diagnostics': [
{
'message': "foo",
Expand All @@ -28,18 +30,24 @@ def test_publish_diagnostics(self) -> Generator:
'message': "baz",
'severity': DiagnosticSeverity.Information,
'source': 'qux',
'range': {'end': {'character': 5, 'line': 0}, 'start': {'character': 4, 'line': 0}}
'range': {'end': {'character': 5, 'line': 0}, 'start': {'character': 4, 'line': 0}},
'tags': [DiagnosticTag.Unnecessary]
}
]
})
} # type: PublishDiagnosticsParams
yield from self.await_client_notification("textDocument/publishDiagnostics", params)
yield lambda: len(self.view.get_regions("lspTESTd1")) > 0
yield lambda: len(self.view.get_regions("lspTESTd2")) > 0
yield lambda: len(self.view.get_regions("lspTESTd3")) > 0
yield lambda: len(self.view.get_regions("lspTESTd3_tags")) == 0
errors = self.view.get_regions("lspTESTd1")
warnings = self.view.get_regions("lspTESTd2")
info = self.view.get_regions("lspTESTd3")
self.assertEqual(len(errors), 1)
self.assertEqual(errors[0], sublime.Region(0, 1))
self.assertEqual(len(warnings), 1)
self.assertEqual(warnings[0], sublime.Region(2, 3))
self.assertEqual(len(info), 1)
self.assertEqual(info[0], sublime.Region(4, 5))

# Testing whether the popup with the diagnostic moves along with next_result
Expand Down