/
detect-multiline.ts
176 lines (151 loc) · 6.21 KB
/
detect-multiline.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import { Position } from 'vscode'
import { addAutocompleteDebugEvent } from '../services/open-telemetry/debug-utils'
import { getLanguageConfig } from '../tree-sitter/language'
import type { DocumentDependentContext, LinesContext } from './get-current-doc-context'
import {
FUNCTION_KEYWORDS,
FUNCTION_OR_METHOD_INVOCATION_REGEX,
OPENING_BRACKET_REGEX,
getLastLine,
indentation,
lines,
} from './text-processing'
interface DetectMultilineParams {
docContext: LinesContext & DocumentDependentContext
languageId: string
position: Position
}
interface DetectMultilineResult {
multilineTrigger: string | null
multilineTriggerPosition: Position | null
}
export function endsWithBlockStart(text: string, languageId: string): string | null {
const blockStart = getLanguageConfig(languageId)?.blockStart
return blockStart && text.trimEnd().endsWith(blockStart) ? blockStart : null
}
// Languages with more than 100 multiline completions in the last month and CAR > 20%:
// https://sourcegraph.looker.com/explore/sourcegraph/cody?qid=JBItVt6VFMlCtMa9KOBmjh&origin_space=562
const LANGUAGES_WITH_MULTILINE_SUPPORT = [
'astro',
'c',
'cpp',
'csharp',
'css',
'dart',
'elixir',
'go',
'html',
'java',
'javascript',
'javascriptreact',
'kotlin',
'php',
'python',
'rust',
'svelte',
'typescript',
'typescriptreact',
'vue',
]
export function detectMultiline(params: DetectMultilineParams): DetectMultilineResult {
const { docContext, languageId, position } = params
const { prefix, prevNonEmptyLine, nextNonEmptyLine, currentLinePrefix, currentLineSuffix } =
docContext
const isMultilineSupported = LANGUAGES_WITH_MULTILINE_SUPPORT.includes(languageId)
const blockStart = endsWithBlockStart(prefix, languageId)
const isBlockStartActive = Boolean(blockStart)
const currentLineText =
currentLineSuffix.trim().length > 0 ? currentLinePrefix + currentLineSuffix : currentLinePrefix
const isMethodOrFunctionInvocation =
!currentLinePrefix.trim().match(FUNCTION_KEYWORDS) &&
currentLineText.match(FUNCTION_OR_METHOD_INVOCATION_REGEX)
// Don't fire multiline completion for method or function invocations
// see https://github.com/sourcegraph/cody/discussions/358#discussioncomment-6519606
// Don't fire multiline completion for unsupported languages.
if (isMethodOrFunctionInvocation || !isMultilineSupported) {
addAutocompleteDebugEvent('detectMultiline', {
languageId,
isMethodOrFunctionInvocation,
})
return {
multilineTrigger: null,
multilineTriggerPosition: null,
}
}
const openingBracketMatch = getLastLine(prefix.trimEnd()).match(OPENING_BRACKET_REGEX)
const isSameLineOpeningBracketMatch =
currentLinePrefix.trim() !== '' &&
openingBracketMatch &&
// Only trigger multiline suggestions when the next non-empty line is indented less
// than the block start line (the newly created block is empty).
indentation(currentLinePrefix) >= indentation(nextNonEmptyLine)
const isNewLineOpeningBracketMatch =
currentLinePrefix.trim() === '' &&
currentLineSuffix.trim() === '' &&
openingBracketMatch &&
// Only trigger multiline suggestions when the next non-empty line is indented the same or less
indentation(prevNonEmptyLine) < indentation(currentLinePrefix) &&
// Only trigger multiline suggestions when the next non-empty line is indented less
// than the block start line (the newly created block is empty).
indentation(prevNonEmptyLine) >= indentation(nextNonEmptyLine)
if (isNewLineOpeningBracketMatch || isSameLineOpeningBracketMatch) {
addAutocompleteDebugEvent('detectMultiline', {
isNewLineOpeningBracketMatch,
isSameLineOpeningBracketMatch,
})
return {
multilineTrigger: openingBracketMatch[0],
multilineTriggerPosition: getPrefixLastNonEmptyCharPosition(prefix, position),
}
}
const nonEmptyLineEndsWithBlockStart =
currentLinePrefix.trim() !== '' &&
isBlockStartActive &&
indentation(currentLinePrefix) >= indentation(nextNonEmptyLine)
const isEmptyLineAfterBlockStart =
currentLinePrefix.trim() === '' &&
currentLineSuffix.trim() === '' &&
// Only trigger multiline suggestions for the beginning of blocks
isBlockStartActive &&
// Only trigger multiline suggestions when the next non-empty line is indented the same or less
indentation(prevNonEmptyLine) < indentation(currentLinePrefix) &&
// Only trigger multiline suggestions when the next non-empty line is indented less
// than the block start line (the newly created block is empty).
indentation(prevNonEmptyLine) >= indentation(nextNonEmptyLine)
if (nonEmptyLineEndsWithBlockStart || isEmptyLineAfterBlockStart) {
addAutocompleteDebugEvent('detectMultiline', {
nonEmptyLineEndsWithBlockStart,
isEmptyLineAfterBlockStart,
})
return {
multilineTrigger: blockStart,
multilineTriggerPosition: getPrefixLastNonEmptyCharPosition(prefix, position),
}
}
addAutocompleteDebugEvent('detectMultiline', {
nonEmptyLineEndsWithBlockStart,
isEmptyLineAfterBlockStart,
isNewLineOpeningBracketMatch,
isSameLineOpeningBracketMatch,
})
return {
multilineTrigger: null,
multilineTriggerPosition: null,
}
}
/**
* Precalculate the multiline trigger position based on `prefix` and `cursorPosition` to be
* able to change it during streaming to the end of the first line of the completion.
*/
function getPrefixLastNonEmptyCharPosition(prefix: string, cursorPosition: Position): Position {
const trimmedPrefix = prefix.trimEnd()
const diffLength = prefix.length - trimmedPrefix.length
if (diffLength === 0) {
return cursorPosition.translate(0, -1)
}
const prefixDiff = prefix.slice(-diffLength)
return new Position(
cursorPosition.line - (lines(prefixDiff).length - 1),
getLastLine(trimmedPrefix).length - 1
)
}