Repository: sublimelsp/LSP

Issue:
bug: labelDetails.detail formatting
**Describe the bug**

https://discord.com/channels/280102180189634562/645268178397560865/1062557137773002762

```
:: <<< lsp-elixir 5: {'items': [{'sortText': '00000000', 'label': 'NaiveDateTime', 'deprecated': False, 'documentation': {'kind': 'markdown', 'value': 'A NaiveDateTime struct (without a time zone) and functions.'}, 'insertTextFormat': 2, 'kind': 22, 'tags': [], 'insertText': 'NaiveDateTime', 'detail': 'struct', 'labelDetails': {'description': 'NaiveDateTime', 'detail': 'struct'}, 'filterText': 'NaiveDateTime'}, {'sortText': '00000001', 'label': 'NimbleCSV', 'deprecated': False, 'documentation': {'kind': 'markdown', 'value': 'NimbleCSV is a small and fast parsing and dumping library.'}, 'insertTextFormat': 2, 'kind': 8, 'tags': [], 'insertText': 'NimbleCSV', 'detail': 'behaviour', 'labelDetails': {'description': 'NimbleCSV', 'detail': 'behaviour'}, 'filterText': 'NimbleCSV'}, {'sortText': '00000002', 'label': 'Node', 'deprecated': False, 'documentation': {'kind': 'markdown', 'value': 'Functions related to VM nodes.'}, 'insertTextFormat': 2, 'kind': 9, 'tags': [], 'insertText': 'Node', 'detail': 'module', 'labelDetails': {'description': 'Node'}, 'filterText': 'Node'}, {'documentation': {'kind': 'markdown', 'value': 'Postgrex.Extensions.Name\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Name', 'labelDetails': {'description': 'alias Postgrex.Extensions.Name'}, 'label': 'Name', 'sortText': '00000003', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Postgrex.Extensions.Name\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Name', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Erl2ex.Pipeline.Names\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Names', 'labelDetails': {'description': 'alias Erl2ex.Pipeline.Names'}, 'label': 'Names', 'sortText': '00000004', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Erl2ex.Pipeline.Names\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Names', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Phoenix.Naming\nConveniences for inflecting and working with names in Phoenix.\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Naming', 'labelDetails': {'description': 'alias Phoenix.Naming'}, 'label': 'Naming', 'sortText': '00000005', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Phoenix.Naming\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Naming', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Phoenix.LiveDashboard.NavBarComponent\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NavBarComponent', 'labelDetails': {'description': 'alias Phoenix.LiveDashboard.NavBarComponent'}, 'label': 'NavBarComponent', 'sortText': '00000006', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Phoenix.LiveDashboard.NavBarComponent\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NavBarComponent', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'JWT.Claim.Nbf\nNot before'}, 'deprecated': False, 'tags': [], 'insertText': 'Nbf', 'labelDetails': {'description': 'alias JWT.Claim.Nbf'}, 'label': 'Nbf', 'sortText': '00000007', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias JWT.Claim.Nbf\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Nbf', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Mint.Negotiate\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Negotiate', 'labelDetails': {'description': 'alias Mint.Negotiate'}, 'label': 'Negotiate', 'sortText': '00000008', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Mint.Negotiate\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Negotiate', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Hex.Netrc\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Netrc', 'labelDetails': {'description': 'alias Hex.Netrc'}, 'label': 'Netrc', 'sortText': '00000009', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Hex.Netrc\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Netrc', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Mix.Tasks.New\nCreates a new Elixir project.\nIt expects the path of the project as argument.'}, 'deprecated': False, 'tags': [], 'insertText': 'New', 'labelDetails': {'description': 'alias Mix.Tasks.New', 'detail': 'task'}, 'label': 'New', 'sortText': '00000010', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Mix.Tasks.New\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'New', 'detail': 'task'}, {'documentation': {'kind': 'markdown', 'value': 'Ecto.NoPrimaryKeyFieldError\nRaised at runtime when an operation that requires a primary key is invoked\nwith a schema that does not define a primary key by using `@primary_key false`\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NoPrimaryKeyFieldError', 'labelDetails': {'description': 'alias Ecto.NoPrimaryKeyFieldError', 'detail': 'exception'}, 'label': 'NoPrimaryKeyFieldError', 'sortText': '00000011', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Ecto.NoPrimaryKeyFieldError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NoPrimaryKeyFieldError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'Ecto.NoPrimaryKeyValueError\nRaised at runtime when an operation that requires a primary key is invoked\nwith a schema missing value for its primary key\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NoPrimaryKeyValueError', 'labelDetails': {'description': 'alias Ecto.NoPrimaryKeyValueError', 'detail': 'exception'}, 'label': 'NoPrimaryKeyValueError', 'sortText': '00000012', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Ecto.NoPrimaryKeyValueError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NoPrimaryKeyValueError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'Mix.NoProjectError\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NoProjectError', 'labelDetails': {'description': 'alias Mix.NoProjectError', 'detail': 'exception'}, 'label': 'NoProjectError', 'sortText': '00000013', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Mix.NoProjectError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NoProjectError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'Ecto.NoResultsError\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NoResultsError', 'labelDetails': {'description': 'alias Ecto.NoResultsError', 'detail': 'exception'}, 'label': 'NoResultsError', 'sortText': '00000014', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Ecto.NoResultsError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NoResultsError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'Phoenix.Router.NoRouteError\nException raised when no route is found.\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NoRouteError', 'labelDetails': {'description': 'alias Phoenix.Router.NoRouteError', 'detail': 'exception'}, 'label': 'NoRouteError', 'sortText': '00000015', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Phoenix.Router.NoRouteError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NoRouteError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'Mix.NoTaskError\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NoTaskError', 'labelDetails': {'description': 'alias Mix.NoTaskError', 'detail': 'exception'}, 'label': 'NoTaskError', 'sortText': '00000016', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Mix.NoTaskError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NoTaskError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'Phoenix.LiveDashboard.ReingoldTilford.Node\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Node', 'labelDetails': {'description': 'alias Phoenix.LiveDashboard.ReingoldTilford.Node', 'detail': 'struct'}, 'label': 'Node', 'sortText': '00000017', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Phoenix.LiveDashboard.ReingoldTilford.Node\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Node', 'detail': 'struct'}, {'documentation': {'kind': 'markdown', 'value': 'Phoenix.ChannelTest.NoopSerializer\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NoopSerializer', 'labelDetails': {'description': 'alias Phoenix.ChannelTest.NoopSerializer'}, 'label': 'NoopSerializer', 'sortText': '00000018', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Phoenix.ChannelTest.NoopSerializer\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NoopSerializer', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'ElixirSense.Core.Normalized\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Normalized', 'labelDetails': {'description': 'alias ElixirSense.Core.Normalized'}, 'label': 'Normalized', 'sortText': '00000019', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias ElixirSense.Core.Normalized\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Normalized', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Code.Normalizer\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Normalizer', 'labelDetails': {'description': 'alias Code.Normalizer'}, 'label': 'Normalizer', 'sortText': '00000020', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Code.Normalizer\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Normalizer', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Phoenix.NotAcceptableError\nRaised when one of the `accept*` headers is not accepted by the server.'}, 'deprecated': False, 'tags': [], 'insertText': 'NotAcceptableError', 'labelDetails': {'description': 'alias Phoenix.NotAcceptableError', 'detail': 'exception'}, 'label': 'NotAcceptableError', 'sortText': '00000021', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Phoenix.NotAcceptableError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NotAcceptableError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'Ecto.Association.NotLoaded\nStruct returned by associations when they are not loaded.'}, 'deprecated': False, 'tags': [], 'insertText': 'NotLoaded', 'labelDetails': {'description': 'alias Ecto.Association.NotLoaded', 'detail': 'struct'}, 'label': 'NotLoaded', 'sortText': '00000022', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Ecto.Association.NotLoaded\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NotLoaded', 'detail': 'struct'}, {'documentation': {'kind': 'markdown', 'value': 'Plug.Conn.NotSentError\nError raised when no response is sent in a request\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NotSentError', 'labelDetails': {'description': 'alias Plug.Conn.NotSentError', 'detail': 'exception'}, 'label': 'NotSentError', 'sortText': '00000023', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Plug.Conn.NotSentError\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NotSentError', 'detail': 'exception'}, {'documentation': {'kind': 'markdown', 'value': 'App.Trade.Order.Note\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Note', 'labelDetails': {'description': 'alias App.Trade.Order.Note', 'detail': 'struct'}, 'label': 'Note', 'sortText': '00000024', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias App.Trade.Order.Note\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Note', 'detail': 'struct'}, {'documentation': {'kind': 'markdown', 'value': 'ElixirLS.LanguageServer.Experimental.Protocol.Proto.Notification\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Notification', 'labelDetails': {'description': 'alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Notification'}, 'label': 'Notification', 'sortText': '00000025', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias ElixirLS.LanguageServer.Experimental.Protocol.Proto.Notification\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Notification', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Postgrex.Notifications\nAPI for notifications (pub/sub) in PostgreSQL.'}, 'deprecated': False, 'tags': [], 'insertText': 'Notifications', 'labelDetails': {'description': 'alias Postgrex.Notifications', 'detail': 'struct'}, 'label': 'Notifications', 'sortText': '00000026', 'insertTextFormat': 2, 'kind': 22, 'additionalTextEdits': [{'newText': '  alias Postgrex.Notifications\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Notifications', 'detail': 'struct'}, {'documentation': {'kind': 'markdown', 'value': 'ElixirLS.LanguageServer.Experimental.Protocol.Notifications\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Notifications', 'labelDetails': {'description': 'alias ElixirLS.LanguageServer.Experimental.Protocol.Notifications'}, 'label': 'Notifications', 'sortText': '00000027', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias ElixirLS.LanguageServer.Experimental.Protocol.Notifications\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Notifications', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Mix.Tasks.Phx.Gen.Notifier\nGenerates a notifier that delivers emails by default.'}, 'deprecated': False, 'tags': [], 'insertText': 'Notifier', 'labelDetails': {'description': 'alias Mix.Tasks.Phx.Gen.Notifier', 'detail': 'task'}, 'label': 'Notifier', 'sortText': '00000028', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Mix.Tasks.Phx.Gen.Notifier\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Notifier', 'detail': 'task'}, {'documentation': {'kind': 'markdown', 'value': 'EctoPSQLExtras.NullIndexes\n'}, 'deprecated': False, 'tags': [], 'insertText': 'NullIndexes', 'labelDetails': {'description': 'alias EctoPSQLExtras.NullIndexes'}, 'label': 'NullIndexes', 'sortText': '00000029', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias EctoPSQLExtras.NullIndexes\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'NullIndexes', 'detail': 'module'}, {'documentation': {'kind': 'markdown', 'value': 'Postgrex.Extensions.Numeric\n'}, 'deprecated': False, 'tags': [], 'insertText': 'Numeric', 'labelDetails': {'description': 'alias Postgrex.Extensions.Numeric'}, 'label': 'Numeric', 'sortText': '00000030', 'insertTextFormat': 2, 'kind': 9, 'additionalTextEdits': [{'newText': '  alias Postgrex.Extensions.Numeric\n', 'range': {'end': {'line': 1, 'character': 0}, 'start': {'line': 1, 'character': 0}}}], 'filterText': 'Numeric', 'detail': 'module'}], 'isIncomplete': True}
```

