@platformos/platformos-language-server-common 0.0.13 → 0.0.16
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/CHANGELOG.md +34 -0
- package/dist/codeActions/CodeActionsProvider.d.ts +1 -1
- package/dist/definitions/providers/RenderPartialDefinitionProvider.js +1 -1
- package/dist/definitions/providers/RenderPartialDefinitionProvider.js.map +1 -1
- package/dist/documentLinks/DocumentLinksProvider.js +4 -2
- package/dist/documentLinks/DocumentLinksProvider.js.map +1 -1
- package/dist/test/MockConnection.d.ts +1 -1
- package/dist/test/MockConnection.js +11 -4
- package/dist/test/MockConnection.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/definitions/providers/PageRouteDefinitionProvider.spec.ts +23 -0
- package/src/definitions/providers/RenderPartialDefinitionProvider.spec.ts +37 -2
- package/src/definitions/providers/RenderPartialDefinitionProvider.ts +1 -1
- package/src/documentLinks/DocumentLinksProvider.ts +8 -7
- package/src/server/startServer.spec.ts +62 -0
- package/src/test/MockConnection.ts +16 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformos/platformos-language-server-common",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "platformOS",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"type-check": "tsc --noEmit -p ./tsconfig.json"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@platformos/liquid-html-parser": "^0.0.
|
|
31
|
-
"@platformos/platformos-check-common": "0.0.
|
|
32
|
-
"@platformos/platformos-graph": "0.0.
|
|
30
|
+
"@platformos/liquid-html-parser": "^0.0.15",
|
|
31
|
+
"@platformos/platformos-check-common": "0.0.16",
|
|
32
|
+
"@platformos/platformos-graph": "0.0.16",
|
|
33
33
|
"@vscode/web-custom-data": "^0.4.6",
|
|
34
34
|
"graphql": "^16.12.0",
|
|
35
35
|
"graphql-language-service": "^5.2.2",
|
|
@@ -547,6 +547,29 @@ describe('Module: PageRouteDefinitionProvider', () => {
|
|
|
547
547
|
expect(result).toHaveLength(1);
|
|
548
548
|
expect(result[0].targetUri).toBe('file:///project/app/views/pages/about.html.liquid');
|
|
549
549
|
});
|
|
550
|
+
|
|
551
|
+
it('navigates when assign is inside a {% liquid %} block in the same container', async () => {
|
|
552
|
+
setup({
|
|
553
|
+
'app/views/pages/about.html.liquid': '<h1>About</h1>',
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const source =
|
|
557
|
+
'{% if true %}{% liquid\n assign url = "/about"\n%}<a href="{{ url }}">About</a>{% endif %}';
|
|
558
|
+
documentManager.open('file:///project/app/views/pages/home.html.liquid', source, 1);
|
|
559
|
+
|
|
560
|
+
const urlOffset = source.indexOf('{{ url }}');
|
|
561
|
+
const urlLine = source.slice(0, urlOffset).split('\n').length - 1;
|
|
562
|
+
const urlChar = urlOffset - source.lastIndexOf('\n', urlOffset - 1) - 1;
|
|
563
|
+
const params: DefinitionParams = {
|
|
564
|
+
textDocument: { uri: 'file:///project/app/views/pages/home.html.liquid' },
|
|
565
|
+
position: { line: urlLine, character: urlChar + 3 }, // Inside {{ url }}
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const result = await provider.definitions(params);
|
|
569
|
+
assert(result);
|
|
570
|
+
expect(result).toHaveLength(1);
|
|
571
|
+
expect(result[0].targetUri).toBe('file:///project/app/views/pages/about.html.liquid');
|
|
572
|
+
});
|
|
550
573
|
});
|
|
551
574
|
|
|
552
575
|
describe('format-aware go-to-definition', () => {
|
|
@@ -48,13 +48,17 @@ describe('RenderPartialDefinitionProvider', () => {
|
|
|
48
48
|
expect(result[0].targetUri).toBe('file:///project/app/views/partials/card.liquid');
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
it('should NOT use search paths for regular render tags', async () => {
|
|
51
|
+
it('should NOT use search paths for regular render tags — falls back to default creation path', async () => {
|
|
52
|
+
// The file only exists under a search path (theme/dress/card.liquid), not the standard path.
|
|
53
|
+
// render should NOT pick up search-path files; instead it falls back to the default
|
|
54
|
+
// app/views/partials/card.liquid creation path.
|
|
52
55
|
const result = await getDefinitions("{% render 'card' %}", 12, {
|
|
53
56
|
'project/app/config.yml': 'theme_search_paths:\n - theme/dress',
|
|
54
57
|
'project/app/views/partials/theme/dress/card.liquid': 'dress card',
|
|
55
58
|
});
|
|
56
59
|
|
|
57
|
-
expect(result).toHaveLength(
|
|
60
|
+
expect(result).toHaveLength(1);
|
|
61
|
+
expect(result[0].targetUri).toBe('file:///project/app/views/partials/card.liquid');
|
|
58
62
|
});
|
|
59
63
|
});
|
|
60
64
|
|
|
@@ -130,6 +134,37 @@ describe('RenderPartialDefinitionProvider', () => {
|
|
|
130
134
|
});
|
|
131
135
|
});
|
|
132
136
|
|
|
137
|
+
describe('missing files — fall back to default path', () => {
|
|
138
|
+
it('render: missing partial resolves to app/views/partials', async () => {
|
|
139
|
+
const result = await getDefinitions("{% render 'my/missing/partial' %}", 12, {});
|
|
140
|
+
|
|
141
|
+
expect(result).toHaveLength(1);
|
|
142
|
+
expect(result[0].targetUri).toBe(
|
|
143
|
+
'file:///project/app/views/partials/my/missing/partial.liquid',
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('function: missing partial resolves to app/lib', async () => {
|
|
148
|
+
const result = await getDefinitions("{% function result = 'commands/missing' %}", 24, {});
|
|
149
|
+
|
|
150
|
+
expect(result).toHaveLength(1);
|
|
151
|
+
expect(result[0].targetUri).toBe('file:///project/app/lib/commands/missing.liquid');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('graphql: missing file resolves to app/graphql', async () => {
|
|
155
|
+
const result = await getDefinitions("{% graphql g = 'users/missing' %}", 18, {});
|
|
156
|
+
|
|
157
|
+
expect(result).toHaveLength(1);
|
|
158
|
+
expect(result[0].targetUri).toBe('file:///project/app/graphql/users/missing.graphql');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('theme_render_rc: missing file returns empty (no default path)', async () => {
|
|
162
|
+
const result = await getDefinitions("{% theme_render_rc 'missing' %}", 20, {});
|
|
163
|
+
|
|
164
|
+
expect(result).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
133
168
|
describe('non-matching nodes', () => {
|
|
134
169
|
it('should return empty for non-string nodes', async () => {
|
|
135
170
|
const result = await getDefinitions("{% assign x = 'hello' %}", 3, {});
|
|
@@ -47,7 +47,7 @@ export class RenderPartialDefinitionProvider implements BaseDefinitionProvider {
|
|
|
47
47
|
const root = URI.parse(rootUri);
|
|
48
48
|
const searchPaths = await this.searchPathsCache.get(root);
|
|
49
49
|
const docType = (tag as LiquidTag).name as DocumentType;
|
|
50
|
-
const fileUri = await this.documentsLocator.
|
|
50
|
+
const fileUri = await this.documentsLocator.locateOrDefault(
|
|
51
51
|
root,
|
|
52
52
|
docType,
|
|
53
53
|
(node as LiquidString).value,
|
|
@@ -64,18 +64,19 @@ function documentLinksVisitor(
|
|
|
64
64
|
|
|
65
65
|
// render, include, function, theme_render_rc all have a .partial field
|
|
66
66
|
if ('partial' in markup && isLiquidString(markup.partial)) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
const uri = await documentsLocator.locateOrDefault(
|
|
68
|
+
root,
|
|
69
|
+
name,
|
|
70
|
+
markup.partial.value,
|
|
71
|
+
searchPaths,
|
|
70
72
|
);
|
|
73
|
+
return DocumentLink.create(range(textDocument, markup.partial), uri);
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
// graphql has a .graphql field
|
|
74
77
|
if ('graphql' in markup && isLiquidString(markup.graphql)) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await documentsLocator.locate(root, name, markup.graphql.value),
|
|
78
|
-
);
|
|
78
|
+
const uri = await documentsLocator.locateOrDefault(root, name, markup.graphql.value);
|
|
79
|
+
return DocumentLink.create(range(textDocument, markup.graphql), uri);
|
|
79
80
|
}
|
|
80
81
|
},
|
|
81
82
|
async LiquidVariable(node) {
|
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
DidRenameFilesNotification,
|
|
8
8
|
FileChangeType,
|
|
9
9
|
PublishDiagnosticsNotification,
|
|
10
|
+
DefinitionRequest,
|
|
10
11
|
} from 'vscode-languageserver';
|
|
11
12
|
import { MockConnection, mockConnection } from '../test/MockConnection';
|
|
12
13
|
import { Dependencies } from '../types';
|
|
13
14
|
import { CHECK_ON_CHANGE, CHECK_ON_OPEN, CHECK_ON_SAVE } from './Configuration';
|
|
14
15
|
import { startServer } from './startServer';
|
|
16
|
+
import { SearchPathsLoader } from '../utils/searchPaths';
|
|
15
17
|
|
|
16
18
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
19
|
|
|
@@ -76,6 +78,7 @@ describe('Module: server', () => {
|
|
|
76
78
|
|
|
77
79
|
afterEach(() => {
|
|
78
80
|
vi.useRealTimers();
|
|
81
|
+
vi.restoreAllMocks();
|
|
79
82
|
});
|
|
80
83
|
|
|
81
84
|
it("should log Let's roll! on successful setup", async () => {
|
|
@@ -315,6 +318,65 @@ describe('Module: server', () => {
|
|
|
315
318
|
);
|
|
316
319
|
});
|
|
317
320
|
|
|
321
|
+
it('go-to-definition reflects updated search paths after saving app/config.yml', async () => {
|
|
322
|
+
// Setup file tree: config pointing to theme/dress, both dress and simple partials present
|
|
323
|
+
fileTree['app/config.yml'] = 'theme_search_paths:\n - theme/dress';
|
|
324
|
+
fileTree['app/views/partials/theme/dress/card.liquid'] = 'dress card';
|
|
325
|
+
fileTree['app/views/partials/theme/simple/card.liquid'] = 'simple card';
|
|
326
|
+
|
|
327
|
+
connection.setup();
|
|
328
|
+
await flushAsync();
|
|
329
|
+
|
|
330
|
+
// Open a document referencing the partial
|
|
331
|
+
const source = "{% theme_render_rc 'card' %}";
|
|
332
|
+
connection.openDocument(filePath, source);
|
|
333
|
+
await flushAsync();
|
|
334
|
+
|
|
335
|
+
// Request definition — character 21 is inside 'card'
|
|
336
|
+
const params = {
|
|
337
|
+
textDocument: { uri: fileURI },
|
|
338
|
+
position: { line: 0, character: 21 },
|
|
339
|
+
};
|
|
340
|
+
const result1 = (await connection.triggerRequest(DefinitionRequest.method, params)) as any[];
|
|
341
|
+
expect(result1).toHaveLength(1);
|
|
342
|
+
expect(result1[0].targetUri).toContain('theme/dress/card.liquid');
|
|
343
|
+
|
|
344
|
+
// Mutate config to point to theme/simple, then save the config file
|
|
345
|
+
fileTree['app/config.yml'] = 'theme_search_paths:\n - theme/simple';
|
|
346
|
+
connection.saveDocument('app/config.yml');
|
|
347
|
+
await flushAsync();
|
|
348
|
+
|
|
349
|
+
// Definition should now resolve to theme/simple
|
|
350
|
+
const result2 = (await connection.triggerRequest(DefinitionRequest.method, params)) as any[];
|
|
351
|
+
expect(result2).toHaveLength(1);
|
|
352
|
+
expect(result2[0].targetUri).toContain('theme/simple/card.liquid');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should invalidate search-paths cache immediately when app/config.yml is saved', async () => {
|
|
356
|
+
connection.setup();
|
|
357
|
+
await flushAsync();
|
|
358
|
+
|
|
359
|
+
const invalidateSpy = vi.spyOn(SearchPathsLoader.prototype, 'invalidate');
|
|
360
|
+
|
|
361
|
+
// Saving app/config.yml should immediately invalidate the cache
|
|
362
|
+
connection.saveDocument('app/config.yml');
|
|
363
|
+
await flushAsync();
|
|
364
|
+
|
|
365
|
+
expect(invalidateSpy).toHaveBeenCalledOnce();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should NOT invalidate search-paths cache when an unrelated file is saved', async () => {
|
|
369
|
+
connection.setup();
|
|
370
|
+
await flushAsync();
|
|
371
|
+
|
|
372
|
+
const invalidateSpy = vi.spyOn(SearchPathsLoader.prototype, 'invalidate');
|
|
373
|
+
|
|
374
|
+
connection.saveDocument('app/views/partials/code.liquid');
|
|
375
|
+
await flushAsync();
|
|
376
|
+
|
|
377
|
+
expect(invalidateSpy).not.toHaveBeenCalled();
|
|
378
|
+
});
|
|
379
|
+
|
|
318
380
|
it('should trigger a re-check on did delete files notifications', async () => {
|
|
319
381
|
connection.setup();
|
|
320
382
|
await flushAsync();
|
|
@@ -2,6 +2,7 @@ import { vi } from 'vitest';
|
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { createConnection } from 'vscode-languageserver/lib/common/server';
|
|
4
4
|
import {
|
|
5
|
+
CancellationToken,
|
|
5
6
|
ClientCapabilities,
|
|
6
7
|
DidChangeTextDocumentNotification,
|
|
7
8
|
DidCloseTextDocumentNotification,
|
|
@@ -47,7 +48,11 @@ type MockConnectionMethods = {
|
|
|
47
48
|
*/
|
|
48
49
|
export type MockConnection = ReturnType<typeof createConnection> & MockConnectionMethods;
|
|
49
50
|
|
|
50
|
-
function protocolConnection(
|
|
51
|
+
function protocolConnection(
|
|
52
|
+
requests: EventEmitter,
|
|
53
|
+
notifications: EventEmitter,
|
|
54
|
+
requestHandlers: Map<string, Function>,
|
|
55
|
+
) {
|
|
51
56
|
return {
|
|
52
57
|
dispose: vi.fn(),
|
|
53
58
|
end: vi.fn(),
|
|
@@ -61,12 +66,13 @@ function protocolConnection(requests: EventEmitter, notifications: EventEmitter)
|
|
|
61
66
|
notifications.addListener(type.method, handler);
|
|
62
67
|
}),
|
|
63
68
|
onRequest: vi.fn().mockImplementation((type: MessageSignature, handler) => {
|
|
69
|
+
requestHandlers.set(type.method, handler);
|
|
64
70
|
requests.addListener(type.method, handler);
|
|
65
71
|
}),
|
|
66
72
|
onUnhandledNotification: vi.fn(),
|
|
67
73
|
sendNotification: vi.fn().mockReturnValue(Promise.resolve()),
|
|
68
74
|
sendProgress: vi.fn(),
|
|
69
|
-
sendRequest: vi.fn(),
|
|
75
|
+
sendRequest: vi.fn().mockReturnValue(Promise.resolve()),
|
|
70
76
|
trace: vi.fn().mockReturnValue(Promise.resolve()),
|
|
71
77
|
} satisfies ProtocolConnection;
|
|
72
78
|
}
|
|
@@ -80,7 +86,8 @@ export function mockConnection(rootUri: string): MockConnection {
|
|
|
80
86
|
|
|
81
87
|
const requests = new EventEmitter();
|
|
82
88
|
const notifications = new EventEmitter();
|
|
83
|
-
const
|
|
89
|
+
const requestHandlers = new Map<string, Function>();
|
|
90
|
+
const spies = protocolConnection(requests, notifications, requestHandlers);
|
|
84
91
|
|
|
85
92
|
// Create a real "connection" with the fake communication channel
|
|
86
93
|
const connection = createConnection(() => spies, watchDog);
|
|
@@ -92,10 +99,15 @@ export function mockConnection(rootUri: string): MockConnection {
|
|
|
92
99
|
notifications.emit(method, params);
|
|
93
100
|
};
|
|
94
101
|
|
|
95
|
-
// Create a mock way to trigger requests in our tests
|
|
102
|
+
// Create a mock way to trigger requests in our tests and get the response back.
|
|
103
|
+
// Calls the registered handler directly so that the return value is available.
|
|
96
104
|
const triggerRequest: MockConnection['triggerRequest'] = async (...args: any[]) => {
|
|
97
105
|
const [type, params] = args;
|
|
98
106
|
const method = typeof type === 'string' ? type : type.method;
|
|
107
|
+
const handler = requestHandlers.get(method);
|
|
108
|
+
if (handler) {
|
|
109
|
+
return handler(params, CancellationToken.None);
|
|
110
|
+
}
|
|
99
111
|
requests.emit(method, params);
|
|
100
112
|
};
|
|
101
113
|
|