Skip to content

Commit 9b6a753

Browse files
authored
Merge pull request #162 from atom-community/additionalTextEdits
2 parents 2317a7d + bf63a49 commit 9b6a753

File tree

5 files changed

+77
-5
lines changed

5 files changed

+77
-5
lines changed

lib/adapters/autocomplete-adapter.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
ServerCapabilities,
1818
TextEdit,
1919
} from "../languageclient"
20+
import ApplyEditAdapter from "./apply-edit-adapter"
2021
import { Point, TextEditor } from "atom"
2122
import * as ac from "atom/autocomplete-plus"
22-
import { Suggestion, TextSuggestion, SnippetSuggestion } from "../types/autocomplete-extended"
23+
import { Suggestion, TextSuggestion, SnippetSuggestion, SuggestionBase } from "../types/autocomplete-extended"
2324

2425
/**
2526
* Defines the behavior of suggestion acceptance. Assume you have "cons|ole" in the editor ( `|` is the cursor position)
@@ -437,6 +438,7 @@ export default class AutocompleteAdapter {
437438
suggestion.displayText = item.label
438439
suggestion.type = AutocompleteAdapter.completionKindToSuggestionType(item.kind)
439440
AutocompleteAdapter.applyDetailsToSuggestion(item, suggestion)
441+
suggestion.completionItem = item
440442
}
441443

442444
public static applyDetailsToSuggestion(item: CompletionItem, suggestion: Suggestion): void {
@@ -494,6 +496,24 @@ export default class AutocompleteAdapter {
494496
suggestion.text = textEdit.newText
495497
}
496498

499+
/**
500+
* Handle additional text edits after a suggestion insert, e.g. `additionalTextEdits`.
501+
*
502+
* `additionalTextEdits` are An optional array of additional text edits that are applied when selecting this
503+
* completion. Edits must not overlap (including the same insert position) with the main edit nor with themselves.
504+
*
505+
* Additional text edits should be used to change text unrelated to the current cursor position (for example adding an
506+
* import statement at the top of the file if the completion item will insert an unqualified type).
507+
*/
508+
public static applyAdditionalTextEdits(event: ac.SuggestionInsertedEvent): void {
509+
const suggestion = event.suggestion as SuggestionBase
510+
const additionalEdits = suggestion.completionItem?.additionalTextEdits
511+
const buffer = event.editor.getBuffer()
512+
513+
ApplyEditAdapter.applyEdits(buffer, Convert.convertLsTextEdits(additionalEdits))
514+
buffer.groupLastChanges()
515+
}
516+
497517
/**
498518
* Public: Adds a snippet to the suggestion if the CompletionItem contains snippet-formatted text
499519
*

lib/auto-languageclient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,10 @@ export default class AutoLanguageClient {
638638
excludeLowerPriority: false,
639639
filterSuggestions: true,
640640
getSuggestions: this.getSuggestions.bind(this),
641-
onDidInsertSuggestion: this.onDidInsertSuggestion.bind(this),
641+
onDidInsertSuggestion: (event) => {
642+
AutocompleteAdapter.applyAdditionalTextEdits(event)
643+
this.onDidInsertSuggestion(event)
644+
},
642645
getSuggestionDetailsOnSelect: this.getSuggestionDetailsOnSelect.bind(this),
643646
}
644647
}

lib/convert.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export default class Convert {
199199
* @param textEdits The language server protocol {atomIde.TextEdit} objects to convert.
200200
* @returns An {Array} of Atom {atomIde.TextEdit} objects.
201201
*/
202-
public static convertLsTextEdits(textEdits: ls.TextEdit[] | null): atomIde.TextEdit[] {
202+
public static convertLsTextEdits(textEdits?: ls.TextEdit[] | null): atomIde.TextEdit[] {
203203
return (textEdits || []).map(Convert.convertLsTextEdit)
204204
}
205205

lib/types/autocomplete-extended.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// See this PR: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/51284
33

44
import * as ac from "atom/autocomplete-plus"
5+
import { CompletionItem } from "../languageclient"
56

67
/** Adds LSP specific properties to the Atom SuggestionBase type */
7-
interface SuggestionBase extends ac.SuggestionBase {
8+
export interface SuggestionBase extends ac.SuggestionBase {
89
/**
910
* A string that is used when filtering and sorting a set of completion items with a prefix present. When `falsy` the
1011
* [displayText](#ac.SuggestionBase.displayText) is used. When no prefix, the `sortText` property is used.
@@ -16,6 +17,9 @@ interface SuggestionBase extends ac.SuggestionBase {
1617
* the suggestion was gathered from.
1718
*/
1819
customReplacmentPrefix?: string
20+
21+
/** Original completion item, if available */
22+
completionItem?: CompletionItem
1923
}
2024
export type TextSuggestion = SuggestionBase & ac.TextSuggestion
2125
export type SnippetSuggestion = SuggestionBase & ac.SnippetSuggestion

test/adapters/autocomplete-adapter.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import AutoCompleteAdapter, { grammarScopeToAutoCompleteSelector } from "../../lib/adapters/autocomplete-adapter"
22
import { ActiveServer } from "../../lib/server-manager.js"
33
import * as ls from "../../lib/languageclient"
4-
import { CompositeDisposable, Point } from "atom"
4+
import { CompositeDisposable, Point, TextEditor } from "atom"
55
import * as ac from "atom/autocomplete-plus"
6+
67
import { createSpyConnection, createFakeEditor } from "../helpers.js"
78
import { TextSuggestion, SnippetSuggestion } from "../../lib/types/autocomplete-extended"
89
import { CompletionItem, Range } from "../../lib/languageclient"
10+
import { dirname, join } from "path"
11+
import { Convert } from "../../lib/main"
912

1013
function createRequest({
1114
prefix = "",
@@ -596,6 +599,48 @@ describe("AutoCompleteAdapter", () => {
596599
})
597600
})
598601

602+
describe("applyAdditionalTextEdits", () => {
603+
it("1", async () => {
604+
const newText = "hello world"
605+
const range = Range.create({ line: 1, character: 0 }, { line: 1, character: 0 + newText.length })
606+
const additionalTextEdits = [
607+
{
608+
range,
609+
newText,
610+
},
611+
]
612+
const results = await getSuggestionsMock(
613+
[
614+
{
615+
label: "align",
616+
sortText: "a",
617+
additionalTextEdits,
618+
},
619+
],
620+
customRequest,
621+
undefined,
622+
undefined,
623+
true
624+
)
625+
expect(results[0].displayText).toBe("align")
626+
expect((results[0] as TextSuggestion).text).toBe("align")
627+
expect((results[0] as TextSuggestion).completionItem).toEqual({
628+
label: "align",
629+
sortText: "a",
630+
additionalTextEdits,
631+
})
632+
const editor = (await atom.workspace.open(join(dirname(__dirname), "fixtures", "hello.js"))) as TextEditor
633+
const suggestionInsertedEvent = {
634+
editor,
635+
triggerPosition: new Point(1, 0), // has no effect?
636+
suggestion: results[0],
637+
} as ac.SuggestionInsertedEvent
638+
AutoCompleteAdapter.applyAdditionalTextEdits(suggestionInsertedEvent)
639+
640+
expect(editor.getBuffer().getTextInRange(Convert.lsRangeToAtomRange(range))).toBe(newText)
641+
})
642+
})
643+
599644
describe("applies the change if shouldReplace is false", () => {
600645
it("1", async () => {
601646
const results = await getSuggestionsMock(

0 commit comments

Comments
 (0)