aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2021-11-05 19:54:27 +0100
committerLibravatar GitHub <noreply@github.com>2021-11-05 19:54:27 +0100
commit8350a0634d1caf34826fb3ac41d5a892cf3ff1c9 (patch)
tree6769f681ff339e0796e2ca43525df3e58b6fc6db /language-web/src/main/js
parentMerge pull request #9 from kris7t/cm6-sonar-fixes (diff)
parentchore(web): implicit completion info in grammar (diff)
downloadrefinery-8350a0634d1caf34826fb3ac41d5a892cf3ff1c9.tar.gz
refinery-8350a0634d1caf34826fb3ac41d5a892cf3ff1c9.tar.zst
refinery-8350a0634d1caf34826fb3ac41d5a892cf3ff1c9.zip
Merge pull request #10 from kris7t/cm6-fixes
More CodeMirrror fixes
Diffstat (limited to 'language-web/src/main/js')
-rw-r--r--language-web/src/main/js/editor/EditorParent.ts9
-rw-r--r--language-web/src/main/js/editor/EditorStore.ts2
-rw-r--r--language-web/src/main/js/language/problem.grammar14
-rw-r--r--language-web/src/main/js/language/props.ts7
-rw-r--r--language-web/src/main/js/xtext/ContentAssistService.ts98
5 files changed, 94 insertions, 36 deletions
diff --git a/language-web/src/main/js/editor/EditorParent.ts b/language-web/src/main/js/editor/EditorParent.ts
index ee1323f6..2d74b863 100644
--- a/language-web/src/main/js/editor/EditorParent.ts
+++ b/language-web/src/main/js/editor/EditorParent.ts
@@ -34,6 +34,9 @@ export const EditorParent = styled('div')(({ theme }) => {
34 '&, .cm-editor': { 34 '&, .cm-editor': {
35 height: '100%', 35 height: '100%',
36 }, 36 },
37 '.cm-content': {
38 padding: 0,
39 },
37 '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': { 40 '.cm-scroller, .cm-tooltip-autocomplete, .cm-completionLabel, .cm-completionDetail': {
38 fontSize: 16, 41 fontSize: 16,
39 fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace', 42 fontFamily: '"JetBrains MonoVariable", "JetBrains Mono", monospace',
@@ -46,7 +49,7 @@ export const EditorParent = styled('div')(({ theme }) => {
46 color: theme.palette.text.secondary, 49 color: theme.palette.text.secondary,
47 }, 50 },
48 '.cm-gutters': { 51 '.cm-gutters': {
49 background: theme.palette.background.default, 52 background: 'rgba(255, 255, 255, 0.1)',
50 color: theme.palette.text.disabled, 53 color: theme.palette.text.disabled,
51 border: 'none', 54 border: 'none',
52 }, 55 },
@@ -57,7 +60,9 @@ export const EditorParent = styled('div')(({ theme }) => {
57 background: 'rgba(0, 0, 0, 0.3)', 60 background: 'rgba(0, 0, 0, 0.3)',
58 }, 61 },
59 '.cm-activeLineGutter': { 62 '.cm-activeLineGutter': {
60 background: 'rgba(0, 0, 0, 0.3)', 63 background: 'transparent',
64 },
65 '.cm-lineNumbers .cm-activeLineGutter': {
61 color: theme.palette.text.primary, 66 color: theme.palette.text.primary,
62 }, 67 },
63 '.cm-cursor, .cm-cursor-primary': { 68 '.cm-cursor, .cm-cursor-primary': {
diff --git a/language-web/src/main/js/editor/EditorStore.ts b/language-web/src/main/js/editor/EditorStore.ts
index ba31efcb..059233f4 100644
--- a/language-web/src/main/js/editor/EditorStore.ts
+++ b/language-web/src/main/js/editor/EditorStore.ts
@@ -112,8 +112,8 @@ export class EditorStore {
112 }), 112 }),
113 semanticHighlighting, 113 semanticHighlighting,
114 // We add the gutters to `extensions` in the order we want them to appear. 114 // We add the gutters to `extensions` in the order we want them to appear.
115 foldGutter(),
116 lineNumbers(), 115 lineNumbers(),
116 foldGutter(),
117 keymap.of([ 117 keymap.of([
118 ...closeBracketsKeymap, 118 ...closeBracketsKeymap,
119 ...commentKeymap, 119 ...commentKeymap,
diff --git a/language-web/src/main/js/language/problem.grammar b/language-web/src/main/js/language/problem.grammar
index c242a4ba..8e39243f 100644
--- a/language-web/src/main/js/language/problem.grammar
+++ b/language-web/src/main/js/language/problem.grammar
@@ -1,3 +1,7 @@
1@detectDelim
2
3@external prop implicitCompletion from '../../../../src/main/js/language/props.ts'
4
1@top Problem { statement* } 5@top Problem { statement* }
2 6
3statement { 7statement {
@@ -24,7 +28,7 @@ statement {
24 RuleBody { ":" sep<OrOp, Conjunction> "~>" sep<OrOp, Action> "." } 28 RuleBody { ":" sep<OrOp, Conjunction> "~>" sep<OrOp, Action> "." }
25 } | 29 } |
26 Assertion { 30 Assertion {
27 ckw<"default">? (NotOp | UnknownOp)? RelationName 31 kw<"default">? (NotOp | UnknownOp)? RelationName
28 ParameterList<AssertionArgument> (":" LogicValue)? "." 32 ParameterList<AssertionArgument> (":" LogicValue)? "."
29 } | 33 } |
30 NodeValueAssertion { 34 NodeValueAssertion {
@@ -34,7 +38,7 @@ statement {
34 ckw<"unique"> sep<",", UniqueNodeName> "." 38 ckw<"unique"> sep<",", UniqueNodeName> "."
35 } | 39 } |
36 ScopeDeclaration { 40 ScopeDeclaration {
37 ckw<"scope"> sep<",", ScopeElement> "." 41 kw<"scope"> sep<",", ScopeElement> "."
38 } 42 }
39} 43}
40 44
@@ -89,11 +93,11 @@ VariableName { QualifiedName }
89 93
90NodeName { QualifiedName } 94NodeName { QualifiedName }
91 95
92QualifiedName { identifier ("::" identifier)* } 96QualifiedName[implicitCompletion=true] { identifier ("::" identifier)* }
93 97
94kw<term> { @specialize[@name={term}]<identifier, term> } 98kw<term> { @specialize[@name={term},implicitCompletion=true]<identifier, term> }
95 99
96ckw<term> { @extend[@name={term}]<identifier, term> } 100ckw<term> { @extend[@name={term},implicitCompletion=true]<identifier, term> }
97 101
98ParameterList<content> { "(" sep<",", content> ")" } 102ParameterList<content> { "(" sep<",", content> ")" }
99 103
diff --git a/language-web/src/main/js/language/props.ts b/language-web/src/main/js/language/props.ts
new file mode 100644
index 00000000..8e488bf5
--- /dev/null
+++ b/language-web/src/main/js/language/props.ts
@@ -0,0 +1,7 @@
1import { NodeProp } from '@lezer/common';
2
3export const implicitCompletion = new NodeProp({
4 deserialize(s: string) {
5 return s === 'true';
6 },
7});
diff --git a/language-web/src/main/js/xtext/ContentAssistService.ts b/language-web/src/main/js/xtext/ContentAssistService.ts
index f085c5b1..aa9a80b0 100644
--- a/language-web/src/main/js/xtext/ContentAssistService.ts
+++ b/language-web/src/main/js/xtext/ContentAssistService.ts
@@ -3,9 +3,11 @@ import type {
3 CompletionContext, 3 CompletionContext,
4 CompletionResult, 4 CompletionResult,
5} from '@codemirror/autocomplete'; 5} from '@codemirror/autocomplete';
6import { syntaxTree } from '@codemirror/language';
6import type { Transaction } from '@codemirror/state'; 7import type { Transaction } from '@codemirror/state';
7import escapeStringRegexp from 'escape-string-regexp'; 8import escapeStringRegexp from 'escape-string-regexp';
8 9
10import { implicitCompletion } from '../language/props';
9import type { UpdateService } from './UpdateService'; 11import type { UpdateService } from './UpdateService';
10import { getLogger } from '../utils/logger'; 12import { getLogger } from '../utils/logger';
11import type { IContentAssistEntry } from './xtextServiceResults'; 13import type { IContentAssistEntry } from './xtextServiceResults';
@@ -14,12 +16,57 @@ const PROPOSALS_LIMIT = 1000;
14 16
15const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*'; 17const IDENTIFIER_REGEXP_STR = '[a-zA-Z0-9_]*';
16 18
17const HIGH_PRIORITY_KEYWORDS = ['<->']; 19const HIGH_PRIORITY_KEYWORDS = ['<->', '~>'];
18
19const QUALIFIED_NAME_SEPARATOR_REGEXP = /::/g;
20 20
21const log = getLogger('xtext.ContentAssistService'); 21const log = getLogger('xtext.ContentAssistService');
22 22
23interface IFoundToken {
24 from: number;
25
26 to: number;
27
28 implicitCompletion: boolean;
29
30 text: string;
31}
32
33function findToken({ pos, state }: CompletionContext): IFoundToken | null {
34 const token = syntaxTree(state).resolveInner(pos, -1);
35 if (token === null) {
36 return null;
37 }
38 if (token.firstChild !== null) {
39 // We only autocomplete terminal nodes. If the current node is nonterminal,
40 // returning `null` makes us autocomplete with the empty prefix instead.
41 return null;
42 }
43 return {
44 from: token.from,
45 to: token.to,
46 implicitCompletion: token.type.prop(implicitCompletion) || false,
47 text: state.sliceDoc(token.from, token.to),
48 };
49}
50
51function shouldCompleteImplicitly(token: IFoundToken | null, context: CompletionContext): boolean {
52 return token !== null
53 && token.implicitCompletion
54 && context.pos - token.from >= 2;
55}
56
57function computeSpan(prefix: string, entryCount: number): RegExp {
58 const escapedPrefix = escapeStringRegexp(prefix);
59 if (entryCount < PROPOSALS_LIMIT) {
60 // Proposals with the current prefix fit the proposals limit.
61 // We can filter client side as long as the current prefix is preserved.
62 return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`);
63 }
64 // The current prefix overflows the proposals limits,
65 // so we have to fetch the completions again on the next keypress.
66 // Hopefully, it'll return a shorter list and we'll be able to filter client side.
67 return new RegExp(`^${escapedPrefix}$`);
68}
69
23function createCompletion(entry: IContentAssistEntry): Completion { 70function createCompletion(entry: IContentAssistEntry): Completion {
24 let boost; 71 let boost;
25 switch (entry.kind) { 72 switch (entry.kind) {
@@ -33,7 +80,7 @@ function createCompletion(entry: IContentAssistEntry): Completion {
33 break; 80 break;
34 default: { 81 default: {
35 // Penalize qualified names (vs available unqualified names). 82 // Penalize qualified names (vs available unqualified names).
36 const extraSegments = entry.proposal.match(QUALIFIED_NAME_SEPARATOR_REGEXP)?.length || 0; 83 const extraSegments = entry.proposal.match(/::/g)?.length || 0;
37 boost = Math.max(-5 * extraSegments, -50); 84 boost = Math.max(-5 * extraSegments, -50);
38 } 85 }
39 break; 86 break;
@@ -47,19 +94,6 @@ function createCompletion(entry: IContentAssistEntry): Completion {
47 }; 94 };
48} 95}
49 96
50function computeSpan(prefix: string, entryCount: number) {
51 const escapedPrefix = escapeStringRegexp(prefix);
52 if (entryCount < PROPOSALS_LIMIT) {
53 // Proposals with the current prefix fit the proposals limit.
54 // We can filter client side as long as the current prefix is preserved.
55 return new RegExp(`^${escapedPrefix}${IDENTIFIER_REGEXP_STR}$`);
56 }
57 // The current prefix overflows the proposals limits,
58 // so we have to fetch the completions again on the next keypress.
59 // Hopefully, it'll return a shorter list and we'll be able to filter client side.
60 return new RegExp(`^${escapedPrefix}$`);
61}
62
63export class ContentAssistService { 97export class ContentAssistService {
64 private readonly updateService: UpdateService; 98 private readonly updateService: UpdateService;
65 99
@@ -76,16 +110,16 @@ export class ContentAssistService {
76 } 110 }
77 111
78 async contentAssist(context: CompletionContext): Promise<CompletionResult> { 112 async contentAssist(context: CompletionContext): Promise<CompletionResult> {
79 const tokenBefore = context.tokenBefore(['QualifiedName']); 113 const tokenBefore = findToken(context);
114 if (!context.explicit && !shouldCompleteImplicitly(tokenBefore, context)) {
115 return {
116 from: context.pos,
117 options: [],
118 };
119 }
80 let range: { from: number, to: number }; 120 let range: { from: number, to: number };
81 let prefix = ''; 121 let prefix = '';
82 if (tokenBefore === null) { 122 if (tokenBefore === null) {
83 if (!context.explicit) {
84 return {
85 from: context.pos,
86 options: [],
87 };
88 }
89 range = { 123 range = {
90 from: context.pos, 124 from: context.pos,
91 to: context.pos, 125 to: context.pos,
@@ -124,7 +158,12 @@ export class ContentAssistService {
124 } 158 }
125 const options: Completion[] = []; 159 const options: Completion[] = [];
126 entries.forEach((entry) => { 160 entries.forEach((entry) => {
127 options.push(createCompletion(entry)); 161 if (prefix === entry.prefix) {
162 // Xtext will generate completions that do not complete the current token,
163 // e.g., `(` after trying to complete an indetifier,
164 // but we ignore those, since CodeMirror won't filter for them anyways.
165 options.push(createCompletion(entry));
166 }
128 }); 167 });
129 log.debug('Fetched', options.length, 'completions from server'); 168 log.debug('Fetched', options.length, 'completions from server');
130 this.lastCompletion = { 169 this.lastCompletion = {
@@ -137,7 +176,7 @@ export class ContentAssistService {
137 176
138 private shouldReturnCachedCompletion( 177 private shouldReturnCachedCompletion(
139 token: { from: number, to: number, text: string } | null, 178 token: { from: number, to: number, text: string } | null,
140 ) { 179 ): boolean {
141 if (token === null || this.lastCompletion === null) { 180 if (token === null || this.lastCompletion === null) {
142 return false; 181 return false;
143 } 182 }
@@ -147,10 +186,13 @@ export class ContentAssistService {
147 return true; 186 return true;
148 } 187 }
149 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo); 188 const [transformedFrom, transformedTo] = this.mapRangeInclusive(lastFrom, lastTo);
150 return from >= transformedFrom && to <= transformedTo && span && span.exec(text); 189 return from >= transformedFrom
190 && to <= transformedTo
191 && typeof span !== 'undefined'
192 && span.exec(text) !== null;
151 } 193 }
152 194
153 private shouldInvalidateCachedCompletion(transaction: Transaction) { 195 private shouldInvalidateCachedCompletion(transaction: Transaction): boolean {
154 if (!transaction.docChanged || this.lastCompletion === null) { 196 if (!transaction.docChanged || this.lastCompletion === null) {
155 return false; 197 return false;
156 } 198 }