From 768601b86fd4912c7de0713f9c239814d15e9853 Mon Sep 17 00:00:00 2001 From: Devon Carew Date: Sun, 9 Oct 2016 11:56:51 -0700 Subject: [PATCH] implement a type hierarchy command --- package.json | 6 ++ src/commands/type_hierarchy.ts | 131 +++++++++++++++++++++++++++++++++ src/extension.ts | 4 + 3 files changed, 141 insertions(+) create mode 100644 src/commands/type_hierarchy.ts diff --git a/package.json b/package.json index 812c98508c..f1b8545aad 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,12 @@ "key": "ctrl+alt+o", "mac": "cmd+alt+o", "when": "editorLangId == dart" + }, + { + "command": "dart.showTypeHierarchy", + "key": "f4", + "mac": "f4", + "when": "editorLangId == dart" } ], "menus": { diff --git a/src/commands/type_hierarchy.ts b/src/commands/type_hierarchy.ts new file mode 100644 index 0000000000..f9b7bcc569 --- /dev/null +++ b/src/commands/type_hierarchy.ts @@ -0,0 +1,131 @@ +"use strict"; + +import * as editors from "../editors"; +import * as vs from "vscode"; +import * as as from "../analysis/analysis_server_types"; +import { Analyzer } from "../analysis/analyzer"; +import { toRange } from "../utils"; + +export class TypeHierarchyCommand implements vs.Disposable { + private context: vs.ExtensionContext; + private analyzer: Analyzer; + private commands: Array = []; + + constructor(context: vs.ExtensionContext, analyzer: Analyzer) { + this.context = context; + this.analyzer = analyzer; + + this.commands.push( + vs.commands.registerTextEditorCommand("dart.showTypeHierarchy", this.showTypeHierarchy, this) + ); + } + + private showTypeHierarchy(editor: vs.TextEditor, editBuilder: vs.TextEditorEdit) { + if (!editors.hasActiveDartEditor()) { + vs.window.showWarningMessage("No active Dart editor."); + return; + } + + let document = editor.document; + + this.analyzer.searchGetTypeHierarchy({ + file: document.fileName, + offset: document.offsetAt(editor.selection.active) + }).then(response => { + let items = response.hierarchyItems; + if (!items) { + vs.window.showInformationMessage('Type hierarchy not available.'); + return; + } + + let options = { placeHolder: name(items, 0) }; + + // TODO: How / where to show implements? + // TODO: How / where to show mixins? + let tree = []; + let startItem = items[0]; + + tree.push(startItem); + addParents(items, tree, startItem); + addChildren(items, tree, startItem); + + vs.window.showQuickPick(tree.map(item => itemToPick(item, items)), options).then(result => { + if (result) { + let location: as.Location = result['location']; + vs.workspace.openTextDocument(location.file).then(document => { + vs.window.showTextDocument(document).then(editor => { + let range = toRange(location); + editor.revealRange(range, vs.TextEditorRevealType.InCenterIfOutsideViewport); + editor.selection = new vs.Selection(range.end, range.start); + }); + }); + } + }); + }); + } + + dispose(): any { + for (let command of this.commands) + command.dispose(); + } +} + +function addParents(items: as.TypeHierarchyItem[], tree: as.TypeHierarchyItem[], item: as.TypeHierarchyItem) { + if (item.superclass) { + let parent = items[item.superclass]; + tree.unshift(parent); + addParents(items, tree, parent); + } +} + +function addChildren(items: as.TypeHierarchyItem[], tree: as.TypeHierarchyItem[], item: as.TypeHierarchyItem) { + // Handle direct children. + for (let index of item.subclasses) { + let child = items[index]; + tree.push(child); + } + + // Handle grandchildren. + for (let index of item.subclasses) { + let child = items[index]; + if (child.subclasses.length > 0) + addChildren(items, tree, child); + } +} + +function itemToPick(item: as.TypeHierarchyItem, items: as.TypeHierarchyItem[]): vs.QuickPickItem { + let desc = ''; + + // extends + if (item.superclass !== undefined && name(items, item.superclass) != 'Object') + desc += `extends ${name(items, item.superclass)}`; + + // implements + if (item.interfaces.length > 0) { + if (desc.length > 0) + desc += ', '; + desc += `implements ${item.interfaces.map(i => name(items, i)).join(', ')}`; + } + + // with + if (item.mixins.length > 0) { + if (desc.length > 0) + desc += ', '; + desc += `with ${item.mixins.map(i => name(items, i)).join(', ')}`; + } + + // TODO: Show the library location in the details (dart:core, ...) (share code + // with dart_workspace_symbol_provider.ts). + + let result = { + label: item.classElement.name, + description: desc, + location: item.classElement.location + }; + + return result; +} + +function name(items: as.TypeHierarchyItem[], index: number) { + return items[index].classElement.name; +} diff --git a/src/extension.ts b/src/extension.ts index 157b9f589a..12cace8061 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,6 +24,7 @@ import { DartRenameProvider } from "./providers/dart_rename_provider"; import { FileChangeHandler } from "./file_change_handler"; import { OpenFileTracker } from "./open_file_tracker"; import { SdkCommands } from "./commands/sdk"; +import { TypeHierarchyCommand } from "./commands/type_hierarchy"; import { ServerStatusNotification } from "./analysis/analysis_server_types"; const DART_MODE: vs.DocumentFilter = { language: "dart", scheme: "file" }; @@ -132,6 +133,9 @@ export function activate(context: vs.ExtensionContext) { // Set up commands for Dart editors. context.subscriptions.push(new EditCommands(context, analyzer)); + + // Register misc commands. + context.subscriptions.push(new TypeHierarchyCommand(context, analyzer)); } function handleConfigurationChange() {