fix(completion): rank word completion candidates by scope tier (#555) - #577
Merged
msarson merged 1 commit intoSep 17, 2026
Merged
Conversation
…on#555) WordCompletionProvider set no sortText anywhere, so the client ordered every candidate alphabetically against every other one. A project-wide EQUATE from the declaration index (msarson#554) could therefore outrank a local variable on a shared prefix by pure alphabetical luck. Assigns one tier per source, stamped as a padded sortText prefix: locals and parameters, then module and PROGRAM data, then MAP procedures, then project-wide index entries, then the static catalogs (keywords, built-ins, data types, controls, attributes, directives), each tier alphabetical within itself. Two details worth calling out: Every candidate gets a tier, including the catalogs the issue does not name. Leaving any of them unstamped would not preserve their position: the client compares an unstamped item's label against a stamped item's sortText, and since digits precede letters, every unstamped candidate would sink below every stamped one. The catalogs reach `seen` without going through add(), so a post-pass stamps whatever is left. The tier number is zero-padded to two digits and the label half lowercased. Without padding a tenth tier would sort between 1 and 2; without lowercasing, Clarion being case-insensitive, MyConst and myconst would swap places depending on capitalisation. Two digits is deliberate — the tiers are spaced by ten, leaving room for nine more between any two existing ones against a ceiling of 99. add() stamps sortText only on first insertion, never on the merge path, so the first (higher-priority) tier to claim a label keeps its rank — the same first-writer-wins rule the closure already followed. A small addIn(tier) wrapper means none of the 13 collector signatures changed. Tests: 3 new cases — the issue's stated ordering (local, PROGRAM global, project-wide EQUATE on a shared prefix), each tier forming one contiguous alphabetical block under case-insensitive comparison, and every candidate carrying a sortText. All 3 fail against the unstamped provider and pass with it. Full suite: 3053 passing, 4 pending (pre-existing, unrelated), 0 failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
msarson
added a commit
that referenced
this pull request
Sep 17, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(completion): rank word completion candidates by scope tier (#555)
Closes #555.
Branch (pushed):
geircodes/Clarion-Extension:fix/word-completion-sorttext-tiers→version-1.0.4Motivation
WordCompletionProviderset nosortTextanywhere, so the client ordered every candidate alphabetically against every other one. Once #554 added project-wide EQUATEs from the declaration index, a cross-file constant could outrank a local variable on a shared prefix by pure alphabetical luck — the constant is valid to the compiler so it belongs in the list, but it shouldn't compete for the top of it.The tiers
One tier per source, alphabetical within each:
collectVariables,collectParameters, the document's own EQUATEscollectProgramGlobalDataSymbolscollectProcedurescollectProjectEquatesThis matches what tsserver does with its own
SortTextenum — locals first, globals and keywords later, auto-import suggestions (the closest analogue to a cross-file EQUATE reached through an un-inlinedINCLUDE) last.Two details worth review attention
Every candidate gets a tier, including the catalogs the issue doesn't name. Leaving any unstamped would not preserve their position: the client compares an unstamped item's
labelagainst a stamped item'ssortText, and since digits precede letters, every unstamped candidate would sink below every stamped one. The catalogs reachseenwithout going throughadd(), so a post-pass stamps whatever is left:The tier number is zero-padded and the label half lowercased. Without padding, a tenth tier would sort between 1 and 2. Without lowercasing — Clarion being case-insensitive —
MyConstandmyconstwould swap places depending on capitalisation. Two digits is deliberate: the tiers are spaced by ten, so there is room for nine more between any two existing ones and the ceiling is 99.Implementation
add()stampssortTextonly on first insertion, never on the merge path, so the first (higher-priority) tier to claim a label keeps its rank — the same first-writer-wins rule the closure already followed forlabel/kind. A smalladdIn(tier)wrapper binds a tier to the callback, so none of the 13 collector signatures had to change:collectEquatestakes a second callback for its project-wide delegation, so the document's own EQUATEs rank as locals while indexed ones rank at tier 4 — the actual complaint in #555.Testing
WordCompletionSortTextTiers555.test.ts— 3 tests:sortTexttoLowerCase())sortText, so none is ranked by its bare labelProved real: reverted the provider, recompiled, reran — all 3 fail. Restored → all 3 pass.
The fixture is a real PROGRAM + MEMBER pair on disk (the #565 pattern, since PROGRAM globals are read from the file) plus a prototype-stubbed SDI and SolutionManager (the #312/#554 pattern), so all three tiers have a candidate sharing the prefix without needing a solution load.
Full suite: 3053 passing, 4 pending, 0 failing (pre-existing pendings, unrelated) — no regressions.
Performance
No measurable cost. Measured against a large PROGRAM file (770 KB, 14,512 lines) with a ~20,000-entry declaration index, averaged over repeated requests:
The per-request stage breakdown puts the tier post-pass at 0–1 ms against a total dominated entirely by tokenization. An A/B on the same file — same typing sequence,
sortTextstamped versus omitted entirely — produced statistically identical request times (slow requests 649–716 ms with tiers, 668–698 ms without; fast requests 18–22 ms versus 21–29 ms), with ~97% of every slow request attributable to re-lexing rather than to anything in this change.Scope
sortTextis set.editor.suggestSelection(recentlyUsed/recentlyUsedByPrefix), and that changes which item is preselected, not the order — so it layers on top of these tiers rather than competing with them. LSP also gives the server no signal about which completion was accepted, so recency can't be tracked server-side anyway. Tiers stay pure scope distance.PROJECT_EQUATE_LIMITtruncation is a separate, pre-existing issue, not addressed here.collectProjectEquateswalksindex.byName— a plainMapin file-scan order — and stops at 300 matches, so a broad prefix silently drops candidates that a narrower one reveals (e.g. typingAB_can omit entries thatAB_Cthen shows). That drops valid candidates rather than merely misordering them, so it deserves its own issue and fix.