Skip to content

Commit 6ef8f53

Browse files
committed
fix #138
1 parent a3646bb commit 6ef8f53

13 files changed

Lines changed: 614 additions & 33 deletions

File tree

package.json

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@
4949
"onCommand:jupyter.runSelectionLine",
5050
"onCommand:jupyter.execCurrentCell",
5151
"onCommand:jupyter.execCurrentCellAndAdvance",
52-
"onCommand:python.displayHelp"
52+
"onCommand:python.displayHelp",
53+
"onCommand:python.buildWorkspaceSymbols"
5354
],
5455
"main": "./out/client/extension",
5556
"contributes": {
@@ -95,6 +96,11 @@
9596
"title": "Sort Imports",
9697
"category": "Python Refactor"
9798
},
99+
{
100+
"command": "python.buildWorkspaceSymbols",
101+
"title": "Build Workspace Symbols",
102+
"category": "Python"
103+
},
98104
{
99105
"command": "python.runtests",
100106
"title": "Run All Unit Tests",
@@ -798,6 +804,39 @@
798804
"default": false,
799805
"description": "Automatically add brackets for functions."
800806
},
807+
"python.workspaceSymbols.tagFilePath": {
808+
"type": "string",
809+
"default": "${workspaceRoot}/tags",
810+
"description": "Fully qualified path to tag file (exuberant ctag file), used to provide workspace symbols."
811+
},
812+
"python.workspaceSymbols.enabled": {
813+
"type": "boolean",
814+
"default": true,
815+
"description": "Set to 'false' to disable Workspace Symbol provider using ctags."
816+
},
817+
"python.workspaceSymbols.rebuildOnStart": {
818+
"type": "boolean",
819+
"default": true,
820+
"description": "Whether to re-build the tags file on start (deaults to true)."
821+
},
822+
"python.workspaceSymbols.rebuildOnFileSave": {
823+
"type": "boolean",
824+
"default": true,
825+
"description": "Whether to re-build the tags file on when changes made to python files are saved."
826+
},
827+
"python.workspaceSymbols.ctagsPath": {
828+
"type": "string",
829+
"default": "ctags",
830+
"description": "Fully qualilified path to the ctags executable (else leave as ctags, assuming it is in current path)."
831+
},
832+
"python.workspaceSymbols.exclusionPatterns": {
833+
"type": "array",
834+
"default": [],
835+
"items": {
836+
"type": "string"
837+
},
838+
"description": "Pattern used to exclude files and folders from ctags (see http://ctags.sourceforge.net/ctags.html)."
839+
},
801840
"python.unitTest.promptToConfigure": {
802841
"type": "boolean",
803842
"default": true,
@@ -930,6 +969,7 @@
930969
"copy-paste": "^1.3.0",
931970
"diff-match-patch": "^1.0.0",
932971
"fs-extra": "^0.30.0",
972+
"fuzzy": "^0.1.3",
933973
"line-by-line": "^0.1.5",
934974
"named-js-regexp": "^1.3.1",
935975
"node-static": "^0.7.9",

resources/ctagOptions

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--recurse=yes
2+
--tag-relative=yes
3+
--exclude=.git
4+
--exclude=log
5+
--exclude=tmp
6+
--exclude=doc
7+
--exclude=deps
8+
--exclude=node_modules
9+
--exclude=.vscode
10+
--exclude=public/assets
11+
--exclude=*.git*
12+
--exclude=*.pyc
13+
--exclude=*.pyo
14+
--exclude=.DS_Store
15+
--exclude=**/*.jar
16+
--exclude=**/*.class
17+
--exclude=**/.idea/
18+
--exclude=**/site-packages/**
19+
--exclude=build
20+
--exclude=Builds
21+
--exclude=doc
22+
--fields=Knz
23+
--extra=+f
24+
--append=no

src/client/common/configSettings.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface IPythonSettings {
1616
terminal: ITerminalSettings;
1717
jupyter: JupyterSettings;
1818
sortImports: ISortImportSettings;
19+
workspaceSymbols: IWorkspaceSymbolSettings;
1920
}
2021

2122
export interface ISortImportSettings {
@@ -84,6 +85,14 @@ export interface IAutoCompeteSettings {
8485
addBrackets: boolean;
8586
extraPaths: string[];
8687
}
88+
export interface IWorkspaceSymbolSettings {
89+
enabled: boolean;
90+
tagFilePath: string;
91+
rebuildOnStart: boolean;
92+
rebuildOnFileSave: boolean;
93+
ctagsPath: string;
94+
exclusionPatterns: string[];
95+
}
8796
export interface ITerminalSettings {
8897
executeInFileDir: boolean;
8998
launchArgs: string[];
@@ -200,6 +209,23 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
200209
addBrackets: false
201210
};
202211

212+
let workspaceSymbolsSettings = systemVariables.resolveAny(pythonSettings.get<IWorkspaceSymbolSettings>('workspaceSymbols'));
213+
if (this.workspaceSymbols) {
214+
Object.assign<IWorkspaceSymbolSettings, IWorkspaceSymbolSettings>(this.workspaceSymbols, workspaceSymbolsSettings);
215+
}
216+
else {
217+
this.workspaceSymbols = workspaceSymbolsSettings;
218+
}
219+
// Support for travis
220+
this.workspaceSymbols = this.workspaceSymbols ? this.workspaceSymbols : {
221+
ctagsPath: 'ctags',
222+
enabled: true,
223+
exclusionPatterns: [],
224+
rebuildOnFileSave: true,
225+
rebuildOnStart: true,
226+
tagFilePath: path.join(vscode.workspace.rootPath, "tags")
227+
};
228+
203229
let unitTestSettings = systemVariables.resolveAny(pythonSettings.get<IUnitTestSettings>('unitTest'));
204230
if (this.unitTest) {
205231
Object.assign<IUnitTestSettings, IUnitTestSettings>(this.unitTest, unitTestSettings);
@@ -262,6 +288,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
262288
public terminal: ITerminalSettings;
263289
public jupyter: JupyterSettings;
264290
public sortImports: ISortImportSettings;
291+
public workspaceSymbols: IWorkspaceSymbolSettings;
265292
}
266293

267294
function getAbsolutePath(pathToCheck: string, rootDir: string): string {

src/client/common/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export namespace Commands {
2121
export const Tests_Select_And_Debug_Method = 'python.selectAndDebugTestMethod';
2222
export const Refactor_Extract_Variable = 'python.refactorExtractVariable';
2323
export const Refaactor_Extract_Method = 'python.refactorExtractMethod';
24+
export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols';
2425

2526
export namespace Jupyter {
2627
export const Get_All_KernelSpecs_For_Language = 'jupyter:getAllKernelSpecsForLanguage';
@@ -87,4 +88,8 @@ export namespace Documentation {
8788
export namespace Formatting {
8889
export const FormatOnSave = '/docs/formatting/';
8990
}
91+
export namespace Workspace {
92+
export const Home = '/docs/workspaceSymbols/';
93+
export const InstallOnWindows = '/docs/workspaceSymbols/#Install-Windows';
94+
}
9095
}

src/client/common/installer.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import * as vscode from 'vscode';
22
import * as settings from './configSettings';
33
import { createDeferred, isNotInstalledError } from './helpers';
44
import { execPythonFile } from './utils';
5+
import * as os from 'os';
6+
import { Documentation } from './constants';
57

68
export enum Product {
79
pytest,
@@ -15,22 +17,34 @@ export enum Product {
1517
yapf,
1618
autopep8,
1719
mypy,
18-
unittest
20+
unittest,
21+
ctags
1922
}
2023

2124
const ProductInstallScripts = new Map<Product, string[]>();
22-
ProductInstallScripts.set(Product.autopep8, ['pip', 'install', 'autopep8']);
23-
ProductInstallScripts.set(Product.flake8, ['pip', 'install', 'flake8']);
24-
ProductInstallScripts.set(Product.mypy, ['pip', 'install', 'mypy-lang']);
25-
ProductInstallScripts.set(Product.nosetest, ['pip', 'install', 'nose']);
26-
ProductInstallScripts.set(Product.pep8, ['pip', 'install', 'pep8']);
27-
ProductInstallScripts.set(Product.pylama, ['pip', 'install', 'pylama']);
28-
ProductInstallScripts.set(Product.prospector, ['pip', 'install', 'prospector']);
29-
ProductInstallScripts.set(Product.pydocstyle, ['pip', 'install', 'pydocstyle']);
30-
ProductInstallScripts.set(Product.pylint, ['pip', 'install', 'pylint']);
31-
ProductInstallScripts.set(Product.pytest, ['pip', 'install', '-U', 'pytest']);
32-
ProductInstallScripts.set(Product.yapf, ['pip', 'install', 'yapf']);
33-
25+
ProductInstallScripts.set(Product.autopep8, ['-m', 'pip', 'install', 'autopep8']);
26+
ProductInstallScripts.set(Product.flake8, ['-m', 'pip', 'install', 'flake8']);
27+
ProductInstallScripts.set(Product.mypy, ['-m', 'pip', 'install', 'mypy-lang']);
28+
ProductInstallScripts.set(Product.nosetest, ['-m', 'pip', 'install', 'nose']);
29+
ProductInstallScripts.set(Product.pep8, ['-m', 'pip', 'install', 'pep8']);
30+
ProductInstallScripts.set(Product.pylama, ['-m', 'pip', 'install', 'pylama']);
31+
ProductInstallScripts.set(Product.prospector, ['-m', 'pip', 'install', 'prospector']);
32+
ProductInstallScripts.set(Product.pydocstyle, ['-m', 'pip', 'install', 'pydocstyle']);
33+
ProductInstallScripts.set(Product.pylint, ['-m', 'pip', 'install', 'pylint']);
34+
ProductInstallScripts.set(Product.pytest, ['-m', 'pip', 'install', '-U', 'pytest']);
35+
ProductInstallScripts.set(Product.yapf, ['-m', 'pip', 'install', 'yapf']);
36+
switch (os.platform()) {
37+
case 'win32': {
38+
// Nothing
39+
break;
40+
}
41+
case 'darwin': {
42+
ProductInstallScripts.set(Product.ctags, ['brew install ctags']);
43+
}
44+
default: {
45+
ProductInstallScripts.set(Product.ctags, ['sudo apt-get install exuberant-ctags']);
46+
}
47+
}
3448

3549
const Linters: Product[] = [Product.flake8, Product.pep8, Product.pylama, Product.prospector, Product.pylint, Product.mypy, Product.pydocstyle];
3650
const Formatters: Product[] = [Product.autopep8, Product.yapf];
@@ -122,25 +136,31 @@ export class Installer {
122136
Installer.terminal = vscode.window.createTerminal('Python Installer');
123137
}
124138

139+
if (product === Product.ctags && os.platform() === 'win32') {
140+
vscode.commands.executeCommand('python.displayHelp', Documentation.Workspace.InstallOnWindows);
141+
return Promise.resolve();
142+
}
143+
125144
let installArgs = ProductInstallScripts.get(product);
126145
const pythonPath = settings.PythonSettings.getInstance().pythonPath;
127146

128-
if (this.outputChannel) {
147+
if (this.outputChannel && installArgs[0] === '-m') {
129148
// Errors are just displayed to the user
130149
this.outputChannel.show();
131-
return execPythonFile(pythonPath, ['-m', ...installArgs], vscode.workspace.rootPath, true, (data) => {
150+
return execPythonFile(pythonPath, installArgs, vscode.workspace.rootPath, true, (data) => {
132151
this.outputChannel.append(data);
133152
});
134153
}
135154
else {
136155
let installScript = installArgs.join(' ');
137-
if (pythonPath.indexOf(' ') >= 0) {
138-
installScript = `"${pythonPath}" -m ${installScript}`;
139-
}
140-
else {
141-
installScript = `${pythonPath} -m ${installScript}`;
156+
if (installArgs[0] === '-m') {
157+
if (pythonPath.indexOf(' ') >= 0) {
158+
installScript = `"${pythonPath}" -m ${installScript}`;
159+
}
160+
else {
161+
installScript = `${pythonPath} -m ${installScript}`;
162+
}
142163
}
143-
144164
Installer.terminal.sendText(installScript);
145165
Installer.terminal.show(false);
146166
// Unfortunately we won't know when the command has completed

src/client/common/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export function validatePath(filePath: string): Promise<string> {
2929
});
3030
});
3131
}
32+
export function fsExistsAsync(filePath: string): Promise<boolean> {
33+
return new Promise<boolean>(resolve => {
34+
fs.exists(filePath, exists => {
35+
PathValidity.set(filePath, exists);
36+
return resolve(exists);
37+
});
38+
});
39+
}
3240

3341
let pythonInterpretterDirectory: string = null;
3442
let previouslyIdentifiedPythonPath: string = null;

src/client/extension.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import { activateExecInTerminalProvider } from './providers/execInTerminalProvid
2020
import * as tests from './unittests/main';
2121
import * as jup from './jupyter/main';
2222
import { HelpProvider } from './helpProvider';
23-
import {activateFormatOnSaveProvider} from './providers/formatOnSaveProvider';
23+
import { activateFormatOnSaveProvider } from './providers/formatOnSaveProvider';
24+
import { WorkspaceSymbols } from './workspaceSymbols/main';
2425

2526
const PYTHON: vscode.DocumentFilter = { language: 'python', scheme: 'file' };
2627
let unitTestOutChannel: vscode.OutputChannel;
@@ -86,20 +87,12 @@ export function activate(context: vscode.ExtensionContext) {
8687
context.subscriptions.push(new LintProvider(context, lintingOutChannel, vscode.workspace.rootPath, documentHasJupyterCodeCells));
8788
tests.activate(context, unitTestOutChannel);
8889

89-
// Possible this extension loads before the others, so lets wait for 5 seconds
90-
setTimeout(disableOtherDocumentSymbolsProvider, 5000);
91-
90+
context.subscriptions.push(new WorkspaceSymbols(lintingOutChannel));
91+
9292
const hepProvider = new HelpProvider();
9393
context.subscriptions.push(hepProvider);
9494
}
9595

96-
function disableOtherDocumentSymbolsProvider() {
97-
const symbolsExt = vscode.extensions.getExtension('donjayamanne.language-symbols');
98-
if (symbolsExt && symbolsExt.isActive) {
99-
symbolsExt.exports.disableDocumentSymbolProvider(PYTHON);
100-
}
101-
}
102-
10396
// this method is called when your extension is deactivated
10497
export function deactivate() {
10598
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { SymbolKind, Position } from 'vscode';
2+
3+
export interface Tag {
4+
fileName: string;
5+
symbolName: string;
6+
symbolKind: SymbolKind;
7+
position: Position;
8+
code: string;
9+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as vscode from 'vscode';
2+
3+
export class StatusBar implements vscode.Disposable {
4+
private disposables: vscode.Disposable[];
5+
private statusBar: vscode.StatusBarItem;
6+
7+
constructor() {
8+
this.disposables = [];
9+
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
10+
this.disposables.push(this.statusBar);
11+
this.statusBar.hide();
12+
vscode.workspace.onDidChangeConfiguration(this.onConfigurationChanged.bind(this));
13+
}
14+
15+
private onConfigurationChanged() {
16+
this.displayStandardMessage();
17+
}
18+
19+
private displayStandardMessage() {
20+
this.clearProgressTicker();
21+
this.statusBar.hide();
22+
23+
this.statusBar.text = '$(sync) Symbols';
24+
this.statusBar.tooltip = 'Python Workspace Symbol Provider';
25+
this.statusBar.command = 'symbols.showOptions';
26+
this.statusBar.show();
27+
}
28+
29+
dispose() {
30+
this.disposables.forEach(d => d.dispose());
31+
}
32+
33+
private progressCounter = 0;
34+
private ticker = ['|', '/', '-', '|', '/', '-', '\\'];
35+
private progressTimeout;
36+
private progressPrefix: string;
37+
38+
public displayProgress(promise: Promise<any>, message: string, tooltip?: string, command?: string) {
39+
this.progressPrefix = this.statusBar.text = message;
40+
this.statusBar.command = command ? command : '';
41+
this.statusBar.tooltip = tooltip ? tooltip : '';
42+
this.statusBar.show();
43+
this.clearProgressTicker();
44+
this.progressTimeout = setInterval(() => this.updateProgressTicker(), 150);
45+
46+
promise.then(value => {
47+
this.displayStandardMessage();
48+
return value;
49+
}).catch(reason => {
50+
this.displayStandardMessage();
51+
return Promise.reject(reason);
52+
});
53+
}
54+
private updateProgressTicker() {
55+
let text = `${this.progressPrefix} ${this.ticker[this.progressCounter % 7]}`;
56+
this.progressCounter += 1;
57+
this.statusBar.text = text;
58+
}
59+
private clearProgressTicker() {
60+
if (this.progressTimeout) {
61+
clearInterval(this.progressTimeout);
62+
}
63+
this.progressTimeout = null;
64+
this.progressCounter = 0;
65+
}
66+
}

0 commit comments

Comments
 (0)