Skip to content

Commit

Permalink
LSP: Highlight document
Browse files Browse the repository at this point in the history
  • Loading branch information
fjakobs committed Apr 17, 2023
1 parent 3c2aaf5 commit 634a963
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 133 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/create-build-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
env:
RELEASE_TAG: ${{ inputs.release_tag }}

- uses: actions/upload-artifact@v2
- uses: actions/upload-artifact@v3
with:
name: lox-lsp
path: "*.vsix"
2 changes: 1 addition & 1 deletion .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
uses: ./.github/workflows/create-build-artifacts.yml
secrets: inherit
with:
release_tag: release-v${{ needs.prepare-release.outputs.version }}
release_tag: v${{ needs.prepare-release.outputs.version }}

create-release:
if: startsWith(github.event.head_commit.message, 'Release:') || github.event_name == 'workflow_dispatch'
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Features:
- Go to symbol
- Document outline
- Rename symbol
- Highlight document: highlights all references to the symbol scoped to this file

## Why????

Expand Down
100 changes: 100 additions & 0 deletions src/server/LoxDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Diagnostic, DiagnosticSeverity, DocumentSymbol, Position } from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import { ErrorReporter } from "../jslox/Error";
import { Scanner } from "../jslox/Scanner";
import { Parser } from "../jslox/Parser";
import { Resolver } from "../jslox/Resolver";
import { Token } from "../jslox/Token";
import { SemanticToken, SemanticTokenAnalyzer } from "./SemanticTokenAnalyzer";

