Skip to content

Commit 7378bcb

Browse files
Will-Sommerspokey
andauthored
Add support for css and scss (#605)
* Begin to add support for css and scss * Continue to work on SCSS, add new selector * Record some tests for scss * Add more tests, move tests to correct dir level * Handle arguments and parameters correctly, tests * Add tests for CSS, rely on SCSS for CSS matchers * Add tests, clean up functions for merge - Fix edge cases in CSS functions with multiple value single args - Add Selector scope - Clean up function in node selector - Remove declaration from statement scope type - Add declaration to item scope type * Delete single line comment test for CSS - Not valid syntax for CSS, just SASS * Ensure we use scssStringTextExtractor - Fixes `"take round`" for "hello (world)" in css * Remove failing state tests - State redefined as a ruleset selector + block rather than single property * Add `parameters` back to argument matcher * Remove test with invalid CSS * Clean up tests * Clean up CSS selector test * Remove `take state` test * More test clean up * Various Changes - Test cases for cursor positions when selecting keys and values - Allow for name/value selection with optional args in SASS - Start to clean up SCSS argument matcher * Changes to childRangeSelector - Also fix types & clean up findAdjacentArgValues * Fix breaking tests from node selector change. * Include all children for import and includes * Update src/languages/scss.ts Update delimited function check name Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> * Update src/util/nodeSelectors.ts Update childRange docstring for clarity Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com> * Change comment to docstring, handle method rename * Add docstring * Fix doc comment * formatting Co-authored-by: Pokey Rule <755842+pokey@users.noreply.github.com>
1 parent 95890de commit 7378bcb

File tree

117 files changed

+3420
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+3420
-1
lines changed

cursorless-talon/src/modifiers/containing_scope.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"-four section": "sectionLevelFour",
3434
"-five section": "sectionLevelFive",
3535
"-six section": "sectionLevelSix",
36+
"selector": "selector",
3637
"state": "statement",
3738
"string": "string",
3839
"type": "type",

src/languages/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const supportedLanguageIds = [
22
"c",
33
"clojure",
44
"cpp",
5+
"css",
56
"csharp",
67
"go",
78
"html",
@@ -15,6 +16,7 @@ export const supportedLanguageIds = [
1516
"python",
1617
"ruby",
1718
"scala",
19+
"scss",
1820
"typescript",
1921
"typescriptreact",
2022
"xml",

src/languages/getNodeMatcher.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import php from "./php";
1818
import python from "./python";
1919
import markdown from "./markdown";
2020
import scala from "./scala";
21+
import { patternMatchers as scss } from "./scss";
2122
import go from "./go";
2223
import { patternMatchers as ruby } from "./ruby";
2324
import { UnsupportedLanguageError } from "../errors";
@@ -53,6 +54,7 @@ const languageMatchers: Record<
5354
> = {
5455
c: cpp,
5556
cpp,
57+
css: scss,
5658
csharp,
5759
clojure,
5860
go,
@@ -65,8 +67,9 @@ const languageMatchers: Record<
6567
markdown,
6668
php,
6769
python,
68-
ruby,
70+
ruby,
6971
scala,
72+
scss,
7073
typescript,
7174
typescriptreact: typescript,
7275
xml: html,

src/languages/getTextFragmentExtractor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { stringTextFragmentExtractor as jsonStringTextFragmentExtractor } from "
55
import { stringTextFragmentExtractor as phpStringTextFragmentExtractor } from "./php";
66
import { stringTextFragmentExtractor as rubyStringTextFragmentExtractor } from "./ruby";
77
import { stringTextFragmentExtractor as typescriptStringTextFragmentExtractor } from "./typescript";
8+
import { stringTextFragmentExtractor as scssStringTextFragmentExtractor } from "./scss";
89
import { UnsupportedLanguageError } from "../errors";
910
import { Range } from "vscode";
1011
import { SupportedLanguageId } from "./constants";
@@ -131,6 +132,10 @@ const textFragmentExtractors: Record<
131132
),
132133
cpp: constructDefaultTextFragmentExtractor("cpp"),
133134
csharp: constructDefaultTextFragmentExtractor("csharp"),
135+
css: constructDefaultTextFragmentExtractor(
136+
"css",
137+
scssStringTextFragmentExtractor
138+
),
134139
go: constructDefaultTextFragmentExtractor("go"),
135140
html: constructDefaultTextFragmentExtractor(
136141
"html",
@@ -170,6 +175,10 @@ const textFragmentExtractors: Record<
170175
"scala",
171176
constructHackedStringTextFragmentExtractor("scala")
172177
),
178+
scss: constructDefaultTextFragmentExtractor(
179+
"scss",
180+
scssStringTextFragmentExtractor
181+
),
173182
typescript: constructDefaultTextFragmentExtractor(
174183
"typescript",
175184
typescriptStringTextFragmentExtractor

src/languages/scss.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { SyntaxNode } from "web-tree-sitter";
2+
import {
3+
NodeMatcherAlternative,
4+
ScopeType,
5+
SelectionWithEditor,
6+
} from "../typings/Types";
7+
import { patternFinder } from "../util/nodeFinders";
8+
import {
9+
cascadingMatcher,
10+
conditionMatcher,
11+
createPatternMatchers,
12+
matcher,
13+
patternMatcher,
14+
trailingMatcher,
15+
} from "../util/nodeMatchers";
16+
import {
17+
childRangeSelector,
18+
delimitedSelector,
19+
getNodeRange,
20+
} from "../util/nodeSelectors";
21+
22+
// curl https://raw.githubusercontent.com/serenadeai/tree-sitter-scss/c478c6868648eff49eb04a4df90d703dc45b312a/src/node-types.json \
23+
// | jq '[.[] | select(.type =="stylesheet") | .children.types[] | select(.type !="declaration") | .type ]'
24+
25+
const STATEMENT_TYPES = [
26+
"apply_statement",
27+
"at_rule",
28+
"charset_statement",
29+
"debug_statement",
30+
"each_statement",
31+
"error_statement",
32+
"for_statement",
33+
"forward_statement",
34+
"function_statement",
35+
"if_statement",
36+
"import_statement",
37+
"include_statement",
38+
"keyframes_statement",
39+
"media_statement",
40+
"mixin_statement",
41+
"namespace_statement",
42+
"placeholder",
43+
"rule_set",
44+
"supports_statement",
45+
"use_statement",
46+
"warn_statement",
47+
"while_statement",
48+
];
49+
50+
function isArgumentListDelimiter(node: SyntaxNode) {
51+
return [",", "(", ")"].includes(node.type) || isAtDelimiter(node);
52+
}
53+
54+
/**
55+
* Determines whether the given `node` is an `at` delimiter node, used in a css
56+
* / scss argument list. For example, the `at` in the call
57+
* `ellipse(115px 55px at 50% 40%)`
58+
*
59+
* @param node The node to check
60+
* @returns `true` if the node is an `at` delimiter node
61+
*/
62+
function isAtDelimiter(node: SyntaxNode) {
63+
return node.type === "plain_value" && node.text === "at";
64+
}
65+
66+
/**
67+
* Matches adjacent nodes returned from {@link siblingFunc} until it reaches a
68+
* delimiter node. This is intended to handle the case of multiple values
69+
* within two delimiters. e.g. `repeating-linear-gradient(red, orange 50px)`
70+
* @param siblingFunc returns the previous or next sibling of the current node if present.
71+
* @returns A non-delimiter node
72+
*/
73+
function findAdjacentArgValues(
74+
siblingFunc: (node: SyntaxNode) => SyntaxNode | null
75+
) {
76+
return (node: SyntaxNode) => {
77+
// Handle the case where we are the cursor is placed before a delimiter, e.g. "|at"
78+
// and we erroneously expand in both directions.
79+
if (isAtDelimiter(node) || node.type === ",") {
80+
node = node.previousSibling!;
81+
}
82+
83+
let nextPossibleRange = siblingFunc(node);
84+
85+
while (nextPossibleRange && !isArgumentListDelimiter(nextPossibleRange)) {
86+
node = nextPossibleRange;
87+
nextPossibleRange = siblingFunc(nextPossibleRange);
88+
}
89+
return node;
90+
};
91+
}
92+
93+
const nodeMatchers: Partial<Record<ScopeType, NodeMatcherAlternative>> = {
94+
ifStatement: "if_statement",
95+
condition: conditionMatcher("condition"),
96+
statement: cascadingMatcher(
97+
patternMatcher(...STATEMENT_TYPES),
98+
matcher(
99+
patternFinder("attribute_selector"),
100+
childRangeSelector([], ["attribute_name", "string_value"])
101+
)
102+
),
103+
string: "string_value",
104+
functionCall: "call_expression",
105+
namedFunction: ["mixin_statement", "function_statement"],
106+
functionName: ["mixin_statement.name!", "function_statement.name!"],
107+
comment: ["comment", "single_line_comment"],
108+
argumentOrParameter: cascadingMatcher(
109+
matcher(
110+
patternFinder("arguments.*!", "parameters.*!"),
111+
delimitedSelector(
112+
(node) => isArgumentListDelimiter(node),
113+
", ",
114+
findAdjacentArgValues((node) => node.previousSibling),
115+
findAdjacentArgValues((node) => node.nextSibling)
116+
)
117+
)
118+
),
119+
name: [
120+
"function_statement.name!",
121+
"declaration.property_name!",
122+
"declaration.variable_name!",
123+
"mixin_statement.name!",
124+
"attribute_selector.attribute_name!",
125+
"parameter.variable_name!",
126+
],
127+
selector: ["rule_set.selectors!"],
128+
collectionKey: trailingMatcher(["declaration.property_name!"], [":"]),
129+
value: cascadingMatcher(
130+
matcher(
131+
patternFinder("declaration"),
132+
childRangeSelector(["property_name", "variable_name"])
133+
),
134+
matcher(
135+
patternFinder("include_statement", "namespace_statement"),
136+
childRangeSelector()
137+
),
138+
patternMatcher(
139+
"return_statement.*!",
140+
"import_statement.*!",
141+
"attribute_selector.plain_value!",
142+
"attribute_selector.string_value!",
143+
"parameter.default_value!"
144+
)
145+
),
146+
collectionItem: "declaration",
147+
};
148+
149+
export const patternMatchers = createPatternMatchers(nodeMatchers);
150+
151+
export function stringTextFragmentExtractor(
152+
node: SyntaxNode,
153+
_selection: SelectionWithEditor
154+
) {
155+
if (node.type === "string_value") {
156+
return getNodeRange(node);
157+
}
158+
159+
return null;
160+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
languageId: css
2+
command:
3+
version: 1
4+
spokenForm: change argue
5+
action: clearAndSetSelection
6+
targets:
7+
- type: primitive
8+
modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}
9+
initialState:
10+
documentContents: |-
11+
12+
.double {
13+
transform: translate(-50%, -50%);
14+
}
15+
selections:
16+
- anchor: {line: 2, character: 25}
17+
active: {line: 2, character: 25}
18+
marks: {}
19+
finalState:
20+
documentContents: |-
21+
22+
.double {
23+
transform: translate(, -50%);
24+
}
25+
selections:
26+
- anchor: {line: 2, character: 23}
27+
active: {line: 2, character: 23}
28+
thatMark:
29+
- anchor: {line: 2, character: 23}
30+
active: {line: 2, character: 23}
31+
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}, isImplicit: false}]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
languageId: css
2+
command:
3+
version: 1
4+
spokenForm: change argue
5+
action: clearAndSetSelection
6+
targets:
7+
- type: primitive
8+
modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}
9+
initialState:
10+
documentContents: |-
11+
div {
12+
background: repeating-linear-gradient(red, orange 50px);
13+
}
14+
selections:
15+
- anchor: {line: 1, character: 47}
16+
active: {line: 1, character: 47}
17+
marks: {}
18+
finalState:
19+
documentContents: |-
20+
div {
21+
background: repeating-linear-gradient(red, );
22+
}
23+
selections:
24+
- anchor: {line: 1, character: 45}
25+
active: {line: 1, character: 45}
26+
thatMark:
27+
- anchor: {line: 1, character: 45}
28+
active: {line: 1, character: 45}
29+
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}, isImplicit: false}]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
languageId: css
2+
command:
3+
version: 1
4+
spokenForm: change argue
5+
action: clearAndSetSelection
6+
targets:
7+
- type: primitive
8+
modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}
9+
initialState:
10+
documentContents: |-
11+
a {
12+
clip-path: polygon(50% 0%, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
13+
}
14+
15+
selections:
16+
- anchor: {line: 1, character: 27}
17+
active: {line: 1, character: 27}
18+
marks: {}
19+
finalState:
20+
documentContents: |-
21+
a {
22+
clip-path: polygon(, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0% 50%, 40% 40%);
23+
}
24+
25+
selections:
26+
- anchor: {line: 1, character: 21}
27+
active: {line: 1, character: 21}
28+
thatMark:
29+
- anchor: {line: 1, character: 21}
30+
active: {line: 1, character: 21}
31+
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}, isImplicit: false}]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
languageId: css
2+
command:
3+
version: 1
4+
spokenForm: change argue
5+
action: clearAndSetSelection
6+
targets:
7+
- type: primitive
8+
modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}
9+
initialState:
10+
documentContents: |-
11+
a {
12+
clip-path: ellipse(115px 55px at 50% 40%);
13+
}
14+
selections:
15+
- anchor: {line: 1, character: 39}
16+
active: {line: 1, character: 39}
17+
marks: {}
18+
finalState:
19+
documentContents: |-
20+
a {
21+
clip-path: ellipse(115px 55px at );
22+
}
23+
selections:
24+
- anchor: {line: 1, character: 35}
25+
active: {line: 1, character: 35}
26+
thatMark:
27+
- anchor: {line: 1, character: 35}
28+
active: {line: 1, character: 35}
29+
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}, isImplicit: false}]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
languageId: css
2+
command:
3+
version: 1
4+
spokenForm: change argue
5+
action: clearAndSetSelection
6+
targets:
7+
- type: primitive
8+
modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}
9+
initialState:
10+
documentContents: |-
11+
a {
12+
clip-path: ellipse(115px 55px at 50% 40%);
13+
}
14+
selections:
15+
- anchor: {line: 1, character: 25}
16+
active: {line: 1, character: 25}
17+
marks: {}
18+
finalState:
19+
documentContents: |-
20+
a {
21+
clip-path: ellipse( at 50% 40%);
22+
}
23+
selections:
24+
- anchor: {line: 1, character: 21}
25+
active: {line: 1, character: 21}
26+
thatMark:
27+
- anchor: {line: 1, character: 21}
28+
active: {line: 1, character: 21}
29+
fullTargets: [{type: primitive, mark: {type: cursor}, selectionType: token, position: contents, insideOutsideType: inside, modifier: {type: containingScope, scopeType: argumentOrParameter, includeSiblings: false}, isImplicit: false}]

0 commit comments

Comments
 (0)