Skip to content
19 changes: 18 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,28 @@ The `--no-build` flag skips recompiling the test project, which means type error
code are silently ignored and the stale binary from a previous build is used instead.
There is no meaningful performance win from skipping the build step.

## Anonymize private code in plans, prose, and examples

When writing anything that will be committed or otherwise persisted (`plans/*.md`,
design docs, PR descriptions, code comments) and it draws on a real session against a
user's private solution — namespaces, type names, file names, log excerpts — replace
those with fictional equivalents (e.g. `Fabrikam.Processing`, `ExchangeRateSource`,
`MiscDataCleanupHandlers.cs`) before committing. Keep the *shape* of the example intact
(same structure, same character offsets/prefixes where they matter to the point being
made) so the technical content is unaffected — only the identifying names change. This
does not apply to this project's own public identifiers (namespaces under
`CSharpLanguageServer.*`, file paths under `src/`, GitHub issue/PR numbers on this repo)
since those aren't private.

## Code Style & Commit Conventions

See [CONTRIBUTING.md](CONTRIBUTING.md#code-style).

Commit messages should be a short single-line summary — no bullet lists of individual changes.
Commit messages use [Conventional Commits](https://www.conventionalcommits.org/) style —
`<type>(<scope>): <short summary>` (e.g. `fix(completion): avoid NullReferenceException on
empty import list`) — as a short single-line summary, no bullet lists of individual changes.
See [CONTRIBUTING.md#commit-messages](CONTRIBUTING.md#commit-messages) for the full list of
`type`s and more examples.

## Key Docs

Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]
* Re-enable completion suggestions for types from unimported namespaces, now inserting the
missing `using` (via `additionalTextEdits`) when one of those items is accepted, gated behind
the new `csharp.completion.completeUnimportedTypes` setting, defaulting to `true`; also add
`csharp.completion.showNameSuggestions` to control whether `textDocument/completion` includes
suggested identifier names (e.g. `stringBuilder` for a `StringBuilder`), defaulting to `false`
and matching the previous hardcoded behavior
- Reported by @kervanaslan in https://github.com/razzmatazz/csharp-language-server/issues/210 and
fixed by @razzmatazz in https://github.com/razzmatazz/csharp-language-server/pull/425
* Stop reporting `textDocument/completion` results as incomplete, which was forcing editors to
re-request the full completion list (including the unimported-namespace scan above) on every
keystroke instead of filtering the already-returned list locally; this could produce a large,
redundant `textDocument/completion` response per character typed on large solutions

## [0.28.0] - 2026-09-13 / Klebiškis
* Speed up analyzer-enabled diagnostics by sharing project-wide analyzer results across document requests for the same Roslyn solution snapshot
- Reported and fixed by @alsi-lawr in https://github.com/razzmatazz/csharp-language-server/issues/403 and https://github.com/razzmatazz/csharp-language-server/pull/404
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ Settings are read from the `csharp` workspace configuration section
- `csharp.razorSupport` - enable Razor (`.cshtml`) document support;
defaults to `false`

- `csharp.completion.completeUnimportedTypes` - include types from namespaces
that aren't yet imported in `textDocument/completion` results, inserting the
missing `using` (via `additionalTextEdits`) when one of those items is
accepted; may increase completion latency on large solutions; defaults to
`true`

- `csharp.completion.showNameSuggestions` - include suggested identifier names
(e.g. `stringBuilder` when completing after `StringBuilder `) in
`textDocument/completion` results; may increase completion latency;
defaults to `false`

- `csharp.solutionPathOverride` - override the solution path to load; useful for
specifying an alternative solution when multiple exist in the workspace;
can also be set via the `--solution` CLI flag; defaults to `null`
Expand Down
84 changes: 74 additions & 10 deletions src/CSharpLanguageServer/Handlers/Completion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,29 @@ module Completion =
member __.GetDescriptionAsync(doc, item, ct) =
service.GetDescriptionAsync(doc, item, ct)

/// Builds Roslyn completion options from the effective `csharp.completion.*` configuration.
/// `ShowItemsFromUnimportedNamespaces` defaults to `true` (re-enabling suggestions for
/// types whose namespace isn't imported yet — https://github.com/razzmatazz/csharp-language-server/issues/210).
/// Selecting one of those items commits as a complex text edit (see `IsComplexTextEdit` below)
/// that both inserts the type name and adds the missing `using` — `completeUnimportedTypes`
/// controls both behaviors together, since offering an item that doesn't compile without an
/// import the user has to add by hand isn't very useful on its own.
/// `ShowNameSuggestions` defaults to `false` since it's rarely useful over LSP and adds latency.
let private getRoslynCompletionOptions (context: RequestContext) =
let completeUnimportedTypes =
context.Config.completion
|> Option.bind _.completeUnimportedTypes
|> Option.defaultValue true

let showNameSuggestions =
context.Config.completion
|> Option.bind _.showNameSuggestions
|> Option.defaultValue false

RoslynCompletionOptions.Default()
|> _.WithBool("ShowItemsFromUnimportedNamespaces", completeUnimportedTypes)
|> _.WithBool("ShowNameSuggestions", showNameSuggestions)

let private dynamicRegistration (cc: ClientCapabilities) =
cc.TextDocument
|> Option.bind _.Completion
Expand Down Expand Up @@ -313,10 +336,7 @@ module Completion =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
|> RoslynCompletionServiceWrapper

let completionOptions =
RoslynCompletionOptions.Default()
|> _.WithBool("ShowItemsFromUnimportedNamespaces", false)
|> _.WithBool("ShowNameSuggestions", false)
let completionOptions = context |> getRoslynCompletionOptions

let completionTrigger = p.Context |> codeActionContextToCompletionTrigger

Expand Down Expand Up @@ -355,10 +375,7 @@ module Completion =
Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)
|> RoslynCompletionServiceWrapper

let completionOptions =
RoslynCompletionOptions.Default()
|> _.WithBool("ShowItemsFromUnimportedNamespaces", false)
|> _.WithBool("ShowNameSuggestions", false)
let completionOptions = context |> getRoslynCompletionOptions

let completionTrigger = p.Context |> codeActionContextToCompletionTrigger

Expand Down Expand Up @@ -417,7 +434,14 @@ module Completion =
lspCompletionItemsWithCacheInfo |> Array.map (fun (item, _, _, _) -> item)

return
{ IsIncomplete = true
// Roslyn's `CompletionList` has no concept of "there's more, ask again" — it
// always returns its full candidate set for the current cursor context in one
// call, unfiltered by the typed prefix (LSP expects the client to filter
// `items` locally using `filterText` as the user keeps typing). Reporting
// `IsIncomplete = true` here would instead make the client re-request on every
// keystroke via `TriggerForIncompleteCompletions`, redoing this same
// (potentially expensive, e.g. unimported-namespace) computation each time.
{ IsIncomplete = false
Items = items
ItemDefaults = None }
|> U2.C2
Expand All @@ -426,6 +450,42 @@ module Completion =
LspWorkspaceUpdate.Empty
}

/// For completion items that need more than inserting `DisplayText` at their original
/// `Span` on commit (`IsComplexTextEdit`) — e.g. an item from an unimported namespace,
/// which also needs a `using` inserted at the top of the file — resolves the full set of
/// changes Roslyn would make and returns everything other than the primary in-place edit
/// as LSP `AdditionalTextEdits`, so accepting the item also adds the missing import.
let private getAdditionalTextEditsForComplexItem
(completionService: Microsoft.CodeAnalysis.Completion.CompletionService)
(doc: Document)
(item: Microsoft.CodeAnalysis.Completion.CompletionItem)
(ct: System.Threading.CancellationToken)
: Async<TextEdit[] option> =
async {
if not item.IsComplexTextEdit then
return None
else
let! change =
completionService.GetChangeAsync(doc, item, System.Nullable(), ct)
|> Async.AwaitTask

if change.TextChanges.IsDefaultOrEmpty then
return None
else
let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask

let additionalEdits =
change.TextChanges
|> Seq.filter (fun tc -> tc.Span <> item.Span)
|> Seq.map (TextEdit.fromTextChange sourceText.Lines)
|> Array.ofSeq

return
match additionalEdits with
| [||] -> None
| edits -> Some edits
}

let resolve
(_context: RequestContext)
(item: CompletionItem)
Expand Down Expand Up @@ -462,10 +522,14 @@ module Completion =
Value = d }
|> U2.C2)