export class LoxDocument {
public diagnostics: Diagnostic[] = [];
public hadError: boolean = false;
public definitions: Map<Token, Token[]> = new Map();
public references: Map<Token, Token> = new Map();
public semanticTokens: SemanticToken[] = [];
public documentSymbols?: DocumentSymbol[];

constructor(public document: TextDocument) {}

analyze() {
const source = this.document.getText();

this.diagnostics = [];
this.semanticTokens = [];

this.hadError = false;
const reporter: ErrorReporter = {
error: (token: Token, message: string) => {
this.hadError = true;
const diagnostic: Diagnostic = {
severity: DiagnosticSeverity.Error,
range: {
start: this.document.positionAt(token.start),
end: this.document.positionAt(token.end),
},
message: message,
source: "Lox",
};
this.diagnostics.push(diagnostic);
},
warn: (token: Token, message: string) => {
const diagnostic: Diagnostic = {
severity: DiagnosticSeverity.Warning,
range: {
start: this.document.positionAt(token.start),
end: this.document.positionAt(token.end),
},
message: message,
source: "Lox",
};
this.diagnostics.push(diagnostic);
},
runtimeError: function (): void {},
};

const tokens = new Scanner(source, reporter).scanTokens();
const parser = new Parser(tokens, reporter);

const statements = parser.parse();
if (!statements || this.hadError) {
return;
}

const resolver = new Resolver(reporter);
resolver.resolve(statements);

if (statements && !this.hadError) {
const analyzer = new SemanticTokenAnalyzer();
analyzer.analyze(statements, resolver);
this.semanticTokens = analyzer.tokens;
this.documentSymbols = analyzer.documentSymbols;
}

this.definitions = resolver.definitions;
this.references = resolver.references;
}

findAllFromPosition(position: Position): Token[] {
const offset = this.document.offsetAt(position);
let definition: Token | undefined = undefined;
for (const [def, references] of this.definitions) {
if (def.start <= offset && def.end >= offset) {
definition = def;
break;
}

for (const reference of references || []) {
if (reference.start <= offset && reference.end >= offset) {
definition = def;
break;
}
}
}
if (!definition) {
return [];
}

return [definition, ...(this.definitions.get(definition) || [])];
}
}
145 changes: 37 additions & 108 deletions src/server/LoxLspServer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
Definition,
DefinitionLink,
Diagnostic,
DiagnosticSeverity,
DocumentHighlight,
DocumentHighlightKind,
DocumentSymbol,
HandlerResult,
Location,
Expand All @@ -17,81 +17,9 @@ import {
WorkspaceEdit,
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import { ErrorReporter } from "../jslox/Error";
import { Scanner } from "../jslox/Scanner";
import { Parser } from "../jslox/Parser";
import { Resolver } from "../jslox/Resolver";
import { Token } from "../jslox/Token";
import { SemanticToken, SemanticTokenAnalyzer, TOKEN_TO_ID } from "./SemanticTokenAnalyzer";

export class LoxDocument {
public diagnostics: Diagnostic[] = [];
public hadError: boolean = false;
public definitions: Map<Token, Token[]> = new Map();
public references: Map<Token, Token> = new Map();
public semanticTokens: SemanticToken[] = [];
public documentSymbols?: DocumentSymbol[];

constructor(public document: TextDocument) {}

analyze() {
const source = this.document.getText();

this.diagnostics = [];
this.semanticTokens = [];

this.hadError = false;
const reporter: ErrorReporter = {
error: (token: Token, message: string) => {
this.hadError = true;
const diagnostic: Diagnostic = {
severity: DiagnosticSeverity.Error,
range: {
start: this.document.positionAt(token.start),
end: this.document.positionAt(token.end),
},
message: message,
source: "Lox",
};
this.diagnostics.push(diagnostic);
},
warn: (token: Token, message: string) => {
const diagnostic: Diagnostic = {
severity: DiagnosticSeverity.Warning,
range: {
start: this.document.positionAt(token.start),
end: this.document.positionAt(token.end),
},
message: message,
source: "Lox",
};
this.diagnostics.push(diagnostic);
},
runtimeError: function (): void {},
};

const tokens = new Scanner(source, reporter).scanTokens();
const parser = new Parser(tokens, reporter);

const statements = parser.parse();
if (!statements || this.hadError) {
return;
}

const resolver = new Resolver(reporter);
resolver.resolve(statements);

if (statements && !this.hadError) {
const analyzer = new SemanticTokenAnalyzer();
analyzer.analyze(statements, resolver);
this.semanticTokens = analyzer.tokens;
this.documentSymbols = analyzer.documentSymbols;
}

this.definitions = resolver.definitions;
this.references = resolver.references;
}
}
import { TOKEN_TO_ID } from "./SemanticTokenAnalyzer";
import { LoxDocument } from "./LoxDocument";

type LspEventListner = ((type: "diagnostics", params: PublishDiagnosticsParams) => void) &
((type: "foo", params: any) => void);
Expand Down Expand Up @@ -209,36 +137,15 @@ export class LoxLspServer {
return null;
}

const offset = loxDocument.document.offsetAt(position);
let definition: Token | undefined = undefined;
for (const [def, references] of loxDocument.definitions) {
if (def.start <= offset && def.end >= offset) {
definition = def;
break;
}

for (const reference of references || []) {
if (reference.start <= offset && reference.end >= offset) {
definition = def;
break;
}
}
}
if (!definition) {
return null;
}

const edits: Array<TextEdit> = [definition]
.concat(loxDocument.definitions.get(definition) || [])
.map((token) => {
return {
range: {
start: loxDocument.document.positionAt(token.start),
end: loxDocument.document.positionAt(token.end),
},
newText: newName,
};
});
const edits: Array<TextEdit> = loxDocument.findAllFromPosition(position).map((token) => {
return {
range: {
start: loxDocument.document.positionAt(token.start),
end: loxDocument.document.positionAt(token.end),
},
newText: newName,
};
});

return {
changes: {
Expand All @@ -264,7 +171,7 @@ export class LoxLspServer {
if (definition.start <= offset && definition.end >= offset) {
return {
range: {
start: definition,
start: loxDocument.document.positionAt(definition.start),
end: loxDocument.document.positionAt(definition.end),
},
placeholder: definition.lexeme,
Expand All @@ -274,7 +181,7 @@ export class LoxLspServer {
if (reference.start <= offset && reference.end >= offset) {
return {
range: {
start: reference,
start: loxDocument.document.positionAt(reference.start),
end: loxDocument.document.positionAt(reference.end),
},
placeholder: reference.lexeme,
Expand All @@ -284,4 +191,26 @@ export class LoxLspServer {
}
return null;
}

onDocumentHighlight(uri: string, position: Position): HandlerResult<DocumentHighlight[] | null | undefined, void> {
const loxDocument = this.loxDocuments.get(uri);
if (!loxDocument || !loxDocument.definitions) {
return null;
}

const symbols = loxDocument.findAllFromPosition(position);
if (!symbols.length) {
return null;
}

return symbols.map((symbol) => {
return {
range: {
start: loxDocument.document.positionAt(symbol.start),
end: loxDocument.document.positionAt(symbol.end),
},
kind: DocumentHighlightKind.Text,
};
});
}
}
35 changes: 12 additions & 23 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,12 @@ const loxServer = new LoxLspServer(documents, (type, params) => {
}
});

let hasWorkspaceFolderCapability = false;
let hasSemanticTokensCapability = false;
let hasSymbolProviderCapability = false;
let hasRenameCapability = false;

connection.onInitialize((params: InitializeParams) => {
const capabilities = params.capabilities;

hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
hasSemanticTokensCapability = !!capabilities.textDocument?.semanticTokens?.requests?.full;
hasSymbolProviderCapability = !!capabilities.textDocument?.documentSymbol?.hierarchicalDocumentSymbolSupport;
hasRenameCapability = !!capabilities.textDocument?.rename?.prepareSupport;
const hasSemanticTokensCapability = !!capabilities.textDocument?.semanticTokens?.requests?.full;
const hasSymbolProviderCapability = !!capabilities.textDocument?.documentSymbol?.hierarchicalDocumentSymbolSupport;
const hasRenameCapability = !!capabilities.textDocument?.rename?.prepareSupport;

const result: InitializeResult = {
capabilities: {
Expand All @@ -55,6 +49,10 @@ connection.onInitialize((params: InitializeParams) => {
},
};

if (capabilities.textDocument?.documentHighlight) {
result.capabilities.documentHighlightProvider = true;
}

if (hasRenameCapability) {
result.capabilities.renameProvider = {
prepareProvider: true,
Expand All @@ -78,23 +76,10 @@ connection.onInitialize((params: InitializeParams) => {
};
}

if (hasWorkspaceFolderCapability) {
result.capabilities.workspace = {
workspaceFolders: {
supported: true,
},
};
}
return result;
});

connection.onInitialized(() => {
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
connection.console.log("Workspace folder change event received.");
});
}
});
connection.onInitialized(() => {});

connection.onPrepareRename((params: TextDocumentPositionParams) => {
return loxServer.onPrepareRename(params.textDocument.uri, params.position);
Expand All @@ -116,6 +101,10 @@ connection.onDocumentSymbol((params) => {
return loxServer.onDocumentSymbol(params.textDocument.uri);
});

connection.onDocumentHighlight((params) => {
return loxServer.onDocumentHighlight(params.textDocument.uri, params.position);
});

connection.languages.semanticTokens.on((params) => {
return loxServer.onSemanticTokens(params.textDocument.uri);
});
Expand Down

0 comments on commit 634a963

Please sign in to comment.