is not formatted properly, see blow

**To Reproduce**
..

**Expected behavior**
Formatted with proper space in between `NativeDateTime` and `struct`

**Screenshots**

![image](https://user-images.githubusercontent.com/1329716/211927390-e99fe40e-6c78-43ab-93d3-26f467ae7dbc.png)

**Environment (please complete the following information):**
- OS: Windows 11
- Sublime Text version: 4147
- LSP version: 1.21.0
- Language servers used: elixir (LSP-elixir 0.6.0 - elixir-ls 0.13.0)



Discussion:
As @rwols reminded us, the idea is that the `detail` field would have function parameters and would look good when just appended to the trigger. But that's of course not guaranteed. So if we'd just show it after a space, it should be fine for both cases.
According to the [LSP specs](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails), there should be no space:

```typescript
	/**
	 * An optional string which is rendered less prominently directly after
	 * {@link CompletionItem.label label}, without any spacing. Should be
	 * used for function signatures or type annotations.
	 */
	detail?: string;
```

I would say the fact that the specs explicitly mention that the client should render it without any spacing, while in general the LSP specs are written in a way to stay away from UI details, makes it pretty clear that we're compliant here and probably doing the best what is possible in ST.

Also note that some language servers, e.g. the D server, might add the space by themselves: https://github.com/sublimelsp/LSP/issues/1924#issuecomment-1002367219
> According to the [LSP specs](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemLabelDetails), there should be no space:
> 
> ```ts
> 	/**
> 	 * An optional string which is rendered less prominently directly after
> 	 * {@link CompletionItem.label label}, without any spacing. Should be
> 	 * used for function signatures or type annotations.
> 	 */
> 	detail?: string;
> ```
> 
> I would say the fact that the specs explicitly mention that the client should render it without any spacing, while in general the LSP specs are written in a way to stay away from UI details, makes it pretty clear that we're compliant here and probably doing the best what is possible in ST.
> 
> Also note that some language servers, e.g. the D server, might add the space by themselves: [#1924 (comment)](https://github.com/sublimelsp/LSP/issues/1924#issuecomment-1002367219)

Cool, I'll open an issue at the language server.
But it also says:

>  or type annotations.

Which is pretty much the case here. I wonder how it expects this to be rendered without any spaces. Maybe like `: struct` but I don't know if that makes sense in elixir.
FWIW, I think that this matter is a quite bad design choice in the specification, in that the `labelDetails.detail` should be used for both function signatures or type annotations. Function signatures usually start with `(`, so that the inconvenience from your screenshot doesn't occur for those even without spacing. All those different fields (`detail`, `documentation`, `labelDetails.detail`, `labelDetails.description`, and all of them are optional) make it quite difficult for clients and for servers to decide where to put what information. Apparently this led to much confusion and to that various servers doing things differently. And regarding the "rendered less prominently" from the specs description, I think that the majority of editors don't have this ability, and it's too much based on VSCode's UI here.

So maybe it would be okay for this client to add a workaround like
```python
if not label_details_detail.startswith((' ', '(')):
    label_details_detail = ' ' + label_details_detail
```
but then the LSP specs should be changed in a way that the client is allowed to add additional spacing. Right now, I think adding such a direct spec violation here wouldn't be a proper solution.

I created an issue in the LSP repo to clarify this (see link above).
Radio silence from MS. Could we consider mitigating it in LSP for now? The workaround logic is pretty simple. Cheers.
I think it would be okay to add a workaround. To be honest, I would probably even move `labelDetails.detail` from the "trigger" to "annotation" on the right side, if it starts with a letter (which likely indicates that it is a type annotation), and only show it in the "trigger" if it is used for a function signature, i.e. starts with `(` ("normal" languages) or `[` (e.g. Mathematica :zany_face:).

There is also recent discussion about the heuristic for when to use `filterText` in the completion popup, so it should be considered whether there are impacts between those if the implementation gets changed.
> I would probably even move `labelDetails.detail` from the "trigger" to "annotation" on the right side

I'd stick to the space approach. As can be seen in my example the right hand side also shows from which module the symbol comes.

Thanks for the insight though!

Resolving patch:
diff --git a/plugin/completion.py b/plugin/completion.py
--- a/plugin/completion.py
+++ b/plugin/completion.py
@@ -65,7 +65,14 @@ def format_completion(
         href = 'subl:lsp_run_text_command_helper {}'.format(args)
         details.append(make_link(href, 'More'))
     if lsp_label_detail and (lsp_label + lsp_label_detail).startswith(lsp_filter_text):
-        trigger = lsp_label + lsp_label_detail
+        if lsp_label_detail[0].isalnum() and lsp_label.startswith(lsp_filter_text):
+            # labelDetails.detail is likely a type annotation
+            # Don't append it to the trigger: https://github.com/sublimelsp/LSP/issues/2169
+            trigger = lsp_label
+            details.append(html.escape(lsp_label_detail))
+        else:
+            # labelDetails.detail is likely a function signature
+            trigger = lsp_label + lsp_label_detail
         annotation = lsp_label_description or lsp_detail
     elif lsp_label.startswith(lsp_filter_text):
         trigger = lsp_label


Test patch:
diff --git a/tests/test_completion.py b/tests/test_completion.py
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -946,6 +946,33 @@ def test_label_details(self) -> None:
             trigger='banner?()',
         )
 
+    def test_label_details_2(self) -> None:
+        self._verify_completion(
+            {
+                "label": "NaiveDateTime",
+                "labelDetails": {
+                    "detail": "struct",
+                    "description": "NaiveDateTime"
+                },
+            },
+            trigger='NaiveDateTime',
+            annotation='NaiveDateTime',
+            details='struct'
+        )
+
+    def test_label_details_3(self) -> None:
+        self._verify_completion(
+            {
+                "label": "NaiveDateTime",
+                "labelDetails": {
+                    "detail": " struct",
+                    "description": "NaiveDateTime"
+                },
+            },
+            trigger='NaiveDateTime struct',
+            annotation='NaiveDateTime'
+        )
+
     def test_filter_text_1(self) -> None:
         self._verify_completion(
             {