Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ Syntax validation and error reporting for malformed JRef structure.

The Language Server understands the JRef schema. It surgically identifies `$ref` keys and their destination paths to provide a clear visual distinction.

### Document Symbols

The Language Server implements a Document Symbol Provider that enables the Outline View, Breadcrumbs and Symbol Search
![Jref Document Symbols](images/documentSymbols.png)

## Requirements

1. Node
Expand Down
Binary file added images/documentSymbols.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 2 additions & 6 deletions server/src/providers/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as fs from 'fs';
import { DefinitionParams, DefinitionLink, Range } from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { JRefSymbol, SymbolTable } from '../symbolTable';
import { ServerContext } from '../utils';
import { ServerContext, createRange } from '../utils';
import { analyze } from '../analyzer';

const defaultTargetRange: Range = {
Expand Down Expand Up @@ -93,11 +93,7 @@ function findTargetRange(targetUri: string, fragment: string, context: ServerCon
const targetSymbol = targetSymbolTable.get(fragment || ''); // Default to empty string if no fragment

if (!targetSymbol) return defaultTargetRange;

return {
start: targetDocument.positionAt(targetSymbol.node.offset),
end: targetDocument.positionAt(targetSymbol.node.offset + targetSymbol.node.length),
};
return createRange(targetDocument, targetSymbol.node);
}

function createOriginSelectionRange(document: TextDocument, ref: JRefSymbol): Range {
Expand Down
74 changes: 74 additions & 0 deletions server/src/providers/documentSymbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { DocumentSymbol, SymbolKind, DocumentSymbolParams } from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { Node } from 'jsonc-parser';
import { ServerContext, createRange } from '../utils';

export function onDocumentSymbol(
params: DocumentSymbolParams,
context: ServerContext,
): DocumentSymbol[] {
const { documents, documentSymbols } = context;
const document = documents.get(params.textDocument.uri);
if (!document) return [];

const symbols = documentSymbols.get(document);
if (!symbols) return [];

const rootSymbol = symbols.get('');
if (!rootSymbol) return [];

return createSymbols(rootSymbol.node, document);
}

function createSymbols(node: Node, document: TextDocument): DocumentSymbol[] {
const symbols: DocumentSymbol[] = [];

if (node.type === 'object') {
node.children?.forEach((child) => {
if (child.type === 'property' && child.children?.length === 2) {
const keyNode = child.children[0];
const valueNode = child.children[1];

const symbol: DocumentSymbol = {
name: keyNode.value,
kind: getSymbolKind(valueNode),
range: createRange(document, child),
selectionRange: createRange(document, keyNode),
children: createSymbols(valueNode, document),
};
symbols.push(symbol);
}
});
} else if (node.type === 'array') {
node.children?.forEach((child, index) => {
const range = createRange(document, child);
const symbol: DocumentSymbol = {
name: index.toString(),
kind: getSymbolKind(child),
range: range,
selectionRange: range,
children: createSymbols(child, document),
};
symbols.push(symbol);
});
}

return symbols;
}

function getSymbolKind(node: Node): SymbolKind {
switch (node.type) {
case 'object':
return SymbolKind.Object;
case 'array':
return SymbolKind.Array;
case 'string':
return SymbolKind.String;
case 'number':
return SymbolKind.Number;
case 'boolean':
return SymbolKind.Boolean;
default:
return SymbolKind.Property;
}
}
4 changes: 4 additions & 0 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createParseErrorDiagnostic } from './providers/diagnostics';
import { onDefinition } from './providers/definition';
import { SymbolTable } from './symbolTable';
import { handleSemanticTokens, tokenTypes } from './providers/semanticTokens';
import { onDocumentSymbol } from './providers/documentSymbols';
import { analyze } from './analyzer';

// Create a connection for the server, using Node's IPC as a transport.
Expand All @@ -30,6 +31,7 @@ connection.onInitialize((params: InitializeParams) => {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
definitionProvider: true,
documentSymbolProvider: true,
semanticTokensProvider: {
legend: {
tokenTypes,
Expand Down Expand Up @@ -59,6 +61,8 @@ function sendDiagnostics(document: TextDocument, parseErrors: ParseError[]) {

connection.onDefinition((params) => onDefinition(params, { documents, documentSymbols }));

connection.onDocumentSymbol((params) => onDocumentSymbol(params, { documents, documentSymbols }));

connection.languages.semanticTokens.on((params) =>
handleSemanticTokens(params, { documents, documentSymbols }),
);
Expand Down
93 changes: 93 additions & 0 deletions server/src/test/documentSymbols.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import * as assert from 'assert';
import { URI } from 'vscode-uri';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { onDocumentSymbol } from '../providers/documentSymbols.js';
import { analyze } from '../analyzer.js';
import { DocumentSymbolParams, SymbolKind } from 'vscode-languageserver/node';

class MockTextDocuments {
private docs = new Map<string, TextDocument>();
constructor(docs: TextDocument[]) {
docs.forEach((doc) => this.docs.set(doc.uri, doc));
}
get(uri: string) {
return this.docs.get(uri);
}
}

suite('Document Symbols Test Suite', () => {
test('Should return symbols for a simple object', () => {
const text = '{"name": "Jason", "age": 30}';
const uri = URI.file('/abs/path/main.jref').toString();
const doc = TextDocument.create(uri, 'jref', 1, text);

const { symbols } = analyze(text);
const context = {
documents: new MockTextDocuments([doc]) as any,
documentSymbols: new WeakMap([[doc, symbols]]),
};

const params: DocumentSymbolParams = {
textDocument: { uri },
};

const result = onDocumentSymbol(params, context);

assert.strictEqual(result.length, 2);
assert.strictEqual(result[0].name, 'name');
assert.strictEqual(result[0].kind, SymbolKind.String);
assert.strictEqual(result[1].name, 'age');
assert.strictEqual(result[1].kind, SymbolKind.Number);
});

test('Should return symbols for nested objects', () => {
const text = '{"person": {"name": "Jason"}}';
const uri = URI.file('/abs/path/main.jref').toString();
const doc = TextDocument.create(uri, 'jref', 1, text);

const { symbols } = analyze(text);
const context = {
documents: new MockTextDocuments([doc]) as any,
documentSymbols: new WeakMap([[doc, symbols]]),
};

const params: DocumentSymbolParams = {
textDocument: { uri },
};

const result = onDocumentSymbol(params, context);

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, 'person');
assert.strictEqual(result[0].kind, SymbolKind.Object);
assert.ok(result[0].children);
assert.strictEqual(result[0].children!.length, 1);
assert.strictEqual(result[0].children![0].name, 'name');
});

test('Should return symbols for arrays', () => {
const text = '{"tags": ["a", "b"]}';
const uri = URI.file('/abs/path/main.jref').toString();
const doc = TextDocument.create(uri, 'jref', 1, text);

const { symbols } = analyze(text);
const context = {
documents: new MockTextDocuments([doc]) as any,
documentSymbols: new WeakMap([[doc, symbols]]),
};

const params: DocumentSymbolParams = {
textDocument: { uri },
};

const result = onDocumentSymbol(params, context);

assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].name, 'tags');
assert.strictEqual(result[0].kind, SymbolKind.Array);
assert.ok(result[0].children);
assert.strictEqual(result[0].children!.length, 2);
assert.strictEqual(result[0].children![0].name, '0');
assert.strictEqual(result[0].children![1].name, '1');
});
});
11 changes: 9 additions & 2 deletions server/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { TextDocuments } from 'vscode-languageserver/node';
import { visit } from './visitor';
import { TextDocuments, Range } from 'vscode-languageserver/node';
import { SymbolTable } from './symbolTable';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { Node } from 'jsonc-parser';

export interface ServerContext {
documents: TextDocuments<TextDocument>;
documentSymbols: WeakMap<TextDocument, SymbolTable>;
}

export function createRange(document: TextDocument, node: Node): Range {
return {
start: document.positionAt(node.offset),
end: document.positionAt(node.offset + node.length),
};
}
5 changes: 5 additions & 0 deletions testfiles/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Jason",
"x": "2",
"y": ["1"]
}