Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@codemirror/lsp-client

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@codemirror/lsp-client - npm Package Compare versions

Comparing version
6.2.4
to
6.2.5
+8
-0
CHANGELOG.md

@@ -0,1 +1,9 @@

## 6.2.5 (2026-06-09)
### Bug fixes
Strip backslash escapes in snippets in places where CodeMirror doesn't support them.
Support the `extraTextEdits` field in completions.
## 6.2.4 (2026-05-15)

@@ -2,0 +10,0 @@

+1
-1
{
"name": "@codemirror/lsp-client",
"version": "6.2.4",
"version": "6.2.5",
"description": "Language server protocol client for CodeMirror",

@@ -5,0 +5,0 @@ "keywords": [

import type * as lsp from "vscode-languageserver-protocol"
import {EditorState, Extension, Facet} from "@codemirror/state"
import {CompletionSource, Completion, CompletionContext, snippet, autocompletion} from "@codemirror/autocomplete"
import {EditorState, Extension, Facet, ChangeDesc, ChangeSpec} from "@codemirror/state"
import {EditorView} from "@codemirror/view"
import {CompletionSource, Completion, CompletionResult, CompletionContext,
snippet, autocompletion, insertCompletionText} from "@codemirror/autocomplete"
import {LSPPlugin} from "./plugin"
import {fromPositionChecked} from "./pos"
// Convert from LSP's snippet syntax to @codemirror/autocompletion's
// one. Remove backslashes before `$}]`, which don't support backslash
// escapes in CodeMirror, and expand `$1` to `${1}`.
function lspToSnippet(text: string): string {
return text.replace(/\\([$}\\])|\$(\d+)/g, (_m, esc, field) => esc || `\${${field}}`)
}
/// Register the [language server completion

@@ -72,2 +82,37 @@ /// source](#lsp-client.serverCompletionSource) as an autocompletion

type TextEdit = {from: number, to: number, text: string}
type ExtraEdits = {index: number, edits: readonly TextEdit[], text: string}
function resultMapper(changes: ChangeDesc | null, extraEdits: ExtraEdits[]) {
return (result: CompletionResult, newChanges: ChangeDesc): CompletionResult => {
changes = changes ? changes.composeDesc(newChanges) : newChanges
let options = result.options.slice()
for (let {index, edits, text} of extraEdits)
options[index] = {...options[index], apply: applyEdits(edits, text, changes)}
return {
...result,
options,
map: resultMapper(changes, extraEdits)
}
}
}
function applyEdits(edits: readonly TextEdit[], text: string, mapped: ChangeDesc | null) {
return (view: EditorView, completion: Completion, from: number, to: number) => {
let base = insertCompletionText(view.state, text, from, to)
let changes: ChangeSpec[] = []
for (let {from, to, text} of edits) {
if (mapped) {
if (mapped.touchesRange(from, to)) continue
let len = to - from
from = mapped.mapPos(from, 1)
to = from + len
}
changes.push({from, to, insert: text})
}
view.dispatch(base, {changes})
}
}
/// A completion source that requests completions from a language

@@ -90,6 +135,7 @@ /// server.

let config = context.state.facet(completionConfig)
let extraEdits: ExtraEdits[] = []
return {
from, to,
options: result.items.map<Completion>(item => {
options: result.items.map<Completion>((item, i) => {
let text = item.textEdit?.newText || item.textEditText || item.insertText || item.label

@@ -107,3 +153,12 @@ let option: Completion = {

if (insertTextFormat == 2 /* Snippet */) {
option.apply = (view, c, from, to) => snippet(text.replace(/\$(\d+)/g, "${$1}"))(view, c, from, to)
option.apply = (view, c, from, to) => snippet(lspToSnippet(text))(view, c, from, to)
} else if (item.additionalTextEdits) {
let edits: TextEdit[] = []
for (let edit of item.additionalTextEdits) {
let from = fromPositionChecked(context.state.doc, edit.range.start)
let to = fromPositionChecked(context.state.doc, edit.range.end)
if (from != null && to != null) edits.push({from, to, text: edit.newText})
}
extraEdits.push({index: i, text, edits})
option.apply = applyEdits(edits, text, null)
} else if (option.label != text) {

@@ -117,3 +172,3 @@ option.apply = text

validFor: result.isIncomplete ? undefined : (config.validFor ?? prefixRegexp(result.items)),
map: (result, changes) => ({...result, from: changes.mapPos(result.from)}),
map: extraEdits.length ? resultMapper(null, extraEdits) : undefined
}

@@ -120,0 +175,0 @@ }, err => {

@@ -14,1 +14,8 @@ import type * as lsp from "vscode-languageserver-protocol"

export function fromPositionChecked(doc: Text, pos: lsp.Position): number | null {
if (pos.line < 0 || pos.line >= doc.lines) return null
let line = doc.line(pos.line + 1)
if (pos.character < 0 || pos.character > line.length) return null
return line.from + pos.character
}

@@ -58,2 +58,9 @@ import type * as lsp from "vscode-languageserver-protocol"

{label: "ookay", kind: 7, documentation: "`code` stuff", insertText: "okay"},
{label: "fn", kind: 3, insertText: "fn(${1:\\$arg})$0", insertTextFormat: 2},
{label: "Bar", kind: 7, insertText: "Bar", additionalTextEdits: [
{range: {start: {line: 0, character: 0}, end: {line: 0, character: 0}}, newText: "use Bar;\n"},
]},
{label: "Zap", kind: 7, insertText: "Zap", additionalTextEdits: [
{range: {start: {line: 2, character: 0}, end: {line: 2, character: 0}}, newText: "MID\n"},
]},
]

@@ -60,0 +67,0 @@ }

@@ -284,2 +284,58 @@ import type * as lsp from "vscode-languageserver-protocol"

})
it("expands snippet completions and unescapes LSP escapes", async () => {
let {client} = setup()
let cm = ed(client, {doc: "..f", selection: {anchor: 3}, extensions: [
serverCompletion(),
autocompletion({interactionDelay: 0, activateOnTypingDelay: 10})
]})
startCompletion(cm)
await wait(60)
let cs = currentCompletions(cm.state)
ist(cs.length, 1)
ist(cs[0].label, "fn")
acceptCompletion(cm)
// `\$arg` must come through as a literal `$arg`, not the raw `\$arg`.
ist(cm.state.sliceDoc(), "..fn($arg)")
})
it("applies additionalTextEdits (e.g. an auto-import)", async () => {
let {client} = setup()
let cm = ed(client, {doc: "..b", selection: {anchor: 3}, extensions: [
serverCompletion(),
autocompletion({interactionDelay: 0, activateOnTypingDelay: 10})
]})
startCompletion(cm)
await wait(60)
let cs = currentCompletions(cm.state)
ist(cs.length, 1)
ist(cs[0].label, "Bar")
acceptCompletion(cm)
// The completion inserts "Bar" *and* the item's additionalTextEdits add
// the `use Bar;` line at the top.
ist(cm.state.sliceDoc(), "use Bar;\n..Bar")
})
it("maps additionalTextEdit positions through changes that arrive while the completion is active", async () => {
let {client} = setup()
// Server's "Zap" completion ships an additionalTextEdit at line 2,
// char 0 — i.e. the start of "L2" in this document.
let cm = ed(client, {doc: "L0\nL1\nL2\n..z", selection: {anchor: 12}, extensions: [
serverCompletion(),
autocompletion({interactionDelay: 0, activateOnTypingDelay: 10})
]})
startCompletion(cm)
await wait(60)
let cs = currentCompletions(cm.state)
ist(cs.length, 1)
ist(cs[0].label, "Zap")
// Insert a line at the very top. This is far from the cursor, so CM
// keeps the completion alive — but the LSP edit's `line: 2` now refers
// to a different line in the new document.
cm.dispatch({changes: {from: 0, insert: "HEADER\n"}})
acceptCompletion(cm)
// The "MID\n" insertion should land before the *original* L2 line,
// which after the HEADER insertion is the third line.
ist(cm.state.sliceDoc(), "HEADER\nL0\nL1\nMID\nL2\n..Zap")
})
})

@@ -286,0 +342,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display