let! additionalTextEdits =
getAdditionalTextEditsForComplexItem completionService doc roslynCompletionItem ct

return
{ item with
Detail = synopsis
Documentation = updatedItemDocumentation }
Documentation = updatedItemDocumentation
AdditionalTextEdits = additionalTextEdits |> Option.orElse item.AdditionalTextEdits }
|> LspResult.success,
LspWorkspaceUpdate.Empty

Expand Down
21 changes: 21 additions & 0 deletions src/CSharpLanguageServer/Types.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@ type CSharpDebugConfiguration =
{ solutionLoadDelay = None
debugMode = None }

type CSharpCompletionConfiguration =
{ completeUnimportedTypes: bool option
showNameSuggestions: bool option }

static member Default =
{ completeUnimportedTypes = None
showNameSuggestions = None }

type CSharpConfiguration =
{ logLevel: string option
applyFormattingOptions: bool option
analyzersEnabled: bool option
useMetadataUris: bool option
razorSupport: bool option
completion: CSharpCompletionConfiguration option
locale: string option
debug: CSharpDebugConfiguration option
solutionPathOverride: string option }
Expand All @@ -34,6 +43,7 @@ type CSharpConfiguration =
analyzersEnabled = None
useMetadataUris = None
razorSupport = None
completion = None
locale = None
debug = None
solutionPathOverride = None }
Expand All @@ -46,6 +56,17 @@ let mergeCSharpConfiguration (oldConfig: CSharpConfiguration) (newConfig: CSharp
analyzersEnabled = newConfig.analyzersEnabled |> Option.orElse oldConfig.analyzersEnabled
useMetadataUris = newConfig.useMetadataUris |> Option.orElse oldConfig.useMetadataUris
razorSupport = newConfig.razorSupport |> Option.orElse oldConfig.razorSupport
completion =
match newConfig.completion with
| Some newCompletion ->
Some
{ completeUnimportedTypes =
newCompletion.completeUnimportedTypes
|> Option.orElse (oldConfig.completion |> Option.bind _.completeUnimportedTypes)
showNameSuggestions =
newCompletion.showNameSuggestions
|> Option.orElse (oldConfig.completion |> Option.bind _.showNameSuggestions) }
| None -> oldConfig.completion
locale = newConfig.locale |> Option.orElse oldConfig.locale
debug =
match newConfig.debug with
Expand Down
Loading
Loading