From 53bebc89f2523a56f28fc8aecea2a903beb27b5e Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Mon, 8 Aug 2016 10:48:38 -0400 Subject: [PATCH] Initial commit -- very basic blame support --- .gitignore | 2 ++ .vscode/launch.json | 28 ++++++++++++++++++ .vscode/settings.json | 12 ++++++++ .vscode/tasks.json | 30 +++++++++++++++++++ .vscodeignore | 9 ++++++ LICENSE | 22 ++++++++++++++ README.md | 65 ++++++++++++++++++++++++++++++++++++++++ package.json | 34 +++++++++++++++++++++ src/codeLensProvider.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++ src/extension.ts | 21 +++++++++++++ src/git.ts | 68 ++++++++++++++++++++++++++++++++++++++++++ test/extension.test.ts | 22 ++++++++++++++ test/index.ts | 22 ++++++++++++++ tsconfig.json | 14 +++++++++ typings/node.d.ts | 1 + typings/vscode-typings.d.ts | 1 + vsc-extension-quickstart.md | 33 +++++++++++++++++++++ 17 files changed, 456 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 .vscodeignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 package.json create mode 100644 src/codeLensProvider.ts create mode 100644 src/extension.ts create mode 100644 src/git.ts create mode 100644 test/extension.test.ts create mode 100644 test/index.ts create mode 100644 tsconfig.json create mode 100644 typings/node.d.ts create mode 100644 typings/vscode-typings.d.ts create mode 100644 vsc-extension-quickstart.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e5962e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out +node_modules \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c77b2ad --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "${workspaceRoot}/out/src", + "preLaunchTask": "npm" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], + "stopOnEntry": false, + "sourceMaps": true, + "outDir": "${workspaceRoot}/out/test", + "preLaunchTask": "npm" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..97e5ddb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false, // set this to true to hide the "out" folder with the compiled JS files + "node_modules": false + }, + "search.exclude": { + "out": true, // set this to false to include "out" folder in search results + "node_modules": true + }, + "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..fb7f662 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,30 @@ +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +// A task runner that calls a custom npm script that compiles the extension. +{ + "version": "0.1.0", + + // we want to run npm + "command": "npm", + + // the command is a shell script + "isShellCommand": true, + + // show the output window only if unrecognized errors occur. + "showOutput": "silent", + + // we run the custom script "compile" as defined in package.json + "args": ["run", "compile", "--loglevel", "silent"], + + // The tsc compiler is started in watching mode + "isWatching": true, + + // use the standard tsc in watch mode problem matcher to find compile problems in the output. + "problemMatcher": "$tsc-watch" +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..93e28ff --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +typings/** +out/test/** +test/** +src/** +**/*.map +.gitignore +tsconfig.json +vsc-extension-quickstart.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b829b07 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Eric Amodio + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..53f8fd5 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# git-codelens README + +This is the README for your extension "git-codelens". After writing up a brief description, we recommend including the following sections. + +## Features + +Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. + +For example if there is an image subfolder under your extension project workspace: + +\!\[feature X\]\(images/feature-x.png\) + +> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. + +## Requirements + +If you have any requirements or dependencies, add a section describing those and how to install and configure them. + +## Extension Settings + +Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. + +For example: + +This extension contributes the following settings: + +* `myExtension.enable`: enable/disable this extension +* `myExtension.thing`: set to `blah` to do something + +## Known Issues + +Calling out known issues can help limit users opening duplicate issues against your extension. + +## Release Notes + +Users appreciate release notes as you update your extension. + +### 1.0.0 + +Initial release of ... + +### 1.0.1 + +Fixed issue #. + +### 1.1.0 + +Added features X, Y, and Z. + +----------------------------------------------------------------------------------------------------------- + +## Working with Markdown + +**Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: + +* Split the editor (`Cmd+\` on OSX or `Ctrl+\` on Windows and Linux) +* Toggle preview (`Shift+CMD+V` on OSX or `Shift+Ctrl+V` on Windows and Linux) +* Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (OSX) to see a list of Markdown snippets + +### For more information + +* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) +* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) + +**Enjoy!** \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..32c72b8 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "git-codelens", + "displayName": "Git CodeLens", + "description": "Provides Git blame information in CodeLens", + "version": "0.0.1", + "author": "Eric Amodio", + "publisher": "eamodio", + "engines": { + "vscode": "^1.3.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "keywords": [ + "git", "gitblame", "blame" + ], + "main": "./out/src/extension", + "contributes": { + }, + "scripts": { + "vscode:prepublish": "node ./node_modules/vscode/bin/compile", + "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install && tsc" + }, + "dependencies": { + }, + "devDependencies": { + "typescript": "^1.8.10", + "vscode": "^0.11.15" + } +} \ No newline at end of file diff --git a/src/codeLensProvider.ts b/src/codeLensProvider.ts new file mode 100644 index 0000000..20a60a4 --- /dev/null +++ b/src/codeLensProvider.ts @@ -0,0 +1,72 @@ +'use strict'; +import {CancellationToken, CodeLens, CodeLensProvider, commands, Range, SymbolInformation, SymbolKind, TextDocument, Uri} from 'vscode'; +import {IBlameLine, gitBlame} from './git'; +import * as moment from 'moment'; + +export class GitCodeLens extends CodeLens { + constructor(public blame: Promise, public fileName: string, public blameRange: Range, range: Range) { + super(range); + + this.blame = blame; + this.fileName = fileName; + this.blameRange = blameRange; + } +} + +export default class GitCodeLensProvider implements CodeLensProvider { + constructor(public repoPath: string) { } + + provideCodeLenses(document: TextDocument, token: CancellationToken): CodeLens[] | Thenable { + // TODO: Should I wait here? + let blame = gitBlame(document.fileName); + + return (commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri) as Promise).then(symbols => { + let lenses: CodeLens[] = []; + symbols.forEach(sym => this._provideCodeLens(document, sym, blame, lenses)); + return lenses; + }); + } + + private _provideCodeLens(document: TextDocument, symbol: SymbolInformation, blame: Promise, lenses: CodeLens[]): void { + switch (symbol.kind) { + case SymbolKind.Module: + case SymbolKind.Class: + case SymbolKind.Interface: + case SymbolKind.Method: + case SymbolKind.Function: + case SymbolKind.Constructor: + case SymbolKind.Field: + case SymbolKind.Property: + break; + default: + return; + } + + var line = document.lineAt(symbol.location.range.start); + // if (line.text.includes(symbol.name)) { + // } + + let lens = new GitCodeLens(blame, document.fileName, symbol.location.range, line.range); + lenses.push(lens); + } + + resolveCodeLens(codeLens: CodeLens, token: CancellationToken): Thenable { + if (codeLens instanceof GitCodeLens) { + return codeLens.blame.then(allLines => { + let lines = allLines.slice(codeLens.blameRange.start.line, codeLens.blameRange.end.line + 1); + let line = lines[0]; + if (lines.length > 1) { + let sorted = lines.sort((a, b) => b.date.getTime() - a.date.getTime()); + line = sorted[0]; + } + + codeLens.command = { + title: `${line.author}, ${moment(line.date).fromNow()}`, + command: 'git.viewFileHistory', + arguments: [Uri.file(codeLens.fileName)] + }; + return codeLens; + });//.catch(ex => Promise.reject(ex)); // TODO: Figure out a better way to stop the codelens from appearing + } + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..ade6af3 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,21 @@ +'use strict'; +import {DocumentSelector, ExtensionContext, languages, workspace} from 'vscode'; +import GitCodeLensProvider from './codeLensProvider'; +import {gitRepoPath} from './git' + +// this method is called when your extension is activated +export function activate(context: ExtensionContext) { + // Workspace not using a folder. No access to git repo. + if (!workspace.rootPath) { + return; + } + + gitRepoPath(workspace.rootPath).then(repoPath => { + let selector: DocumentSelector = { scheme: 'file' }; + context.subscriptions.push(languages.registerCodeLensProvider(selector, new GitCodeLensProvider(repoPath))); + }).catch(reason => console.warn(reason)); +} + +// this method is called when your extension is deactivated +export function deactivate() { +} \ No newline at end of file diff --git a/src/git.ts b/src/git.ts new file mode 100644 index 0000000..8f2f94a --- /dev/null +++ b/src/git.ts @@ -0,0 +1,68 @@ +'use strict'; +import {spawn} from 'child_process'; +import {dirname} from 'path'; + +export declare interface IBlameLine { + line: number; + author: string; + date: Date; + sha: string; + //code: string; +} + +export function gitRepoPath(cwd) { + const mapper = (input, output) => { + output.push(input.toString().replace(/\r?\n|\r/g, '')) + }; + + return new Promise((resolve, reject) => { + gitCommand(cwd, mapper, 'rev-parse', '--show-toplevel') + .then(result => resolve(result[0])) + .catch(reason => reject(reason)); + }); +} + +const blameMatcher = /^(.*)\t\((.*)\t(.*)\t(.*?)\)(.*)$/gm; + +export function gitBlame(fileName: string) { + const mapper = (input, output) => { + let m: Array; + while ((m = blameMatcher.exec(input.toString())) != null) { + output.push({ + line: parseInt(m[4], 10), + author: m[2], + date: new Date(m[3]), + sha: m[1] + //code: m[5] + }); + } + }; + + return gitCommand(dirname(fileName), mapper, 'blame', '-c', '-M', '-w', '--', fileName) as Promise; +} + +function gitCommand(cwd: string, map: (input: Buffer, output: Array) => void, ...args): Promise { + return new Promise((resolve, reject) => { + let spawn = require('child_process').spawn; + let process = spawn('git', args, { cwd: cwd }); + + let output: Array = []; + process.stdout.on('data', data => { + map(data, output); + }); + + let errors: Array = []; + process.stderr.on('data', err => { + errors.push(err.toString()); + }); + + process.on('close', (exitCode, exitSignal) => { + if (exitCode && errors.length) { + reject(errors.toString()); + return; + } + + resolve(output); + }); + }); +} \ No newline at end of file diff --git a/test/extension.test.ts b/test/extension.test.ts new file mode 100644 index 0000000..5c4a4da --- /dev/null +++ b/test/extension.test.ts @@ -0,0 +1,22 @@ +// +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. +// + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +import * as myExtension from '../src/extension'; + +// Defines a Mocha test suite to group tests of similar kind together +suite("Extension Tests", () => { + + // Defines a Mocha unit test + test("Something 1", () => { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +}); \ No newline at end of file diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..50bae45 --- /dev/null +++ b/test/index.ts @@ -0,0 +1,22 @@ +// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +var testRunner = require('vscode/lib/testrunner'); + +// You can directly control Mocha options by uncommenting the following lines +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eea1622 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "outDir": "out", + "noLib": true, + "sourceMap": true, + "rootDir": "." + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} \ No newline at end of file diff --git a/typings/node.d.ts b/typings/node.d.ts new file mode 100644 index 0000000..5ed7730 --- /dev/null +++ b/typings/node.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/typings/vscode-typings.d.ts b/typings/vscode-typings.d.ts new file mode 100644 index 0000000..5590dc8 --- /dev/null +++ b/typings/vscode-typings.d.ts @@ -0,0 +1 @@ +/// diff --git a/vsc-extension-quickstart.md b/vsc-extension-quickstart.md new file mode 100644 index 0000000..6cdea2b --- /dev/null +++ b/vsc-extension-quickstart.md @@ -0,0 +1,33 @@ +# Welcome to your first VS Code Extension + +## What's in the folder +* This folder contains all of the files necessary for your extension +* `package.json` - this is the manifest file in which you declare your extension and command. +The sample plugin registers a command and defines its title and command name. With this information +VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. +* `src/extension.ts` - this is the main file where you will provide the implementation of your command. +The file exports one function, `activate`, which is called the very first time your extension is +activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. +We pass the function containing the implementation of the command as the second parameter to +`registerCommand`. + +## Get up and running straight away +* press `F5` to open a new window with your extension loaded +* run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World` +* set breakpoints in your code inside `src/extension.ts` to debug your extension +* find output from your extension in the debug console + +## Make changes +* you can relaunch the extension from the debug toolbar after changing code in `src/extension.ts` +* you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes + +## Explore the API +* you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts` + +## Run tests +* open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests` +* press `F5` to run the tests in a new window with your extension loaded +* see the output of the test result in the debug console +* make changes to `test/extension.test.ts` or create new test files inside the `test` folder + * by convention, the test runner will only consider files matching the name pattern `**.test.ts` + * you can create folders inside the `test` folder to structure your tests any way you want \ No newline at end of file