Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit 5a7255d

Browse files
Add injections and wrapping grammars for tree-sitter grammar
1 parent 9505029 commit 5a7255d

File tree

7 files changed

+276
-0
lines changed

7 files changed

+276
-0
lines changed

grammars/tree-sitter-html.cson

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This grammar is responsible for injecting the actual PHP or HTML grammars
2+
# For the actual PHP scopes, see tree-sitter-php.cson
3+
name: 'PHP'
4+
scopeName: 'text.html.php'
5+
type: 'tree-sitter'
6+
parser: 'tree-sitter-embedded-php'
7+
8+
fileTypes: [
9+
'aw'
10+
'ctp'
11+
'inc'
12+
'install'
13+
'module'
14+
'php'
15+
'php_cs'
16+
'php3'
17+
'php4'
18+
'php5'
19+
'phpt'
20+
'phtml'
21+
'profile'
22+
]
23+
24+
firstLineRegex: [
25+
'^\\s*<\\?([pP][hH][pP]|=|\\s|$)'
26+
]
27+
28+
scopes:
29+
'template > content:nth-child(0)': [
30+
{match: /^\#!.*(?:\s|\/)php\d?(?:$|\s)/, scopes: 'comment.line.shebang.php'}
31+
],
32+
33+
'php': [
34+
{match: '\\n', scopes: 'meta.embedded.block.php'}
35+
'meta.embedded.line.php'
36+
]

grammars/tree-sitter-phpdoc.cson

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: 'PHPDoc'
2+
scopeName: 'comment.block.documentation.phpdoc.php'
3+
type: 'tree-sitter'
4+
parser: 'tree-sitter-phpdoc'
5+
6+
injectionRegex: 'phpdoc|PHPDoc'
7+
8+
scopes:
9+
tag_name: 'keyword.other.phpdoc.php'
10+
type_list: 'meta.other.type.phpdoc.php'
11+
'type_list > "|"': 'punctuation.separator.delimiter.php'
12+
'array_type > "[]"': 'keyword.other.array.phpdoc.php'
13+
primitive_type: 'keyword.other.type.php'
14+
'named_type > name': 'support.class.php'
15+
'* > namespace_name_as_prefix': 'support.other.namespace.php'
16+
# FIXME not working
17+
# '* > namespace_name_as_prefix > "\\"': 'punctuation.separator.inheritance.php'
18+
'* > qualified_name > name': 'support.class.php'

lib/main.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
exports.activate = function() {
2+
if (!atom.grammars.addInjectionPoint) return
3+
4+
// inject source.php into text.html.php
5+
atom.grammars.addInjectionPoint('text.html.php', {
6+
type: 'php',
7+
language () { return 'php' },
8+
content (php) { return php }
9+
})
10+
11+
// inject html into text.html.php
12+
atom.grammars.addInjectionPoint('text.html.php', {
13+
type: 'template',
14+
language () { return 'html' },
15+
content (node) { return node.descendantsOfType('content') }
16+
})
17+
18+
// inject phpDoc comments into PHP comments
19+
atom.grammars.addInjectionPoint('source.php', {
20+
type: 'comment',
21+
language (comment) {
22+
if (comment.text.startsWith('/**') && !comment.text.startsWith('/***')) {
23+
return 'phpdoc'
24+
}
25+
},
26+
content (comment) { return comment }
27+
})
28+
}

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"atom": "*",
77
"node": "*"
88
},
9+
"main": "lib/main",
910
"homepage": "http://atom.github.io/language-php",
1011
"repository": {
1112
"type": "git",
@@ -16,7 +17,9 @@
1617
"url": "https://github.com/atom/language-php/issues"
1718
},
1819
"dependencies": {
20+
"tree-sitter-embedded-php": "0.0.4",
1921
"tree-sitter-php-abc": "^0.17.0",
22+
"tree-sitter-phpdoc": "0.0.4"
2023
},
2124
"devDependencies": {
2225
"coffeelint": "^1.10.1",

spec/tree-sitter-helpers.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const dedent = require("dedent");
2+
3+
module.exports = {
4+
// https://github.com/atom/atom/blob/b3d3a52d9e4eb41f33df7b91ad1f8a2657a04487/spec/tree-sitter-language-mode-spec.js#L47-L55
5+
expectTokensToEqual(editor, expectedTokenLines, startingRow = 1) {
6+
const lastRow = editor.getLastScreenRow();
7+
8+
for (let row = startingRow; row <= lastRow - startingRow; row++) {
9+
const tokenLine = editor
10+
.tokensForScreenRow(row)
11+
.map(({ text, scopes }) => ({
12+
text,
13+
scopes: scopes.map((scope) =>
14+
scope
15+
.split(" ")
16+
.map((className) => className.replace("syntax--", ""))
17+
.join(".")
18+
),
19+
}));
20+
21+
const expectedTokenLine = expectedTokenLines[row - startingRow];
22+
23+
expect(tokenLine.length).toEqual(expectedTokenLine.length);
24+
for (let i = 0; i < tokenLine.length; i++) {
25+
expect(tokenLine[i].text).toEqual(
26+
expectedTokenLine[i].text,
27+
`Token ${i}, row: ${row}`
28+
);
29+
expect(tokenLine[i].scopes).toEqual(
30+
expectedTokenLine[i].scopes,
31+
`Token ${i}, row: ${row}, token: '${tokenLine[i].text}'`
32+
);
33+
}
34+
}
35+
},
36+
37+
toHaveScopesAtPosition(posn, token, expected, includeEmbeddedScopes = false) {
38+
if (expected === undefined) {
39+
expected = token;
40+
}
41+
if (token === undefined) {
42+
expected = [];
43+
}
44+
45+
// token is not used at this time; it's just a way to keep note where we are
46+
// in the line
47+
48+
let filterEmbeddedScopes = (scope) =>
49+
includeEmbeddedScopes ||
50+
(scope !== "text.html.php" &&
51+
scope !== "meta.embedded.block.php" &&
52+
scope !== "meta.embedded.line.php");
53+
54+
let actual = this.actual
55+
.scopeDescriptorForBufferPosition(posn)
56+
.scopes.filter(filterEmbeddedScopes);
57+
58+
let notExpected = actual.filter((scope) => !expected.includes(scope));
59+
let notReceived = expected.filter((scope) => !actual.includes(scope));
60+
61+
let pass = notExpected.length === 0 && notReceived.length === 0;
62+
63+
if (pass) {
64+
this.message = () => "Scopes matched";
65+
} else {
66+
let line = this.actual.getBuffer().lineForRow(posn[0]);
67+
let caret = " ".repeat(posn[1]) + "^";
68+
69+
this.message = () =>
70+
`Failure:
71+
Scopes did not match at position [${posn.join(", ")}]:
72+
${line}
73+
${caret}
74+
These scopes were expected but not received:
75+
${notReceived.join(", ")}
76+
These scopes were received but not expected:
77+
${notExpected.join(", ")}
78+
`;
79+
}
80+
81+
return pass;
82+
},
83+
84+
setPhpText(content) {
85+
this.setText(`<?php
86+
${dedent(content)}
87+
`);
88+
},
89+
90+
nextHighlightingUpdate(editor) {
91+
return new Promise((resolve) => {
92+
const subscription = editor
93+
.getBuffer()
94+
.getLanguageMode()
95+
.onDidChangeHighlighting(() => {
96+
subscription.dispose();
97+
resolve();
98+
});
99+
});
100+
},
101+
};

spec/tree-sitter-html-spec.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const dedent = require("dedent");
2+
const {expectTokensToEqual, toHaveScopesAtPosition, nextHighlightingUpdate} = require('./tree-sitter-helpers')
3+
4+
describe("Tree-sitter PHP grammar", () => {
5+
var editor;
6+
7+
beforeEach(async () => {
8+
atom.config.set("core.useTreeSitterParsers", true);
9+
await atom.packages.activatePackage("language-php");
10+
await atom.packages.activatePackage("language-html");
11+
editor = await atom.workspace.open("foo.php");
12+
});
13+
14+
beforeEach(function () {
15+
this.addMatchers({ toHaveScopesAtPosition });
16+
});
17+
18+
describe("loading the grammar", () => {
19+
it('loads the wrapper HTML grammar', () => {
20+
embeddingGrammar = atom.grammars.grammarForScopeName("text.html.php");
21+
expect(embeddingGrammar).toBeTruthy();
22+
expect(embeddingGrammar.scopeName).toBe("text.html.php");
23+
expect(embeddingGrammar.constructor.name).toBe("TreeSitterGrammar");
24+
// FIXME how to test that all selectors were loaded correctly? Invalid
25+
// selectors may generate errors and it would be great to catch those here.
26+
27+
// injections
28+
expect(embeddingGrammar.injectionPointsByType.template).toBeTruthy();
29+
expect(embeddingGrammar.injectionPointsByType.php).toBeTruthy();
30+
})
31+
});
32+
33+
describe("shebang", () => {
34+
it("recognises shebang on the first line of document", () => {
35+
editor.setText(dedent`
36+
#!/usr/bin/env php
37+
<?php echo "test"; ?>
38+
`);
39+
40+
// expect(editor).toHaveScopesAtPosition([0, 0], "#!", ["text.html.php", "comment.line.shebang.php", "punctuation.definition.comment.php"], true);
41+
expect(editor).toHaveScopesAtPosition([0, 1], "#!", ["text.html.php", "comment.line.shebang.php",
42+
// FIXME following scopes differ from TM
43+
'source.html'
44+
], true);
45+
expect(editor).toHaveScopesAtPosition([0, 2], "/usr/bin/env php", ["text.html.php", "comment.line.shebang.php",
46+
// FIXME following scopes differ from TM
47+
'source.html'
48+
], true);
49+
expect(editor).toHaveScopesAtPosition([1, 0], "<?php", ["text.html.php",
50+
// "meta.embedded.line.php", "punctuation.section.embedded.begin.php"
51+
], true);
52+
expect(editor).toHaveScopesAtPosition([1, 1], "<?php", ["text.html.php", "meta.embedded.line.php", "punctuation.section.embedded.begin.php",
53+
// FIXME following scopes differ from TM
54+
'source.php'
55+
], true);
56+
});
57+
58+
it("does not recognize shebang on any of the other lines", () => {
59+
editor.setText(dedent`
60+
61+
#!/usr/bin/env php
62+
<?php echo "test"; ?>
63+
`);
64+
65+
expect(editor).toHaveScopesAtPosition([1, 0], "#!", ["text.html.php"], true);
66+
expect(editor).toHaveScopesAtPosition([1, 1], "#!", ["text.html.php",
67+
// FIXME following scopes differ from TM
68+
'meta.embedded.line.php',
69+
'source.php',
70+
'punctuation.section.embedded.begin.php'
71+
], true);
72+
});
73+
});
74+
});

0 commit comments

Comments
 (0)