diff options
| -rw-r--r-- | editors/vscode/package.json | 31 | ||||
| -rw-r--r-- | editors/vscode/src/ctx.ts | 9 | ||||
| -rw-r--r-- | editors/vscode/src/extension.ts | 7 | ||||
| -rw-r--r-- | editors/vscode/src/inlay_hints.ts | 244 | ||||
| -rw-r--r-- | src/analysis/analysis.odin | 29 | ||||
| -rw-r--r-- | src/server/inlay_hints.odin | 82 | ||||
| -rw-r--r-- | src/server/requests.odin | 44 | ||||
| -rw-r--r-- | src/server/types.odin | 8 |
8 files changed, 445 insertions, 9 deletions
diff --git a/editors/vscode/package.json b/editors/vscode/package.json index cf2db7c..678f058 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -34,6 +34,11 @@ "command": "ols.restart", "title": "Restart Odin Language Server", "category": "Odin Language Server" + }, + { + "command": "ols.createOls", + "title": "Create ols.json file in project", + "category": "Odin Language Server" } ], "configuration": { @@ -118,7 +123,27 @@ "scopeName": "source.odin", "path": "./syntaxes/odin.tmLanguage.json" } - ] + ], + "colors": [ + { + "id": "odin.inlayHints.foreground", + "description": "Foreground color of inlay hints", + "defaults": { + "dark": "#A0A0A0F0", + "light": "#747474", + "highContrast": "#BEBEBE" + } + }, + { + "id": "odin.inlayHints.background", + "description": "Background color of inlay hints", + "defaults": { + "dark": "#11223300", + "light": "#11223300", + "highContrast": "#11223300" + } + } + ] }, "scripts": { "vscode:prepublish": "npm run compile", @@ -130,10 +155,10 @@ }, "devDependencies": { "@types/glob": "^7.1.3", - "@types/mocha": "^8.2.2", + "@types/mocha": "^9.0.0", "@types/node": "^14.14.43", "@types/node-fetch": "^2.5.7", - "@types/vscode": "^1.55.0", + "@types/vscode": "^1.60.0", "@typescript-eslint/eslint-plugin": "^4.22.1", "@typescript-eslint/parser": "^4.22.1", "eslint": "^7.25.0", diff --git a/editors/vscode/src/ctx.ts b/editors/vscode/src/ctx.ts index 7abf9c4..a1bf5d4 100644 --- a/editors/vscode/src/ctx.ts +++ b/editors/vscode/src/ctx.ts @@ -4,6 +4,8 @@ import * as lc from 'vscode-languageclient/node'; import { Config } from './config'; import { isOdinEditor, OdinEditor } from './util'; +import * as inlayHints from './inlay_hints'; + //modified from https://github.com/rust-analyzer/rust-analyzer/blob/master/editors/code/src/ctx.ts - 09.05.2021 export class Ctx { @@ -24,6 +26,9 @@ export class Ctx { cwd: string, ): Promise<Ctx> { const res = new Ctx(config, extCtx, client, serverPath); + + inlayHints.activate(res); + return res; } @@ -53,6 +58,10 @@ export class Ctx { return this.extCtx.subscriptions; } + isOdinDocument(document: vscode.TextDocument): number { + return vscode.languages.match({scheme: 'file', language: 'odin'}, document); + } + pushCleanup(d: Disposable) { this.extCtx.subscriptions.push(d); } diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index e7163c2..69dcd28 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -139,6 +139,10 @@ export async function activate(context: vscode.ExtensionContext) { client.start(); }); + vscode.commands.registerCommand("ols.createOls", async() => { + createOlsConfig(ctx); + }); + client.start(); parseOlsFile(config, olsFile); @@ -183,7 +187,8 @@ export function createOlsConfig(ctx: Ctx) { collections: [{ name: "core", path: corePath }], enable_document_symbols: true, enable_semantic_tokens: false, - enable_hover: true + enable_hover: true, + enable_snippets: true }; const olsPath = vscode.workspace.workspaceFolders![0].uri.fsPath; diff --git a/editors/vscode/src/inlay_hints.ts b/editors/vscode/src/inlay_hints.ts new file mode 100644 index 0000000..441cd0c --- /dev/null +++ b/editors/vscode/src/inlay_hints.ts @@ -0,0 +1,244 @@ +// modification of https://github.com/clangd/vscode-clangd/blob/master/src/inlay-hints.ts + +import * as vscode from 'vscode'; +import * as vscodelc from 'vscode-languageclient/node'; + +import { Ctx } from './ctx'; + +export function activate(context: Ctx) { + const feature = new InlayHintsFeature(context); + context.client.registerFeature(feature); +} + +// Currently, only one hint kind (parameter hints) are supported, +// but others (e.g. type hints) may be added in the future. +enum InlayHintKind { + Parameter = 'parameter', + Type = 'type' +} + +interface InlayHint { + range: vscodelc.Range; + kind: InlayHintKind | string; + label: string; +} + +interface InlayHintsParams { + textDocument: vscodelc.TextDocumentIdentifier; +} + +namespace InlayHintsRequest { + export const type = + new vscodelc.RequestType<InlayHintsParams, InlayHint[], void>( + 'odin/inlayHints'); +} + +interface InlayDecorations { + // Hints are grouped based on their InlayHintKind, because different kinds + // require different decoration types. + // A future iteration of the API may have free-form hint kinds, and instead + // specify style-related information (e.g. before vs. after) explicitly. + // With such an API, we could group hints based on unique presentation styles + // instead. + parameterHints: vscode.DecorationOptions[]; + typeHints: vscode.DecorationOptions[]; +} + +interface HintStyle { + decorationType: vscode.TextEditorDecorationType; + + toDecoration(hint: InlayHint, + conv: vscodelc.Protocol2CodeConverter): vscode.DecorationOptions; +} + +const parameterHintStyle = createHintStyle('before'); +const typeHintStyle = createHintStyle('after'); + +function createHintStyle(position: 'before' | 'after'): HintStyle { + const fg = new vscode.ThemeColor('odin.inlayHints.foreground'); + const bg = new vscode.ThemeColor('odin.inlayHints.background'); + return { + decorationType: vscode.window.createTextEditorDecorationType({ + [position]: { + color: fg, + backgroundColor: bg, + fontStyle: 'normal', + fontWeight: 'normal', + textDecoration: ';font-size:smaller' + } + }), + toDecoration(hint: InlayHint, conv: vscodelc.Protocol2CodeConverter): + vscode.DecorationOptions { + return { + range: conv.asRange(hint.range), + renderOptions: { [position]: { contentText: hint.label } } + }; + } + }; +} + +interface FileEntry { + document: vscode.TextDocument; + + // Last applied decorations. + cachedDecorations: InlayDecorations | null; + + // Source of the token to cancel in-flight inlay hints request if any. + inlaysRequest: vscode.CancellationTokenSource | null; +} + +class InlayHintsFeature implements vscodelc.StaticFeature { + private enabled = false; + private sourceFiles = new Map<string, FileEntry>(); // keys are URIs + private readonly disposables: vscode.Disposable[] = []; + + constructor(private readonly context: Ctx) { } + + fillClientCapabilities(_capabilities: vscodelc.ClientCapabilities) { } + fillInitializeParams(_params: vscodelc.InitializeParams) { } + + initialize(capabilities: vscodelc.ServerCapabilities, + _documentSelector: vscodelc.DocumentSelector | undefined) { + const serverCapabilities: vscodelc.ServerCapabilities & + { inlayHintsProvider?: boolean } = capabilities; + if (serverCapabilities.inlayHintsProvider) { + this.enabled = true; + this.startShowingHints(); + } + } + + onDidChangeVisibleTextEditors() { + if (!this.enabled) { + return; + } + + + const newSourceFiles = new Map<string, FileEntry>(); + + // Rerender all, even up-to-date editors for simplicity + this.context.visibleOdinEditors.forEach(async editor => { + const uri = editor.document.uri.toString(); + const file = this.sourceFiles.get(uri) ?? { + document: editor.document, + cachedDecorations: null, + inlaysRequest: null + }; + newSourceFiles.set(uri, file); + + // No text documents changed, so we may try to use the cache + if (!file.cachedDecorations) { + const hints = await this.fetchHints(file); + if (!hints) { + return; + } + + file.cachedDecorations = this.hintsToDecorations(hints); + } + + this.renderDecorations(editor, file.cachedDecorations); + }); + + // Cancel requests for no longer visible (disposed) source files + this.sourceFiles.forEach((file, uri) => { + if (!newSourceFiles.has(uri)) { + file.inlaysRequest?.cancel(); + } + }); + + this.sourceFiles = newSourceFiles; + } + + onDidChangeTextDocument({ contentChanges, + document }: vscode.TextDocumentChangeEvent) { + if (!this.enabled || contentChanges.length === 0 || + !this.context.isOdinDocument(document)) { + return; + } + + this.syncCacheAndRenderHints(); + } + + dispose() { this.stopShowingHints(); } + + private startShowingHints() { + vscode.window.onDidChangeVisibleTextEditors( + this.onDidChangeVisibleTextEditors, this, this.disposables); + vscode.workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, + this.disposables); + + // Set up initial cache shape + this.context.visibleOdinEditors.forEach( + editor => this.sourceFiles.set(editor.document.uri.toString(), { + document: editor.document, + inlaysRequest: null, + cachedDecorations: null + })); + + this.syncCacheAndRenderHints(); + } + + private stopShowingHints() { + this.sourceFiles.forEach(file => file.inlaysRequest?.cancel()); + this.context.visibleOdinEditors.forEach( + editor => this.renderDecorations(editor, + { parameterHints: [], typeHints: [] })); + this.disposables.forEach(d => d.dispose()); + } + + private renderDecorations(editor: vscode.TextEditor, + decorations: InlayDecorations) { + editor.setDecorations(parameterHintStyle.decorationType, + decorations.parameterHints); + editor.setDecorations(typeHintStyle.decorationType, decorations.typeHints); + } + + private syncCacheAndRenderHints() { + this.sourceFiles.forEach( + (file, uri) => this.fetchHints(file).then(hints => { + if (!hints) { + return; + } + + file.cachedDecorations = this.hintsToDecorations(hints); + + for (const editor of this.context.visibleOdinEditors) { + if (editor.document.uri.toString() == uri) { + this.renderDecorations(editor, file.cachedDecorations); + } + } + })); + } + + private hintsToDecorations(hints: InlayHint[]): InlayDecorations { + const decorations: InlayDecorations = { parameterHints: [], typeHints: [] }; + const conv = this.context.client.protocol2CodeConverter; + for (const hint of hints) { + switch (hint.kind) { + case InlayHintKind.Parameter: { + decorations.parameterHints.push( + parameterHintStyle.toDecoration(hint, conv)); + continue; + } + case InlayHintKind.Type: { + decorations.typeHints.push(typeHintStyle.toDecoration(hint, conv)); + continue; + } + // Don't handle unknown hint kinds because we don't know how to style + // them. This may change in a future version of the protocol. + } + } + return decorations; + } + + private async fetchHints(file: FileEntry): Promise<InlayHint[] | null> { + file.inlaysRequest?.cancel(); + + const tokenSource = new vscode.CancellationTokenSource(); + file.inlaysRequest = tokenSource; + + const request = { textDocument: { uri: file.document.uri.toString() } }; + + return this.context.client.sendRequest(InlayHintsRequest.type, request, + tokenSource.token); + } +}
\ No newline at end of file diff --git a/src/analysis/analysis.odin b/src/analysis/analysis.odin index 6fa8d67..c16bdf9 100644 --- a/src/analysis/analysis.odin +++ b/src/analysis/analysis.odin @@ -2044,7 +2044,7 @@ get_locals_for_range_stmt :: proc(file: ast.File, stmt: ast.Range_Stmt, ast_cont if stmt.expr == nil { return; } - + if symbol, ok := resolve_type_expression(ast_context, stmt.expr); ok { #partial switch v in symbol.value { case index.SymbolMapValue: @@ -2237,6 +2237,33 @@ clear_locals :: proc(ast_context: ^AstContext) { clear(&ast_context.usings); } +resolve_entire_file :: proc(document: ^common.Document, allocator := context.allocator) -> []^index.Symbol { + ast_context := make_ast_context(document.ast, document.imports, document.package_name, document.uri.uri); + + get_globals(document.ast, &ast_context); + + ast_context.current_package = ast_context.document_package; + + symbols := make([]^index.Symbol, allocator); + + for decl in document.ast.decls { + switch v in decl.derived { + case ast.Proc_Lit: + resolve_entire_procedure(v.type, &symbols, allocator); + } + } +} + +resolve_entire_procedure :: proc(procedure: ^ast.Proc_Type, symbols: ^[]^index.Symbol, allocator := context.allocator) { + if procedure == nil { + return {}; + } + + get_locals() + + +} + concatenate_symbols_information :: proc(ast_context: ^AstContext, symbol: index.Symbol, is_completion: bool) -> string { pkg := path.base(symbol.pkg, false, context.temp_allocator); diff --git a/src/server/inlay_hints.odin b/src/server/inlay_hints.odin new file mode 100644 index 0000000..7d96c84 --- /dev/null +++ b/src/server/inlay_hints.odin @@ -0,0 +1,82 @@ +package server + +import "core:odin/ast" +import "core:fmt" + +import "shared:common" +import "shared:analysis" +import "shared:index" + +//document +get_inlay_hints :: proc(document: ^common.Document) -> ([]InlayHint, bool) { + + using analysis; + + hints := make([dynamic]InlayHint, context.temp_allocator); + + ast_context := make_ast_context(document.ast, document.imports, document.package_name, document.uri.uri); + + Visit_Data :: struct { + calls: [dynamic]ast.Call_Expr, + } + + data := Visit_Data { + calls = make([dynamic]ast.Call_Expr, context.temp_allocator), + }; + + visit :: proc(visitor: ^ast.Visitor, node: ^ast.Node) -> ^ast.Visitor { + if node == nil || visitor == nil { + return nil; + } + + data := cast(^Visit_Data)visitor.data; + + if call, ok := node.derived.(ast.Call_Expr); ok { + append(&data.calls, call); + } + + return visitor; + } + + visitor := ast.Visitor { + data = &data, + visit = visit, + } + + for decl in document.ast.decls { + ast.walk(&visitor, decl); + } + + loop: for call in &data.calls { + symbol_arg_count := 0 + for arg in call.args { + if _, ok := arg.derived.(ast.Field); ok { + continue loop; + } + } + + if symbol, ok := resolve_type_expression(&ast_context, &call); ok { + if symbol_call, ok := symbol.value.(index.SymbolProcedureValue); ok { + for arg in symbol_call.arg_types { + for name in arg.names { + if symbol_arg_count >= len(call.args) { + continue loop; + } + + if ident, ok := name.derived.(ast.Ident); ok { + hint := InlayHint { + kind = "parameter", + label = fmt.tprintf("%v:", ident.name), + range = common.get_token_range(call.args[symbol_arg_count], string(document.text)), + } + append(&hints, hint); + } + symbol_arg_count += 1; + } + } + } + } + } + + return hints[:], true; +}
\ No newline at end of file diff --git a/src/server/requests.odin b/src/server/requests.odin index f4dc267..8fc0fa1 100644 --- a/src/server/requests.odin +++ b/src/server/requests.odin @@ -43,6 +43,7 @@ RequestType :: enum { FormatDocument, Hover, CancelRequest, + InlayHint, } RequestInfo :: struct { @@ -186,6 +187,7 @@ request_map: map[string]RequestType = { "textDocument/hover" = .Hover, "$/cancelRequest" = .CancelRequest, "textDocument/formatting" = .FormatDocument, + "odin/inlayHints" = .InlayHint, }; handle_error :: proc (err: common.Error, id: RequestId, writer: ^Writer) { @@ -282,6 +284,8 @@ handle_request :: proc (request: json.Value, config: ^common.Config, writer: ^Wr case .CancelRequest: case .FormatDocument: task_proc = request_format_document; + case .InlayHint: + task_proc = request_inlay_hint; } task := common.Task { @@ -298,9 +302,9 @@ handle_request :: proc (request: json.Value, config: ^common.Config, writer: ^Wr break; } } - case .Initialize,.Initialized: + case .Initialize, .Initialized: task_proc(&task); - case .Completion,.Definition,.Hover,.FormatDocument: + case .Completion, .Definition, .Hover, .FormatDocument: uri := root["params"].(json.Object)["textDocument"].(json.Object)["uri"].(json.String); @@ -315,7 +319,7 @@ handle_request :: proc (request: json.Value, config: ^common.Config, writer: ^Wr task_proc(&task); - case .DidClose,.DidChange,.DidOpen,.DidSave: + case .DidClose, .DidChange, .DidOpen, .DidSave: uri := root["params"].(json.Object)["textDocument"].(json.Object)["uri"].(json.String); @@ -335,7 +339,7 @@ handle_request :: proc (request: json.Value, config: ^common.Config, writer: ^Wr document_release(document); case .Shutdown,.Exit: task_proc(&task); - case .SignatureHelp,.SemanticTokensFull,.SemanticTokensRange,.DocumentSymbol: + case .SignatureHelp, .SemanticTokensFull, .SemanticTokensRange, .DocumentSymbol, .InlayHint: uri := root["params"].(json.Object)["textDocument"].(json.Object)["uri"].(json.String); @@ -541,6 +545,7 @@ request_initialize :: proc (task: ^common.Task) { tokenModifiers = token_modifiers, }, }, + inlayHintsProvider = true, documentSymbolProvider = config.enable_document_symbols, hoverProvider = config.enable_hover, documentFormattingProvider = config.enable_format, @@ -1108,3 +1113,34 @@ request_hover :: proc (task: ^common.Task) { send_response(response, writer); } + +request_inlay_hint :: proc (task: ^common.Task) { + info := get_request_info(task); + + using info; + + defer { + document_release(document); + json.destroy_value(root); + free(info); + } + + params_object, ok := params.(json.Object); + + if !ok { + handle_error(.ParseError, id, writer); + return; + } + + hints: []InlayHint; + hints, ok = get_inlay_hints(document); + + if !ok { + handle_error(.InternalError, id, writer); + return; + } + + response := make_response_message(params = hints, id = id); + + send_response(response, writer); +}
\ No newline at end of file diff --git a/src/server/types.odin b/src/server/types.odin index 9f3c866..d369b33 100644 --- a/src/server/types.odin +++ b/src/server/types.odin @@ -26,6 +26,7 @@ ResponseParams :: union { SemanticTokens, Hover, []TextEdit, + []InlayHint, } ResponseMessage :: struct { @@ -91,6 +92,7 @@ ServerCapabilities :: struct { documentSymbolProvider: bool, hoverProvider: bool, documentFormattingProvider: bool, + inlayHintsProvider: bool, } CompletionOptions :: struct { @@ -357,4 +359,10 @@ Command :: struct { title: string, command: string, arguments: []string, +} + +InlayHint :: struct { + range: common.Range, + kind: string, + label: string, }
\ No newline at end of file |