@likec4/language-server 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/contrib/likec4.monarch.ts +4 -9
- package/contrib/likec4.tmLanguage.json +1 -1
- package/dist/ast.d.ts +14 -20
- package/dist/ast.js +30 -25
- package/dist/generated/ast.d.ts +24 -3
- package/dist/generated/ast.js +40 -8
- package/dist/generated/grammar.js +1 -1
- package/dist/lsp/SemanticTokenProvider.js +8 -1
- package/dist/model/model-builder.js +62 -28
- package/dist/model/model-locator.js +1 -1
- package/dist/model/model-parser.js +29 -17
- package/dist/model-change/ModelChanges.js +24 -26
- package/dist/model-change/{changeViewStyle.d.ts → changeElementStyle.d.ts} +5 -5
- package/dist/model-change/changeElementStyle.js +141 -0
- package/dist/protocol.d.ts +12 -12
- package/dist/references/scope-computation.js +5 -3
- package/dist/validation/index.js +2 -0
- package/dist/validation/property-checks.d.ts +5 -0
- package/dist/validation/property-checks.js +11 -0
- package/package.json +4 -4
- package/dist/model-change/changeViewStyle.js +0 -123
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
cleanParsedModel,
|
|
9
9
|
ElementViewOps,
|
|
10
10
|
isFqnIndexedDocument,
|
|
11
|
+
parseAstOpacityProperty,
|
|
11
12
|
resolveRelationPoints,
|
|
12
13
|
streamModel,
|
|
13
14
|
toAutoLayout,
|
|
14
15
|
toElementStyle,
|
|
15
|
-
toElementStyleExcludeDefaults,
|
|
16
16
|
toRelationshipStyleExcludeDefaults
|
|
17
17
|
} from "../ast.js";
|
|
18
18
|
import { elementRef, getFqnElementRef } from "../elementRef.js";
|
|
@@ -56,22 +56,22 @@ export class LikeC4ModelParser {
|
|
|
56
56
|
return doc;
|
|
57
57
|
}
|
|
58
58
|
parseSpecification(doc) {
|
|
59
|
-
const { isValid } = checksFromDiagnostics(doc);
|
|
59
|
+
const { isValid: isValid2 } = checksFromDiagnostics(doc);
|
|
60
60
|
const { parseResult, c4Specification } = doc;
|
|
61
|
-
const specifications = parseResult.value.specifications.filter(
|
|
62
|
-
const element_specs = specifications.flatMap((s) => s.elements.filter(
|
|
61
|
+
const specifications = parseResult.value.specifications.filter(isValid2);
|
|
62
|
+
const element_specs = specifications.flatMap((s) => s.elements.filter(isValid2));
|
|
63
63
|
for (const { kind, style } of element_specs) {
|
|
64
64
|
try {
|
|
65
65
|
const kindName = kind.name;
|
|
66
66
|
c4Specification.kinds[kindName] = {
|
|
67
67
|
...c4Specification.kinds[kindName],
|
|
68
|
-
...
|
|
68
|
+
...toElementStyle(style?.props)
|
|
69
69
|
};
|
|
70
70
|
} catch (e) {
|
|
71
71
|
logWarnError(e);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
const relations_specs = specifications.flatMap((s) => s.relationships.filter(
|
|
74
|
+
const relations_specs = specifications.flatMap((s) => s.relationships.filter(isValid2));
|
|
75
75
|
for (const { kind, props } of relations_specs) {
|
|
76
76
|
try {
|
|
77
77
|
const kindName = kind.name;
|
|
@@ -110,7 +110,7 @@ export class LikeC4ModelParser {
|
|
|
110
110
|
const kind = astNode.kind.$refText;
|
|
111
111
|
const tags = this.convertTags(astNode.body);
|
|
112
112
|
const stylePropsAst = astNode.body?.props.find(ast.isStyleProperties)?.props;
|
|
113
|
-
const
|
|
113
|
+
const style = toElementStyle(stylePropsAst);
|
|
114
114
|
const astPath = this.getAstNodePath(astNode);
|
|
115
115
|
let [title, description, technology] = astNode.props ?? [];
|
|
116
116
|
const bodyProps = astNode.body?.props.filter(ast.isElementStringProperty) ?? [];
|
|
@@ -127,7 +127,7 @@ export class LikeC4ModelParser {
|
|
|
127
127
|
...links && isNonEmptyArray(links) && { links },
|
|
128
128
|
...isTruthy(technology) && { technology },
|
|
129
129
|
...isTruthy(description) && { description },
|
|
130
|
-
|
|
130
|
+
style
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
parseRelation(astNode) {
|
|
@@ -160,11 +160,11 @@ export class LikeC4ModelParser {
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
parseViews(doc) {
|
|
163
|
-
const { isValid } = checksFromDiagnostics(doc);
|
|
164
|
-
const views = doc.parseResult.value.views.flatMap((v) =>
|
|
163
|
+
const { isValid: isValid2 } = checksFromDiagnostics(doc);
|
|
164
|
+
const views = doc.parseResult.value.views.flatMap((v) => isValid2(v) ? v.views.filter(isValid2) : []);
|
|
165
165
|
for (const view of views) {
|
|
166
166
|
try {
|
|
167
|
-
const v = this.parseElementView(view);
|
|
167
|
+
const v = this.parseElementView(view, isValid2);
|
|
168
168
|
doc.c4Views.push(v);
|
|
169
169
|
} catch (e) {
|
|
170
170
|
logWarnError(e);
|
|
@@ -247,6 +247,10 @@ export class LikeC4ModelParser {
|
|
|
247
247
|
acc.custom[prop.key] = value.trim();
|
|
248
248
|
return acc;
|
|
249
249
|
}
|
|
250
|
+
if (ast.isIconProperty(prop)) {
|
|
251
|
+
acc.custom[prop.key] = prop.value;
|
|
252
|
+
return acc;
|
|
253
|
+
}
|
|
250
254
|
if (ast.isColorProperty(prop)) {
|
|
251
255
|
acc.custom[prop.key] = prop.value;
|
|
252
256
|
return acc;
|
|
@@ -255,6 +259,14 @@ export class LikeC4ModelParser {
|
|
|
255
259
|
acc.custom[prop.key] = prop.value;
|
|
256
260
|
return acc;
|
|
257
261
|
}
|
|
262
|
+
if (ast.isBorderProperty(prop)) {
|
|
263
|
+
acc.custom[prop.key] = prop.value;
|
|
264
|
+
return acc;
|
|
265
|
+
}
|
|
266
|
+
if (ast.isOpacityProperty(prop)) {
|
|
267
|
+
acc.custom[prop.key] = parseAstOpacityProperty(prop);
|
|
268
|
+
return acc;
|
|
269
|
+
}
|
|
258
270
|
nonexhaustive(prop);
|
|
259
271
|
},
|
|
260
272
|
{
|
|
@@ -295,11 +307,11 @@ export class LikeC4ModelParser {
|
|
|
295
307
|
}
|
|
296
308
|
nonexhaustive(astNode);
|
|
297
309
|
}
|
|
298
|
-
parseViewRule(astRule) {
|
|
310
|
+
parseViewRule(astRule, isValid2) {
|
|
299
311
|
if (ast.isIncludePredicate(astRule) || ast.isExcludePredicate(astRule)) {
|
|
300
312
|
const exprs = astRule.expressions.flatMap((n) => {
|
|
301
313
|
try {
|
|
302
|
-
return this.parsePredicateExpr(n);
|
|
314
|
+
return isValid2(n) ? this.parsePredicateExpr(n) : [];
|
|
303
315
|
} catch (e) {
|
|
304
316
|
logWarnError(e);
|
|
305
317
|
return [];
|
|
@@ -323,7 +335,7 @@ export class LikeC4ModelParser {
|
|
|
323
335
|
}
|
|
324
336
|
nonexhaustive(astRule);
|
|
325
337
|
}
|
|
326
|
-
parseElementView(astNode) {
|
|
338
|
+
parseElementView(astNode, isValid2) {
|
|
327
339
|
const body = astNode.body;
|
|
328
340
|
invariant(body, "ElementView body is not defined");
|
|
329
341
|
const astPath = this.getAstNodePath(astNode);
|
|
@@ -345,8 +357,8 @@ export class LikeC4ModelParser {
|
|
|
345
357
|
viewOf ?? ""
|
|
346
358
|
);
|
|
347
359
|
}
|
|
348
|
-
const title = body.props.find((p) => p.key === "title")?.value;
|
|
349
|
-
const description = body.props.find((p) => p.key === "description")?.value;
|
|
360
|
+
const title = toSingleLine(body.props.find((p) => p.key === "title")?.value);
|
|
361
|
+
const description = removeIndent(body.props.find((p) => p.key === "description")?.value);
|
|
350
362
|
const tags = this.convertTags(body);
|
|
351
363
|
const links = body.props.filter(ast.isLinkProperty).map((p) => p.value);
|
|
352
364
|
const basic = {
|
|
@@ -359,7 +371,7 @@ export class LikeC4ModelParser {
|
|
|
359
371
|
...isNonEmptyArray(links) && { links },
|
|
360
372
|
rules: body.rules.flatMap((n) => {
|
|
361
373
|
try {
|
|
362
|
-
return this.parseViewRule(n);
|
|
374
|
+
return isValid2(n) ? this.parseViewRule(n, isValid2) : [];
|
|
363
375
|
} catch (e) {
|
|
364
376
|
logWarnError(e);
|
|
365
377
|
return [];
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { invariant, nonexhaustive } from "@likec4/core";
|
|
2
|
-
import { Range
|
|
2
|
+
import { Range } from "vscode-languageserver-protocol";
|
|
3
|
+
import { changeElementStyle } from "./changeElementStyle.js";
|
|
3
4
|
import { changeViewLayout } from "./changeViewLayout.js";
|
|
4
|
-
import { changeViewStyle } from "./changeViewStyle.js";
|
|
5
5
|
function unionRangeOfAllEdits(edits) {
|
|
6
6
|
let start = Number.MAX_SAFE_INTEGER;
|
|
7
7
|
let end = Number.MIN_SAFE_INTEGER;
|
|
8
|
-
let startCharacter =
|
|
9
|
-
let endCharacter =
|
|
8
|
+
let startCharacter = Number.MAX_SAFE_INTEGER;
|
|
9
|
+
let endCharacter = Number.MIN_SAFE_INTEGER;
|
|
10
10
|
for (const { range } of edits) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
if (range.start.line <= start) {
|
|
12
|
+
if (start == range.start.line) {
|
|
13
|
+
startCharacter = Math.min(range.start.character, startCharacter);
|
|
14
|
+
} else {
|
|
15
|
+
start = range.start.line;
|
|
16
|
+
startCharacter = range.start.character;
|
|
17
|
+
}
|
|
14
18
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
if (end <= range.end.line) {
|
|
20
|
+
if (end == range.end.line) {
|
|
21
|
+
endCharacter = Math.max(range.end.character, endCharacter);
|
|
22
|
+
} else {
|
|
23
|
+
end = range.end.line;
|
|
24
|
+
endCharacter = range.end.character;
|
|
25
|
+
}
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
28
|
return Range.create(start, startCharacter, end, endCharacter);
|
|
@@ -41,9 +49,9 @@ export class LikeC4ModelChanges {
|
|
|
41
49
|
const applyResult = await lspConnection.workspace.applyEdit({
|
|
42
50
|
label: `LikeC4 - change view ${changeView.viewId}`,
|
|
43
51
|
edit: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
changes: {
|
|
53
|
+
[textDocument.uri]: edits
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
});
|
|
49
57
|
if (!applyResult.applied) {
|
|
@@ -65,21 +73,11 @@ export class LikeC4ModelChanges {
|
|
|
65
73
|
const edits = [];
|
|
66
74
|
for (const change of changes) {
|
|
67
75
|
switch (change.op) {
|
|
68
|
-
case "change-
|
|
69
|
-
edits.push(...
|
|
70
|
-
...lookup,
|
|
71
|
-
targets: change.targets,
|
|
72
|
-
key: "color",
|
|
73
|
-
value: change.color
|
|
74
|
-
}));
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
case "change-shape": {
|
|
78
|
-
edits.push(...changeViewStyle(this.services, {
|
|
76
|
+
case "change-element-style": {
|
|
77
|
+
edits.push(...changeElementStyle(this.services, {
|
|
79
78
|
...lookup,
|
|
80
79
|
targets: change.targets,
|
|
81
|
-
|
|
82
|
-
value: change.shape
|
|
80
|
+
style: change.style
|
|
83
81
|
}));
|
|
84
82
|
break;
|
|
85
83
|
}
|
|
@@ -2,14 +2,14 @@ import { type Fqn, type NonEmptyArray } from '@likec4/core';
|
|
|
2
2
|
import { TextEdit } from 'vscode-languageserver-protocol';
|
|
3
3
|
import { ast, type ParsedAstElementView, type ParsedLikeC4LangiumDocument } from '../ast';
|
|
4
4
|
import type { LikeC4Services } from '../module';
|
|
5
|
-
type
|
|
5
|
+
import type { ChangeView } from '../protocol';
|
|
6
|
+
type ChangeElementStyleArg = {
|
|
6
7
|
view: ParsedAstElementView;
|
|
7
8
|
doc: ParsedLikeC4LangiumDocument;
|
|
8
9
|
viewAst: ast.ElementView;
|
|
9
|
-
key: string;
|
|
10
|
-
value: string;
|
|
11
10
|
targets: NonEmptyArray<Fqn>;
|
|
11
|
+
style: ChangeView.ChangeElementStyle['style'];
|
|
12
12
|
};
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function changeElementStyle(services: LikeC4Services, { view, viewAst, targets, style }: ChangeElementStyleArg): TextEdit[];
|
|
14
14
|
export {};
|
|
15
|
-
//# sourceMappingURL=
|
|
15
|
+
//# sourceMappingURL=changeElementStyle.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { invariant, isAncestor, nonNullable } from "@likec4/core";
|
|
2
|
+
import { GrammarUtils } from "langium";
|
|
3
|
+
import { entries, findLast, isNumber, last } from "remeda";
|
|
4
|
+
import { TextEdit } from "vscode-languageserver-protocol";
|
|
5
|
+
import { ast } from "../ast.js";
|
|
6
|
+
const { findNodeForKeyword, findNodeForProperty } = GrammarUtils;
|
|
7
|
+
const asViewStyleRule = (target, style, indent = 0) => {
|
|
8
|
+
const indentStr = indent > 0 ? " ".repeat(indent) : "";
|
|
9
|
+
return [
|
|
10
|
+
indentStr + `style ${target} {`,
|
|
11
|
+
...entries(style).map(([key, value]) => indentStr + ` ${key} ${value}`),
|
|
12
|
+
indentStr + `}`
|
|
13
|
+
].join("\n");
|
|
14
|
+
};
|
|
15
|
+
const isMatchingViewRule = (fqn, index) => (rule) => {
|
|
16
|
+
if (!ast.isViewRuleStyle(rule)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const [target, ...rest] = rule.targets;
|
|
20
|
+
if (!target || rest.length > 0 || !ast.isElementRef(target)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const ref = target.el.ref;
|
|
24
|
+
const _fqn = ref ? index.getFqn(ref) : null;
|
|
25
|
+
return _fqn === fqn;
|
|
26
|
+
};
|
|
27
|
+
export function changeElementStyle(services, {
|
|
28
|
+
view,
|
|
29
|
+
viewAst,
|
|
30
|
+
targets,
|
|
31
|
+
style
|
|
32
|
+
}) {
|
|
33
|
+
const viewCstNode = viewAst.$cstNode;
|
|
34
|
+
invariant(viewCstNode, "viewCstNode");
|
|
35
|
+
const insertPos = last(viewAst.body.rules)?.$cstNode?.range.end ?? last(viewAst.body.props)?.$cstNode?.range.end ?? viewAst.body.$cstNode?.range.start;
|
|
36
|
+
invariant(insertPos, "insertPos is not defined");
|
|
37
|
+
const indent = viewCstNode.range.start.character + 2;
|
|
38
|
+
const fqnIndex = services.likec4.FqnIndex;
|
|
39
|
+
const styleRules = viewAst.body.rules.filter(ast.isViewRuleStyle);
|
|
40
|
+
const viewOf = view.viewOf;
|
|
41
|
+
const existing = [];
|
|
42
|
+
const insert = [];
|
|
43
|
+
targets.forEach((target) => {
|
|
44
|
+
const rule = findLast(styleRules, isMatchingViewRule(target, fqnIndex));
|
|
45
|
+
const fqn = viewOf && isAncestor(viewOf, target) ? target.substring(viewOf.length + 1) : target;
|
|
46
|
+
if (rule) {
|
|
47
|
+
existing.push({ fqn, rule });
|
|
48
|
+
} else {
|
|
49
|
+
insert.push({ fqn });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const modifiedRange = {
|
|
53
|
+
start: insertPos,
|
|
54
|
+
end: insertPos
|
|
55
|
+
};
|
|
56
|
+
const includeRange = (range) => {
|
|
57
|
+
if (range.start.line <= modifiedRange.start.line) {
|
|
58
|
+
if (range.start.line == modifiedRange.start.line) {
|
|
59
|
+
modifiedRange.start.character = Math.min(range.start.character, modifiedRange.start.character);
|
|
60
|
+
} else {
|
|
61
|
+
modifiedRange.start.line = range.start.line;
|
|
62
|
+
modifiedRange.start.character = range.start.character;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (range.end.line >= modifiedRange.end.line) {
|
|
66
|
+
if (range.end.line == modifiedRange.end.line) {
|
|
67
|
+
modifiedRange.end.character = Math.max(range.end.character, modifiedRange.end.character);
|
|
68
|
+
} else {
|
|
69
|
+
modifiedRange.end.line = range.end.line;
|
|
70
|
+
modifiedRange.end.character = range.end.character;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const edits = [];
|
|
75
|
+
if (insert.length > 0) {
|
|
76
|
+
const linesToInsert = [
|
|
77
|
+
"",
|
|
78
|
+
...insert.map(({ fqn }) => asViewStyleRule(fqn, style, indent))
|
|
79
|
+
];
|
|
80
|
+
edits.push(
|
|
81
|
+
TextEdit.insert(
|
|
82
|
+
insertPos,
|
|
83
|
+
linesToInsert.join("\n")
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
modifiedRange.start = {
|
|
87
|
+
line: insertPos.line + 1,
|
|
88
|
+
character: indent
|
|
89
|
+
};
|
|
90
|
+
modifiedRange.end = {
|
|
91
|
+
line: insertPos.line + linesToInsert.length,
|
|
92
|
+
character: indent + (last(linesToInsert)?.length ?? 0) + 1
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (existing.length > 0) {
|
|
96
|
+
for (const { rule } of existing) {
|
|
97
|
+
const ruleCstNode = rule.$cstNode;
|
|
98
|
+
invariant(ruleCstNode, "RuleCstNode not found");
|
|
99
|
+
for (const [key, _value] of entries.strict(style)) {
|
|
100
|
+
const value = isNumber(_value) ? _value.toString() + "%" : _value;
|
|
101
|
+
const ruleProp = rule.styleprops.find((p) => p.key === key);
|
|
102
|
+
if (ruleProp && ruleProp.$cstNode) {
|
|
103
|
+
const { range: { start, end } } = nonNullable(
|
|
104
|
+
findNodeForProperty(ruleProp.$cstNode, "value"),
|
|
105
|
+
"cant find value cst node"
|
|
106
|
+
);
|
|
107
|
+
includeRange({
|
|
108
|
+
start,
|
|
109
|
+
end: {
|
|
110
|
+
line: end.line,
|
|
111
|
+
character: start.character + value.length + 1
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
edits.push(TextEdit.replace({ start, end }, value));
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const insertPos2 = findNodeForKeyword(ruleCstNode, "{")?.range.end;
|
|
118
|
+
invariant(insertPos2, "Opening brace not found");
|
|
119
|
+
const indentStr = " ".repeat(2 + ruleCstNode.range.start.character);
|
|
120
|
+
const insertKeyValue = indentStr + key + " " + value;
|
|
121
|
+
edits.push(
|
|
122
|
+
TextEdit.insert(
|
|
123
|
+
insertPos2,
|
|
124
|
+
"\n" + insertKeyValue
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
includeRange({
|
|
128
|
+
start: {
|
|
129
|
+
line: insertPos2.line + 1,
|
|
130
|
+
character: indentStr.length + 1
|
|
131
|
+
},
|
|
132
|
+
end: {
|
|
133
|
+
line: insertPos2.line + 1,
|
|
134
|
+
character: insertKeyValue.length + 1
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return edits;
|
|
141
|
+
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AutoLayoutDirection, ComputedView, ElementShape, Fqn, LikeC4ComputedModel, LikeC4Model, NonEmptyArray, RelationID, ThemeColor, ViewID } from '@likec4/core';
|
|
1
|
+
import type { AutoLayoutDirection, BorderStyle, ComputedView, ElementShape, Fqn, LikeC4ComputedModel, LikeC4Model, NonEmptyArray, RelationID, ThemeColor, ViewID } from '@likec4/core';
|
|
2
2
|
import type { DocumentUri, Location } from 'vscode-languageserver-protocol';
|
|
3
3
|
import { NotificationType, RequestType, RequestType0 } from 'vscode-languageserver-protocol';
|
|
4
4
|
export declare const onDidChangeModel: NotificationType<string>;
|
|
@@ -33,22 +33,22 @@ export type LocateParams = {
|
|
|
33
33
|
export declare const locate: RequestType<LocateParams, Location | null, void>;
|
|
34
34
|
export type LocateRequest = typeof locate;
|
|
35
35
|
export declare namespace ChangeView {
|
|
36
|
-
interface ChangeColor {
|
|
37
|
-
op: 'change-color';
|
|
38
|
-
color: ThemeColor;
|
|
39
|
-
targets: NonEmptyArray<Fqn>;
|
|
40
|
-
}
|
|
41
|
-
interface ChangeShape {
|
|
42
|
-
op: 'change-shape';
|
|
43
|
-
shape: ElementShape;
|
|
44
|
-
targets: NonEmptyArray<Fqn>;
|
|
45
|
-
}
|
|
46
36
|
interface ChangeAutoLayout {
|
|
47
37
|
op: 'change-autolayout';
|
|
48
38
|
layout: AutoLayoutDirection;
|
|
49
39
|
}
|
|
40
|
+
interface ChangeElementStyle {
|
|
41
|
+
op: 'change-element-style';
|
|
42
|
+
style: {
|
|
43
|
+
border?: BorderStyle;
|
|
44
|
+
opacity?: number;
|
|
45
|
+
shape?: ElementShape;
|
|
46
|
+
color?: ThemeColor;
|
|
47
|
+
};
|
|
48
|
+
targets: NonEmptyArray<Fqn>;
|
|
49
|
+
}
|
|
50
50
|
}
|
|
51
|
-
export type ChangeView = ChangeView.
|
|
51
|
+
export type ChangeView = ChangeView.ChangeAutoLayout | ChangeView.ChangeElementStyle;
|
|
52
52
|
export interface ChangeViewRequestParams {
|
|
53
53
|
viewId: ViewID;
|
|
54
54
|
changes: NonEmptyArray<ChangeView>;
|
|
@@ -95,9 +95,11 @@ export class LikeC4ScopeComputation extends DefaultScopeComputation {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
localScope.
|
|
98
|
+
if (nestedScopes.size > 0) {
|
|
99
|
+
for (const [name, descriptions] of nestedScopes.entriesGroupedByKey()) {
|
|
100
|
+
if (!localScope.has(name) && descriptions.length === 1) {
|
|
101
|
+
localScope.add(name, descriptions[0]);
|
|
102
|
+
}
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
scopes.addAll(container, localScope.values());
|
package/dist/validation/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { logger } from "../logger.js";
|
|
2
2
|
import { elementChecks } from "./element.js";
|
|
3
|
+
import { opacityPropertyRuleChecks } from "./property-checks.js";
|
|
3
4
|
import { relationChecks } from "./relation.js";
|
|
4
5
|
import {
|
|
5
6
|
elementKindChecks,
|
|
@@ -20,6 +21,7 @@ export function registerValidationChecks(services) {
|
|
|
20
21
|
logger.info("registerValidationChecks");
|
|
21
22
|
const registry = services.validation.ValidationRegistry;
|
|
22
23
|
registry.register({
|
|
24
|
+
OpacityProperty: opacityPropertyRuleChecks(services),
|
|
23
25
|
SpecificationRule: specificationRuleChecks(services),
|
|
24
26
|
Model: modelRuleChecks(services),
|
|
25
27
|
ModelViews: modelViewsChecks(services),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ValidationCheck } from 'langium';
|
|
2
|
+
import type { ast } from '../ast';
|
|
3
|
+
import type { LikeC4Services } from '../module';
|
|
4
|
+
export declare const opacityPropertyRuleChecks: (_: LikeC4Services) => ValidationCheck<ast.OpacityProperty>;
|
|
5
|
+
//# sourceMappingURL=property-checks.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const opacityPropertyRuleChecks = (_) => {
|
|
2
|
+
return (node, accept) => {
|
|
3
|
+
const opacity = parseFloat(node.value);
|
|
4
|
+
if (isNaN(opacity) || opacity < 0 || opacity > 100) {
|
|
5
|
+
accept("warning", `Value ignored, must be between 0% and 100%`, {
|
|
6
|
+
node,
|
|
7
|
+
property: "value"
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@likec4/language-server",
|
|
3
3
|
"description": "LikeC4 Language Server",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bugs": "https://github.com/likec4/likec4/issues",
|
|
7
7
|
"homepage": "https://likec4.dev",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"test": "vitest run"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@likec4/core": "1.
|
|
78
|
-
"@likec4/graph": "1.
|
|
77
|
+
"@likec4/core": "1.1.1",
|
|
78
|
+
"@likec4/graph": "1.1.1",
|
|
79
79
|
"@total-typescript/ts-reset": "^0.5.1",
|
|
80
80
|
"fast-equals": "^5.0.1",
|
|
81
81
|
"langium": "^3.0.0",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"@types/node": "^20.12.7",
|
|
96
96
|
"@types/object-hash": "^3.0.6",
|
|
97
97
|
"@types/string-hash": "^1",
|
|
98
|
-
"execa": "^
|
|
98
|
+
"execa": "^9.1.0",
|
|
99
99
|
"langium-cli": "3.0.3",
|
|
100
100
|
"npm-run-all2": "^6.1.2",
|
|
101
101
|
"typescript": "^5.4.5",
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { invariant, isAncestor, nonNullable } from "@likec4/core";
|
|
2
|
-
import { GrammarUtils } from "langium";
|
|
3
|
-
import { findLast, last, partition } from "remeda";
|
|
4
|
-
import { TextEdit } from "vscode-languageserver-protocol";
|
|
5
|
-
import { ast } from "../ast.js";
|
|
6
|
-
const { findNodeForKeyword, findNodeForProperty } = GrammarUtils;
|
|
7
|
-
const asViewStyleRule = (target, key, value, indent = 0) => {
|
|
8
|
-
const indentStr = indent > 0 ? " ".repeat(indent) : "";
|
|
9
|
-
return [
|
|
10
|
-
indentStr + `style ${target} {`,
|
|
11
|
-
indentStr + ` ${key} ${value}`,
|
|
12
|
-
indentStr + `}`
|
|
13
|
-
].join("\n");
|
|
14
|
-
};
|
|
15
|
-
const isMatchingViewRule = (fqn, index) => (rule) => {
|
|
16
|
-
if (!ast.isViewRuleStyle(rule)) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
const [target, ...rest] = rule.targets;
|
|
20
|
-
if (!target || rest.length > 0 || !ast.isElementRef(target)) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
const ref = target.el.ref;
|
|
24
|
-
const _fqn = ref ? index.getFqn(ref) : null;
|
|
25
|
-
return _fqn === fqn;
|
|
26
|
-
};
|
|
27
|
-
export function changeViewStyle(services, {
|
|
28
|
-
view,
|
|
29
|
-
viewAst,
|
|
30
|
-
targets,
|
|
31
|
-
key,
|
|
32
|
-
value
|
|
33
|
-
}) {
|
|
34
|
-
const viewCstNode = viewAst.$cstNode;
|
|
35
|
-
invariant(viewCstNode, "viewCstNode");
|
|
36
|
-
const insertPos = last(viewAst.body.rules)?.$cstNode?.range.end ?? last(viewAst.body.props)?.$cstNode?.range.end ?? viewAst.body.$cstNode?.range.start;
|
|
37
|
-
invariant(insertPos, "insertPos is not defined");
|
|
38
|
-
const indent = viewCstNode.range.start.character + 2;
|
|
39
|
-
const fqnIndex = services.likec4.FqnIndex;
|
|
40
|
-
const styleRules = viewAst.body.rules.filter(ast.isViewRuleStyle);
|
|
41
|
-
const viewOf = view.viewOf;
|
|
42
|
-
const targetsWithRules = targets.map((target) => {
|
|
43
|
-
const rule = findLast(styleRules, isMatchingViewRule(target, fqnIndex));
|
|
44
|
-
const fqn = viewOf && isAncestor(viewOf, target) ? target.substring(viewOf.length + 1) : target;
|
|
45
|
-
if (rule) {
|
|
46
|
-
return {
|
|
47
|
-
fqn,
|
|
48
|
-
rule
|
|
49
|
-
};
|
|
50
|
-
} else {
|
|
51
|
-
return {
|
|
52
|
-
fqn
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
const [existing, insert] = partition(
|
|
57
|
-
targetsWithRules,
|
|
58
|
-
(a) => !!a.rule
|
|
59
|
-
);
|
|
60
|
-
const modifiedRange = {
|
|
61
|
-
start: insertPos,
|
|
62
|
-
end: insertPos
|
|
63
|
-
};
|
|
64
|
-
const includeRange = (range) => {
|
|
65
|
-
if (range.start.line < modifiedRange.start.line) {
|
|
66
|
-
modifiedRange.start = range.start;
|
|
67
|
-
}
|
|
68
|
-
if (range.end.line > modifiedRange.end.line) {
|
|
69
|
-
modifiedRange.end = range.end;
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
const edits = [];
|
|
73
|
-
if (insert.length > 0) {
|
|
74
|
-
const linesToInsert = [
|
|
75
|
-
"",
|
|
76
|
-
...insert.map(({ fqn }) => asViewStyleRule(fqn, key, value, indent))
|
|
77
|
-
];
|
|
78
|
-
edits.push(
|
|
79
|
-
TextEdit.insert(
|
|
80
|
-
insertPos,
|
|
81
|
-
linesToInsert.join("\n")
|
|
82
|
-
)
|
|
83
|
-
);
|
|
84
|
-
modifiedRange.end = {
|
|
85
|
-
line: modifiedRange.end.line + linesToInsert.length,
|
|
86
|
-
character: last(linesToInsert)?.length ?? insertPos.character
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
if (existing.length > 0) {
|
|
90
|
-
for (const { rule } of existing) {
|
|
91
|
-
const ruleCstNode = rule.$cstNode;
|
|
92
|
-
invariant(ruleCstNode, "RuleCstNode not found");
|
|
93
|
-
const ruleProp = rule.styleprops.find((p) => p.key === key);
|
|
94
|
-
if (ruleProp && ruleProp.$cstNode) {
|
|
95
|
-
const { range: { start, end } } = nonNullable(
|
|
96
|
-
findNodeForProperty(ruleProp.$cstNode, "value"),
|
|
97
|
-
"cant find value cst node"
|
|
98
|
-
);
|
|
99
|
-
includeRange(ruleProp.$cstNode.range);
|
|
100
|
-
edits.push(TextEdit.replace({ start, end }, value));
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const insertPos2 = findNodeForKeyword(ruleCstNode, "{")?.range.end;
|
|
104
|
-
invariant(insertPos2, "Opening brace not found");
|
|
105
|
-
const indentStr = " ".repeat(2 + ruleCstNode.range.start.character);
|
|
106
|
-
const insertKeyValue = indentStr + key + " " + value;
|
|
107
|
-
edits.push(
|
|
108
|
-
TextEdit.insert(
|
|
109
|
-
insertPos2,
|
|
110
|
-
"\n" + insertKeyValue
|
|
111
|
-
)
|
|
112
|
-
);
|
|
113
|
-
includeRange({
|
|
114
|
-
start: insertPos2,
|
|
115
|
-
end: {
|
|
116
|
-
line: insertPos2.line + 1,
|
|
117
|
-
character: insertKeyValue.length
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return edits;
|
|
123
|
-
}
|