Skip to content

Commit 76a020f

Browse files
[autocomplete-plus] Add support for “additional” text edits… (#1301)
* [autocomplete-plus] Add support for “additional” text edits… …when a suggestion is accepted. This can include things like automatically adding an `import` statement for the accepted suggestion. * Bump `autocomplete.provider` service to v5.1.0 * Insert `textEdit` text as a snippet if `snippet` is truthy * Apply automated lints (for PR 1301) Committer's (DeeDeeG's) note: I manually split these whitespace-only changes out from the other commits of this pull request, in order to make their diffs more succinct and readable. (These changes were originally done in the first two commits of the Pull Request.)
1 parent 1f257c9 commit 76a020f

6 files changed

Lines changed: 735 additions & 59 deletions

File tree

packages/autocomplete-plus/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Displays possible autocomplete suggestions on keystroke (or manually by typing `
1414

1515
`autocomplete+` has a powerful autocomplete provider API, allowing provider authors to add language-specific behavior to this package.
1616

17-
You should *definitely* install additional providers (the default provider bundled with this package is somewhat crude): https://github.com/atom/autocomplete-plus/wiki/Autocomplete-Providers
17+
You should *definitely* install additional providers (the default provider bundled with this package is somewhat crude): here’s [a list of all Pulsar packages (built-in or community) that provide `autocomplete.provider`](https://web.pulsar-edit.dev/packages?serviceType=provided&service=autocomplete.provider).
1818

1919
## Usage
2020

@@ -77,7 +77,7 @@ Then add these to your keymap file:
7777

7878
Great autocomplete depends on having great autocomplete providers. If there is not already a great provider for the language / grammar that you are working in, please consider creating a provider.
7979

80-
[Read the `Provider API` documentation](https://github.com/atom/autocomplete-plus/wiki/Provider-API) to learn how to create a new autocomplete provider.
80+
[Read the `Provider API` documentation](./docs/provider-api.md) to learn how to create a new autocomplete provider.
8181

8282
## `SymbolProvider` Configuration
8383

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
# `autocomplete.provider` service documentation
2+
3+
Read this document if you’d like to write your own autocompletion provider.
4+
5+
If you want to turn a language server into an autocompletion provider, you can use `atom-languageclient` instead of providing this service directly. [Consult its README](https://github.com/savetheclocktower/atom-languageclient?tab=readme-ov-file#developing-packages) for more information.
6+
7+
## Service definition
8+
9+
The following TypeScript-style code block describes the `autocomplete.provider` service in its current form.
10+
11+
The current service version is **5.1.0**. The documentation is accurate for version 4 or greater of the API; aspects that are present only in versions later than 4 are annotated as such.
12+
13+
```ts
14+
import { Point, Range, ScopeDescriptor, TextEditor } from 'atom';
15+
16+
17+
/**
18+
* A {@link Range} or any object that can be accepted by {@link
19+
* Range.prototype.fromObject}.
20+
*/
21+
type RangeCompatible =
22+
| Range
23+
| [Point, Point],
24+
| [[number, number], [number, number]]
25+
| [{ row: number, column: number}, { row: number, column: number}]
26+
27+
/**
28+
* Describes a range of a buffer and the text to insert into it. This is one
29+
* possible insertion strategy among several for a suggestion.
30+
*
31+
* Like a `Range` from the Language Server Protocol specification, but with an
32+
* Atom-style {@link Range}.
33+
*
34+
* This type bears some similarity to an existing `TextEdit` type used by
35+
* Atom-IDE. The existing type uses the `oldRange` property instead of `range`,
36+
* but is otherwise identical. Both are acceptable.
37+
*/
38+
type TextEdit = {
39+
newText: string
40+
} & ({ range: RangeCompatible } | { oldRange: RangeCompatible })
41+
42+
/**
43+
* Known types of suggestions; these have predefined styles when rendered.
44+
*/
45+
type SuggestionType =
46+
| 'variable'
47+
| 'constant'
48+
| 'property'
49+
| 'value'
50+
| 'method'
51+
| 'function'
52+
| 'class'
53+
| 'type'
54+
| 'keyword'
55+
| 'tag'
56+
| 'snippet'
57+
| 'import'
58+
| 'require'
59+
60+
/**
61+
* A single suggestion as returned by `getSuggestions` or
62+
* `getSuggestionDetailsOnSelect`.
63+
*/
64+
type Suggestion = {
65+
// A suggestion can be inserted one of several ways: as plain text, as a
66+
// snippet, or as a `TextEdit`. One of these three properties is therefore
67+
// required; the others are optional.
68+
//
69+
// Of these three, later properties win out over earlier ones. For example,
70+
// `textEdit` will be preferred over both `snippet` and `text` if it is
71+
// present.
72+
73+
/**
74+
* The text to be inserted. Will be used if `snippet` is absent.
75+
*/
76+
text?: string
77+
78+
/**
79+
* A snippet to insert upon suggestion selection.
80+
*/
81+
snippet?: string
82+
83+
/**
84+
* Text edit to make when the item is chosen. Use this when you want to be
85+
* specific about which range of the buffer is replaced. When present, this
86+
* will be used as the insertion strategy instead of the default behavior.
87+
*
88+
* If a suggestion needs to make several edits upon insertion, the rest can
89+
* be specified via `additionalTextEdits`.
90+
*
91+
* Added in 5.1.0.
92+
*/
93+
textEdit?: TextEdit
94+
95+
/**
96+
* The text to show in the menu for this suggestion. Optional; falls back to
97+
* `snippet`, then `text`.
98+
*/
99+
displayText?: string
100+
101+
/**
102+
* A list of `Range`s to replace when inserting the text. Each `Range`present
103+
* in this list will result in one insertion of the suggestion's textor
104+
* snippet.
105+
*
106+
* Can insert the autocompletion's text/snippet into one specific range or
107+
* multiple. Use this when you know the exact range of the current buffer
108+
* that should be replaced with the given text.
109+
*
110+
* Added in 5.0.0. Has no effect if `textEdit` is specified.
111+
*/
112+
ranges?: Range[]
113+
114+
/**
115+
* Text before the cursor that should be replaced as part of the insertion of
116+
* this suggestion. Optional; if omitted, the prefix before the cursor will
117+
* be used. Has no effect if `textEdit` or `ranges` is specified.
118+
*/
119+
replacementPrefix?: string
120+
121+
/**
122+
* A "type" for this suggestion. Used to classify suggestions and distinguish
123+
* them visually. The types of {@link SuggestionType} are preferred (and have
124+
* predefined styles), but you can use an arbitrary string if none of those
125+
* types suffice.
126+
*/
127+
type?: SuggestionType | string
128+
129+
/**
130+
* Text edits to make when the item is chosen — in addition to the main item.
131+
*
132+
* These are typically optional edits, such as an automatic `import`
133+
* statement that's inserted when a suggestion warrants it.
134+
*
135+
* When present, these edits are made in all code paths, regardless of the
136+
* original insertion strategy.
137+
*
138+
* Added in 5.1.0.
139+
*/
140+
additionalTextEdits?: TextEdit
141+
142+
/**
143+
* A label to display before the suggestion. This can indicate useful
144+
* information like a method return type. Both text and HTML variants are
145+
* supported; `leftLabelHTML` takes precedence over `leftLabel` when both are
146+
* present.
147+
*/
148+
leftLabel?: string
149+
leftLabelHTML?: string
150+
151+
/**
152+
* A label to display after the suggestion. This can indicate useful
153+
* information like a type annotation. Both text and HTML variants are
154+
* supported; `rightLabelHTML` takes precedence over `rightLabel` when both
155+
* are present.
156+
*/
157+
rightLabel?: string
158+
rightLabelHTML?: string
159+
160+
/**
161+
* Class name to add to the suggestion's row in the HTML. Allows for further
162+
* styling customization, if needed.
163+
*/
164+
className?: string
165+
166+
/**
167+
* An override to allow you to specify your own icon. Should follow Octicon
168+
* conventions; e.g., `"<i class="icon-move-right"></i>"`. Optional.
169+
*/
170+
iconHTML?: string
171+
172+
/**
173+
* A docstring summary or short description of the suggestion. When
174+
* specified, it will be displayed at the bottom of the suggestions list.
175+
* Optional.
176+
*/
177+
description?: string
178+
179+
/**
180+
* A url to the documentation or more information about this suggestion. When
181+
* specified, a `More…` link will be displayed in the description area.
182+
*/
183+
descriptionMoreURL?: string
184+
185+
/**
186+
* A list of indices where the characters in the prefix appear in this
187+
* suggestion's text.
188+
* @type {Object}
189+
*/
190+
characterMatchIndices?: number[]
191+
192+
// (Either `text`, `snippet`, or `textEdit` must be provided.)
193+
} & { text: string } | { snippet: string } | { textEdit: TextEdit};
194+
195+
/**
196+
* The provider object that you should make available to `autocomplete-plus`.
197+
* This should be the return value of whatever method you specified in your
198+
* `providedServices` metadata.
199+
*/
200+
type ServiceProvider = {
201+
/**
202+
* Selector for which this provider should be active. Multiple values can be
203+
* given separated by commas.
204+
*/
205+
selector: string,
206+
/**
207+
* Selector for which this provider should be inactive, even if scope
208+
* otherwise matches `selector`. Multiple values can be given separated by
209+
* commas.
210+
*/
211+
disableForSelector: string,
212+
213+
/**
214+
* The priority of this provider relative to others. Higher numbers beat
215+
* lower numbers.
216+
*/
217+
inclusionPriority: number,
218+
219+
/**
220+
* When `true`, this provider excludes options from providers with a lower
221+
* priority from even appearing in the menu.
222+
*/
223+
excludeLowerPriority: boolean,
224+
225+
/**
226+
* The priority of this provider's suggestions relative to other suggestions
227+
* that may exist in the list. Influences the ordering of suggestions within
228+
* a menu.
229+
*/
230+
suggestionPriority: number,
231+
232+
/**
233+
* When `true`, `autocomplete-plus` expects to receive many suggestions and
234+
* will filter the list based on what's already been typed in the token. When
235+
* `false`, you assert that whatever you deliver to `autocomplete-plus` has
236+
* already been filtered.
237+
*/
238+
filterSuggestions: boolean,
239+
240+
/**
241+
* Retrieves suggestions for a given editor at a given point. Can consult
242+
* other metadata. Can go async.
243+
*/
244+
getSuggestions(meta: {
245+
/** The current text editor. */
246+
editor: TextEditor,
247+
/** The position of the cursor. */
248+
bufferPosition: Point,
249+
/**
250+
* The scope descriptor at the given buffer position.
251+
* @see https://docs.pulsar-edit.dev/api/pulsar/latest/ScopeDescriptor/
252+
*/
253+
scopeDescriptor: ScopeDescriptor,
254+
/**
255+
* The prefix that the user has typed before the cursor. Typically
256+
* represents all word-like characters between the cursor and the last
257+
* non-word character.
258+
*/
259+
prefix: string,
260+
/**
261+
* Whether the user activated this menu manually or had it appear
262+
* automatically while typing.
263+
*/
264+
activatedManually: boolean
265+
}): Suggestion[] | Promise<Suggestion[]>,
266+
267+
/**
268+
* Fills in further details on this suggestion when it is highlighted in the
269+
* menu. Optional.
270+
*
271+
* A language server can use this method to send `completionItem/resolve` and
272+
* return an updated suggestion with the new data.
273+
*/
274+
getSuggestionDetailsOnSelect?(suggestion: Suggestion): Promise<Suggestion>,
275+
276+
/**
277+
* Invoked after a chosen suggestion is inserted into the editor. Optional.
278+
*/
279+
onDidInsertSuggestion?(meta: {
280+
/** The current text editor. */
281+
editor: TextEditor,
282+
/** The position of the cursor when the suggestion was chosen. */
283+
triggerPosition: Point,
284+
/** The suggestion that was chosen. */
285+
suggestion: Suggestion
286+
}): void;
287+
288+
/**
289+
* Called when your provider needs to be cleaned up. Optional.
290+
*/
291+
dispose?(): void;
292+
}
293+
294+
```
295+
296+
297+
## Registering your provider with `autocomplete-plus`
298+
299+
In your `package.json`, add:
300+
301+
```json
302+
"providedServices": {
303+
"autocomplete.provider": {
304+
"versions": {
305+
"4.0.0": "provideAutocomplete"
306+
}
307+
}
308+
}
309+
```
310+
311+
You may call this value whatever you like; `provideAutocomplete` is a suggestion.
312+
313+
Then, in your main package export, define a method of the same name:
314+
315+
```js
316+
module.exports = {
317+
activate() {
318+
// existing activation code
319+
}
320+
321+
provideAutocomplete() {
322+
// Return a value that conforms to the `ServiceProvider` interface
323+
// described above…
324+
return new Provider()
325+
326+
// …or return multiple such providers as an array.
327+
return [new Provider(), new OtherProvider()]
328+
}
329+
}
330+
```
331+
332+
333+
## Tips
334+
335+
`autocomplete-plus` guesses at the “prefix” — that is, the range of characters before the cursor that might be part of whatever suggestion you will insert.
336+
337+
For some languages, you may need to override this by specifying a `replacementPrefix` value for each suggestion:
338+
339+
```js
340+
let provider = {
341+
selector: 'source.js',
342+
getSuggestions({ editor, bufferPosition }) {
343+
let prefix = this.getPrefix(editor, bufferPosition)
344+
},
345+
346+
getPrefix(editor, bufferPosition) {
347+
// Whatever your prefix regex might be.
348+
let regex = /[\w0-9_-]+$/
349+
350+
// Get the text for the line up to the triggered buffer position.
351+
let line = line = editor.getTextInRange([
352+
[bufferPosition.row, 0],
353+
bufferPosition
354+
])
355+
356+
// Match the regex to the line, and return the match (if any).
357+
return line.match(regex)?.[0] ?? ''
358+
}
359+
360+
}
361+
```

0 commit comments

Comments
 (0)