diff --git a/cmakels/CMakeLists.txt b/cmakels/CMakeLists.txt index ce7003c..7c886ba 100644 --- a/cmakels/CMakeLists.txt +++ b/cmakels/CMakeLists.txt @@ -12,6 +12,9 @@ if(CMAKE_BUILD_TYPE STREQUAL Debug) if((CMAKE_C_COMPILER_ID STREQUAL "GNU") OR (CMAKE_C_COMPILER_ID STREQUAL "Clang")) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address") endif() + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-limit-debug-info") + endif() endif() include(CTest) diff --git a/cmakels/clients/vscode/.vscode/launch.json b/cmakels/clients/vscode/.vscode/launch.json index 6018202..16643a8 100644 --- a/cmakels/clients/vscode/.vscode/launch.json +++ b/cmakels/clients/vscode/.vscode/launch.json @@ -4,7 +4,8 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { "version": "0.2.0", - "configurations": [{ + "configurations": [ + { "name": "Run Extension", "type": "extensionHost", "request": "launch", diff --git a/cmakels/clients/vscode/content/browseTemplate.html b/cmakels/clients/vscode/content/browseTemplate.html new file mode 100644 index 0000000..9f86a3f --- /dev/null +++ b/cmakels/clients/vscode/content/browseTemplate.html @@ -0,0 +1,11 @@ + + + + + + TITLE + + + PLACEHOLDER + + \ No newline at end of file diff --git a/cmakels/clients/vscode/content/icon.svg b/cmakels/clients/vscode/content/icon.svg new file mode 100644 index 0000000..501479e --- /dev/null +++ b/cmakels/clients/vscode/content/icon.svg @@ -0,0 +1,134 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/cmakels/clients/vscode/content/previewTemplate.html b/cmakels/clients/vscode/content/previewTemplate.html new file mode 100644 index 0000000..2dc0c1a --- /dev/null +++ b/cmakels/clients/vscode/content/previewTemplate.html @@ -0,0 +1,43 @@ + + + + + + + Graph preview + + + +
+ % + + + + + + + +
+ + PLACEHOLDER + + diff --git a/cmakels/clients/vscode/content/scaling.js b/cmakels/clients/vscode/content/scaling.js new file mode 100644 index 0000000..c5660f3 --- /dev/null +++ b/cmakels/clients/vscode/content/scaling.js @@ -0,0 +1,148 @@ +var originalWidth = 100; +var originalWidthUnit = "%"; + +var originalHeight = 100; +var originalHeightUnit = "%"; + +var svg = null; + +var scale = 1; +var fitToHeightToggledOn = false; +var fitToWidthToggledOn = false; + +var vscode = null; +try { + vscode = acquireVsCodeApi(); +}catch(error){ + console.error(error); + // swallow, so in the script can be tested in a browser +} + +function initializeScale(initialScale, initialFitToWidthMode, initialFitToHeightMode) { + var svgEls = document.getElementsByTagName("svg"); + if (svgEls.length < 1) { + console.error("Cannot find any 'svg' element in the document."); + return; + } + + svg = svgEls[0]; + var sizePattern = /^([\d.]+)(em|px|%|cm|mm|in|pt|pc)$/g; + + var match = sizePattern.exec(svg.getAttribute("width")); + if (match) { + originalWidth = match[1]; + originalWidthUnit = match[2]; + } + + sizePattern.lastIndex = -1; + var match = sizePattern.exec(svg.getAttribute("height")); + if (match) { + originalHeight = match[1]; + originalHeightUnit = match[2]; + } + + // apply initial values + setScale(initialScale); + if (initialFitToWidthMode) fitToWidth(); + setFitToWidthMode(initialFitToWidthMode); + if (initialFitToHeightMode) fitToHeight(); + setFitToHeightMode(initialFitToHeightMode); + + // update the html, but do not send message to extension + update(false); +} + +function larger() { + untoggleModes(); + scale*=1.5; + update(); +} + +function smaller() { + untoggleModes(); + scale/=1.5; + update(); +} + +function original() { + untoggleModes(); + scale=1; + update(); +} + +/** + * Used when the user sets the scale manually, or the page is re-initialized. + */ +function setScale(value) { + scale = value; + untoggleModes(); + update(); +} + +function fitToWidth() { + redefineAsPx(); + setFitToHeightMode(false); + scale=(window.innerWidth-30)/originalWidth; + update(); +} + +function fitToHeight() { + redefineAsPx(); + setFitToWidthMode(false); + scale=(window.innerHeight-80)/originalHeight; + update(); +} + +const toggledOnStyle = "background-color: black;color: white;"; + +function untoggleModes() { + setFitToWidthMode(false); + setFitToHeightMode(false); +} + +function setFitToHeightMode(value) { + if (fitToHeightToggledOn == value) return; + fitToHeightToggledOn = value; + var newStyle = fitToHeightToggledOn ? toggledOnStyle : ""; + document.getElementById("fitToHeight").setAttribute("style", newStyle); + + // post message to the extension, so the fit-to-height toggle is respected after the webview is updated + postMessage({command: 'fitToHeight', value: fitToHeightToggledOn}); +} + +function setFitToWidthMode(value) { + if (fitToWidthToggledOn == value) return; + fitToWidthToggledOn = value; + var newStyle = fitToWidthToggledOn ? toggledOnStyle : ""; + document.getElementById("fitToWidth").setAttribute("style", newStyle); + + // post message to the extension, so the fit-to-width toggle is respected after the webview is updated + postMessage({command: 'fitToWidth', value: fitToWidthToggledOn}); +} + +function redefineAsPx() { + originalWidthUnit = originalHeightUnit = "px"; +} + +function update(sendMessage=true) { + if (svg) { + svg.setAttribute("style", "width: "+(originalWidth*scale)+originalWidthUnit+ + "; height: "+(originalHeight*scale)+originalHeightUnit); + } + document.getElementById("scalePercent").setAttribute("value", (scale*100).toFixed(0)); + + // post message to the extension, so the scale is respected after the webview is updated + if (sendMessage) postMessage({command: 'scale', value: scale}); +} + +function exportSvg() { + postMessage({command: 'export'}) +} + +function openInBrowser() { + postMessage({command: 'open'}) +} + +function postMessage(message) { + if (vscode) vscode.postMessage(message); +} \ No newline at end of file diff --git a/cmakels/clients/vscode/package-lock.json b/cmakels/clients/vscode/package-lock.json index dc46d85..b1094e0 100644 --- a/cmakels/clients/vscode/package-lock.json +++ b/cmakels/clients/vscode/package-lock.json @@ -1,6 +1,6 @@ { "name": "cmakels-client", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -478,6 +478,11 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -657,6 +662,19 @@ "wrappy": "1" } }, + "opn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-6.0.0.tgz", + "integrity": "sha512-I9PKfIZC+e4RXZ/qr1RhgyCnGgYX0UEIlXgWnCOVACIvFgaC9rz6Won7xbdhoHrd8IIhV7YEpHjreNUNkqCGkQ==", + "requires": { + "is-wsl": "^1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -814,6 +832,14 @@ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -925,6 +951,11 @@ "extsprintf": "^1.2.0" } }, + "viz.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/viz.js/-/viz.js-2.1.2.tgz", + "integrity": "sha512-UO6CPAuEMJ8oNR0gLLNl+wUiIzQUsyUOp8SyyDKTqVRBtq7kk1VnFmIZW8QufjxGrGEuI+LVR7p/C7uEKy0LQw==" + }, "vscode": { "version": "1.1.33", "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.33.tgz", diff --git a/cmakels/clients/vscode/package.json b/cmakels/clients/vscode/package.json index 051a46b..6b3daa1 100644 --- a/cmakels/clients/vscode/package.json +++ b/cmakels/clients/vscode/package.json @@ -53,6 +53,11 @@ "command": "cmakels.restart", "title": "cmakels: Restart Language Server", "description": "Restart the running instance of the language server" + }, + { + "command": "cmakels.target_dependencies", + "title": "cmakels: Get target dependencies", + "description": "Get target dependencies" } ] }, @@ -64,7 +69,8 @@ "test": "npm run compile && node ./node_modules/vscode/bin/test" }, "dependencies": { - "vscode-languageclient": "^4.1.4" + "vscode-languageclient": "^4.1.4", + "viz.js": "^2.0.0" }, "devDependencies": { "typescript": "^3.3.1", @@ -72,5 +78,6 @@ "tslint": "^5.12.1", "@types/node": "^10.12.21", "@types/mocha": "^2.2.42" - } + }, + "extensionDependencies": [] } diff --git a/cmakels/clients/vscode/src/extension.ts b/cmakels/clients/vscode/src/extension.ts index e15d05f..8dc8cb6 100644 --- a/cmakels/clients/vscode/src/extension.ts +++ b/cmakels/clients/vscode/src/extension.ts @@ -1,11 +1,18 @@ -import { workspace, ExtensionContext, commands } from 'vscode'; +import { workspace, ExtensionContext, commands, window, ViewColumn } from 'vscode'; import { LanguageClient, LanguageClientOptions, ServerOptions, + TextDocumentPositionParams, + TextDocumentIdentifier, + RequestType, + Range, + MarkupContent } from 'vscode-languageclient'; +import { GraphvizPreviewGenerator } from './vscode-graphviz/GraphvizPreviewGenerator'; + /** * Method to get workspace configuration option * @param option name of the option (e.g. for cmakels.path should be path) @@ -19,6 +26,88 @@ function getConfig(option: string, defaultValue?: any): T { let client: LanguageClient; + +namespace CustomDependenciesRequest { + export const type = + new RequestType( + 'custom/dependencies'); +} + +type DependencyNodeId = string | number; + +/** + * The result of a dependency request. + */ +interface Dependencies { + /** + * The dependency graph as a list of `DependencyNode`s ... + */ + nodes: DependencyNode[]; + + /** + * ... and a list of edges between nodes. + */ + edges: DependencyEdge[]; + + /** + * An optional root, identifies the root node, if any. + */ + root: DependencyNodeId; + + /** + * An optional range is a range inside a text document + * that is used to visualize a dependency request, e.g. by changing the background color. + */ + range?: Range; +} + +/** + * A node in the dependency graph + */ +interface DependencyNode { + /** + * Identifier of the node + */ + id: DependencyNodeId; + + /** + * The node's content. + */ + content: MarkupContent; +} + +/** + * An edge in the dependency graph + */ +interface DependencyEdge { + /** + * Parent + */ + from: DependencyNodeId; + + /** + * Child + */ + to: DependencyNodeId; + + /** + * Edge kind + */ + kind: number; +} + +/** + * Edge kind. + * TODO: instead of graphical representation, the kind could be a semantic kind. + * But not sure if it could be expressed universal enough. + */ +namespace DependencyEdgeKind { + export const Solid = 1; + export const Dashed = 2; + export const Dotted = 3; + export const Bold = 4; +} + export function activate(context: ExtensionContext) { let executablePath = getConfig('path'); @@ -56,6 +145,49 @@ export function activate(context: ExtensionContext) { client.start(); })); + context.subscriptions.push(commands.registerCommand('cmakels.target_dependencies', async () => { + if(window.activeTextEditor) { + let identifier : TextDocumentIdentifier = + { uri: window.activeTextEditor.document.uri.toString()}; + + let params :TextDocumentPositionParams = { + textDocument: identifier, + position: window.activeTextEditor.selection.active}; + + client.sendRequest(CustomDependenciesRequest.type, params ).then((result) => { + // console.log("yeah: " + result.dependencies[0].id); + const graphvizPreviewGenerator = new GraphvizPreviewGenerator(context); + let dotgraph:string = "digraph graphname {\n"; + dotgraph += "node [color=purple]\n"; + for (let e of result.edges) { + // if(d.children) { + // for(let child of d.children) { + dotgraph += "\"" + e.from + "\"->\"" + e.to + "\" [color=\"blue\" style=\""; + switch(e.kind) { + case DependencyEdgeKind.Bold: + dotgraph += "bold"; + break; + case DependencyEdgeKind.Dashed: + dotgraph += "dashed"; + break; + case DependencyEdgeKind.Solid: + dotgraph += "solid"; + break; + case DependencyEdgeKind.Dotted: + dotgraph += "dotted"; + break; + } + dotgraph +="\"];"; + // } + // } + } + dotgraph += "}"; + graphvizPreviewGenerator.revealOrCreatePreview("my_target", dotgraph, ViewColumn.Beside); + + }); + } + })); + client.start(); } diff --git a/cmakels/clients/vscode/src/vscode-graphviz/ContentUtils.ts b/cmakels/clients/vscode/src/vscode-graphviz/ContentUtils.ts new file mode 100644 index 0000000..8283dc3 --- /dev/null +++ b/cmakels/clients/vscode/src/vscode-graphviz/ContentUtils.ts @@ -0,0 +1,16 @@ +import { ExtensionContext } from "vscode"; +import * as path from "path"; +var fs = require("fs"); + +export const CONTENT_FOLDER = "content"; + +export async function getPreviewTemplate(context: ExtensionContext, templateName: string): Promise { + let previewPath = context.asAbsolutePath(path.join(CONTENT_FOLDER, templateName)); + + return new Promise((resolve, reject) => { + fs.readFile(previewPath, "utf8", function (err:any, data:any) { + if (err) reject(err); + else resolve(data); + }); + }); +} diff --git a/cmakels/clients/vscode/src/vscode-graphviz/GraphvizPreviewGenerator.ts b/cmakels/clients/vscode/src/vscode-graphviz/GraphvizPreviewGenerator.ts new file mode 100644 index 0000000..4cb70da --- /dev/null +++ b/cmakels/clients/vscode/src/vscode-graphviz/GraphvizPreviewGenerator.ts @@ -0,0 +1,192 @@ +import { ExtensionContext, TextDocument, window, ViewColumn, Uri, WebviewPanel, workspace, Disposable } from "vscode"; +const { Module, render } = require('viz.js/full.render.js'); +let Viz = require("viz.js"); +import * as path from "path"; +// import { SvgExporter } from "./SvgExporter"; +// import { OpenInBrowser } from "./OpenInBrowser"; +import { getPreviewTemplate, CONTENT_FOLDER } from "./ContentUtils"; +var fs = require("fs"); + +export class GraphvizPreviewGenerator extends Disposable { + + webviewPanels = new Map(); + + // timeout: NodeJS.Timer; + + constructor(private context: ExtensionContext) { + super(() => this.dispose()); + } + + // setNeedsRebuild(uri: Uri, needsRebuild: boolean): void { + // let panel = this.webviewPanels.get(uri); + + // if (panel) { + // panel.setNeedsRebuild(needsRebuild); + + // this.resetTimeout(); + // } + // } + + // resetTimeout(): void { + // if(this.timeout) { + // clearTimeout(this.timeout); + // } + // this.timeout = setTimeout(() => this.rebuild(), 1000); + // } + + // dispose(): void { + // clearTimeout(this.timeout); + // } + + // rebuild(): void { + // this.webviewPanels.forEach(panel => { + // if(panel.getNeedsRebuild() && panel.getPanel().visible) + // this.updateContent(panel, workspace.textDocuments.find(doc => doc.uri == panel.uri)); + // }); + // } + + async revealOrCreatePreview(id: string, doc: string, displayColumn: ViewColumn): Promise { + let previewPanel = this.webviewPanels.get(id); + + if (previewPanel) { + previewPanel.reveal(displayColumn); + } + else { + previewPanel = this.createPreviewPanel(doc, displayColumn); + this.webviewPanels.set(id, previewPanel); + // when the user closes the tab, remove the panel + previewPanel.getPanel().onDidDispose(() => this.webviewPanels.delete(id), undefined, this.context.subscriptions); + // when the pane becomes visible again, refresh it + // previewPanel.getPanel().onDidChangeViewState(_ => this.rebuild()); + + // previewPanel.getPanel().webview.onDidReceiveMessage(e => this.handleMessage(previewPanel, e), undefined, this.context.subscriptions); + } + + this.updateContent(previewPanel, doc); + } + + handleMessage(previewPanel: PreviewPanel, message: any): void { + console.log(`Message received from the webview: ${message.command}`); + + switch(message.command){ + case 'scale': + previewPanel.setScale(message.value); + break; + case 'fitToHeight': + previewPanel.setFitToHeight(message.value); + break; + case 'fitToWidth': + previewPanel.setFitToWidth(message.value); + break; + // case 'export': + // new SvgExporter().export(previewPanel.uri); + // break; + // case 'open': + // new OpenInBrowser(this.context).open(previewPanel.uri); + // break; + default: + console.warn('Unexpected command: ' + message.command); + } + } + + createPreviewPanel(doc: string, displayColumn: ViewColumn): PreviewPanel { + let previewTitle = 'Preview: '; //${path.basename(window.activeTextEditor.document.fileName)}'`; + + let webViewPanel = window.createWebviewPanel('graphvizPreview', previewTitle, displayColumn, { + enableFindWidget: true, + enableScripts: true, + localResourceRoots: [Uri.file(path.join(this.context.extensionPath, "content"))] + }); + + webViewPanel.iconPath = Uri.file(this.context.asAbsolutePath("content/icon.svg")); + + return new PreviewPanel(doc, webViewPanel); + } + + async updateContent(previewPanel: PreviewPanel, doc: string) { + if(!previewPanel.getPanel().webview.html) { + previewPanel.getPanel().webview.html = "Please wait..."; + } + // previewPanel.setNeedsRebuild(false); + previewPanel.getPanel().webview.html = await this.getPreviewHtml(previewPanel, doc); + } + + toSvg(doc: string): Thenable | string { + return new Viz({Module, render}).renderString(doc); + } + + private async getPreviewHtml(previewPanel: PreviewPanel, doc: string): Promise { + let templateHtml = await getPreviewTemplate(this.context, "previewTemplate.html"); + + // change resource URLs to vscode-resource: + templateHtml = templateHtml.replace(/