diff --git a/.travis.yml b/.travis.yml index 6b16dc7c56b44..391e3cec47554 100644 --- a/.travis.yml +++ b/.travis.yml @@ -106,7 +106,8 @@ jobs: - os: osx env: CXX=c++ before_script: skip - script: travis_retry yarn test:theia ; + script: + - travis_retry yarn test:theia ; - stage: deploy if: NOT type IN (cron, pull_request) os: linux diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f703cdc24779..ae8aa87c7b77e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,14 @@ ## v0.4.0 - [plugin] added `tasks.onDidEndTask` Plug-in API +- [plugin] Introduce `vscode.previeHtml` command support - [cpp] fixed `CPP_CLANGD_COMMAND` and `CPP_CLANGD_ARGS` environment variables - [electron] open markdown links in the OS default browser - [plugin] the "Command" interface has been split into two: "CommandDescription" and "Command". "Command" has been made compatible with the "Command" interface in vscode. This is not a breaking change, currently, but fields in those interfaces have been deprecated and will be removed in the future. +- [plugin] added ability to display webview panel in 'left', 'right' and 'bottom' area +- [plugin] added `tasks.taskExecutions` Plug-in API Breaking changes: - menus aligned with built-in VS Code menus [#4173](https://github.com/theia-ide/theia/pull/4173) diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts index dc0e0e12fc50e..17eb8706afe09 100644 --- a/dev-packages/application-manager/src/generator/frontend-generator.ts +++ b/dev-packages/application-manager/src/generator/frontend-generator.ts @@ -126,6 +126,8 @@ const electron = require('electron'); const { join, resolve } = require('path'); const { isMaster } = require('cluster'); const { fork } = require('child_process'); +const Storage = require('electron-store'); +const electronStore = new Storage(); const { app, shell, BrowserWindow, ipcMain, Menu } = electron; const applicationName = \`${this.pck.props.frontend.config.applicationName}\`; @@ -150,8 +152,26 @@ if (isMaster) { const y = Math.floor(bounds.y + (bounds.height - height) / 2); const x = Math.floor(bounds.x + (bounds.width - width) / 2); + const windowStateName = 'windowstate'; + const windowState = electronStore.get(windowStateName, { + width, height, x, y + }); + + let windowOptions = { + show: false, + title: applicationName, + width: windowState.width, + height: windowState.height, + x: windowState.x, + y: windowState.y + }; + if (windowState.isMaximized) { + windowOptions.isMaximized = true; + } + // Always hide the window, we will show the window when it is ready to be shown in any case. - const newWindow = new BrowserWindow({ width, height, x, y, show: false, title: applicationName }); + const newWindow = new BrowserWindow(windowOptions); + windowOptions.isMaximized && newWindow.maximize(); newWindow.on('ready-to-show', () => newWindow.show()); // Prevent calls to "window.open" from opening an ElectronBrowser window, @@ -161,6 +181,20 @@ if (isMaster) { shell.openExternal(url); }); + const saveState = () => { + const bounds = newWindow.getBounds(); + electronStore.set(windowStateName, { + isMaximized: newWindow.isMaximized(), + width: bounds.width, + height: bounds.height, + x: bounds.x, + y: bounds.y + }) + } + newWindow.on('close', saveState); + newWindow.on('resize', saveState); + newWindow.on('move', saveState); + if (!!theUrl) { newWindow.loadURL(theUrl); } diff --git a/dev-packages/cli/src/check-hoisting.ts b/dev-packages/cli/src/check-hoisting.ts index 16f82197af62f..a08df64fdbcd2 100644 --- a/dev-packages/cli/src/check-hoisting.ts +++ b/dev-packages/cli/src/check-hoisting.ts @@ -34,6 +34,11 @@ interface Diagnostic { type DiagnosticMap = Map; +/** + * Folders to skip inside the `node_modules` when checking the hoisted dependencies. Such as the `.bin` and `.cache` folders. + */ +const toSkip = ['.bin', '.cache']; + function collectIssues(): DiagnosticMap { console.log('🔍 Analyzing hoisted dependencies in the Theia extensions...'); const root = process.cwd(); @@ -45,7 +50,7 @@ function collectIssues(): DiagnosticMap { const extensionPath = path.join(packages, extension); const nodeModulesPath = path.join(extensionPath, 'node_modules'); if (fs.existsSync(nodeModulesPath)) { - for (const dependency of fs.readdirSync(nodeModulesPath).filter(name => name !== '.bin')) { + for (const dependency of fs.readdirSync(nodeModulesPath).filter(name => toSkip.indexOf(name) === -1)) { const dependencyPath = path.join(nodeModulesPath, dependency); const version = versionOf(dependencyPath); let message = `Dependency '${dependency}' ${version ? `[${version}] ` : ''}was not hoisted to the root 'node_modules' folder.`; diff --git a/examples/browser/package.json b/examples/browser/package.json index eebf9f793d6fd..880a35334b1c8 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -61,7 +61,7 @@ "watch": "concurrently -n compile,bundle \"theiaext watch --preserveWatchOutput\" \"theia build --watch --mode development\"", "start": "export THEIA_DEFAULT_PLUGINS=local-dir:../../plugins && theia start", "start:debug": "yarn start --log-level=debug", - "test": "wdio --max-old-space-size=4096 wdio.conf.js", + "test": "wdio wdio.conf.js", "test-non-headless": "wdio wdio-non-headless.conf.js", "coverage:compile": "yarn build --config coverage-webpack.config.js", "coverage:remap": "remap-istanbul -i coverage/coverage.json -o coverage/coverage-final.json --exclude 'frontend/index.js' && rimraf coverage/coverage.json", diff --git a/examples/browser/wdio.base.conf.js b/examples/browser/wdio.base.conf.js index bc1fe71f07801..a21116b3e81e2 100644 --- a/examples/browser/wdio.base.conf.js +++ b/examples/browser/wdio.base.conf.js @@ -261,7 +261,7 @@ function makeConfig(headless) { try { result = browser.execute("return window.__coverage__;"); } catch (error) { - console.error(`Error retreiving the coverage: ${error}`); + console.error(`Error retrieving the coverage: ${error}`); return; } try { diff --git a/packages/core/package.json b/packages/core/package.json index 7a7563e02f219..b52cf80a7473d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,6 +21,7 @@ "ajv": "^6.5.3", "body-parser": "^1.17.2", "electron": "^2.0.14", + "electron-store": "^2.0.0", "es6-promise": "^4.2.4", "express": "^4.16.3", "file-icons-js": "^1.0.3", diff --git a/packages/core/src/browser/preferences/preference-contribution.ts b/packages/core/src/browser/preferences/preference-contribution.ts index 657366103e295..dace15b5127b5 100644 --- a/packages/core/src/browser/preferences/preference-contribution.ts +++ b/packages/core/src/browser/preferences/preference-contribution.ts @@ -74,12 +74,12 @@ export interface PreferenceItem { } export interface PreferenceSchemaProperty extends PreferenceItem { - description: string; + description?: string; scope?: 'application' | 'window' | 'resource' | PreferenceScope; } export interface PreferenceDataProperty extends PreferenceItem { - description: string; + description?: string; scope?: PreferenceScope; } export namespace PreferenceDataProperty { diff --git a/packages/core/src/browser/style/scrollbars.css b/packages/core/src/browser/style/scrollbars.css index 70dd19a062cd1..c256eb4ce264c 100644 --- a/packages/core/src/browser/style/scrollbars.css +++ b/packages/core/src/browser/style/scrollbars.css @@ -138,7 +138,7 @@ /* Make horizontal scrollbar, decorations overview ruler and vertical scrollbar arrows opaque */ .vs .monaco-scrollable-element > .scrollbar, .vs-dark .monaco-scrollable-element > .scrollbar, -.decorationsOverviewRuler, { +.decorationsOverviewRuler { background: var(--theia-scrollbar-rail-color); } @@ -157,10 +157,18 @@ background: var(--theia-scrollbar-active-thumb-color) !important; } +.monaco-scrollable-element > .visible.scrollbar.vertical:hover > .slider { + background: var(--theia-scrollbar-thumb-color) !important; +} + .monaco-scrollable-element > .scrollbar.vertical > .slider { width: var(--theia-scrollbar-width) !important; } +.monaco-scrollable-element > .scrollbar.vertical > .slider.active { + background: var(--theia-scrollbar-active-thumb-color) !important; +} + .monaco-scrollable-element > .scrollbar.horizontal > .slider { height: var(--theia-scrollbar-width) !important; } diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index be8c3e880d525..675c397ad0c4c 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -37,7 +37,7 @@ height: calc(var(--theia-private-horizontal-tab-height) + var(--theia-private-horizontal-tab-scrollbar-rail-height) / 2); min-width: 35px; line-height: var(--theia-private-horizontal-tab-height); - padding: 0px 8px; + padding: 0px 2px 0px 4px; background: var(--theia-layout-color3); } @@ -95,16 +95,19 @@ } .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable > .p-TabBar-tabCloseIcon { - margin-left: 4px; padding-top: 6px; height: 16px; width: 16px; - background-image: var(--theia-icon-close); background-size: 16px; background-position: center; background-repeat: no-repeat; } +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable:hover > .p-TabBar-tabCloseIcon, +.p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-current > .p-TabBar-tabCloseIcon { + background-image: var(--theia-icon-close); +} + .p-TabBar.theia-app-centers .p-TabBar-tab.p-mod-closable.theia-mod-dirty > .p-TabBar-tabCloseIcon { background-size: 10px; background-image: var(--theia-icon-circle); diff --git a/packages/core/src/node/messaging/ipc-protocol.ts b/packages/core/src/node/messaging/ipc-protocol.ts index afa098c0d5d6d..153e88cb65658 100644 --- a/packages/core/src/node/messaging/ipc-protocol.ts +++ b/packages/core/src/node/messaging/ipc-protocol.ts @@ -45,13 +45,22 @@ export function checkParentAlive(): void { export const ipcEntryPoint = process.env[THEIA_ENTRY_POINT]; +const THEIA_ENV_REGEXP_EXCLUSION = new RegExp('THEIA_*'); export function createIpcEnv(options?: { entryPoint?: string env?: NodeJS.ProcessEnv }): NodeJS.ProcessEnv { const op = Object.assign({}, options); const childEnv = Object.assign({}, op.env); + + for (const key of Object.keys(childEnv)) { + if (THEIA_ENV_REGEXP_EXCLUSION.test(key)) { + delete childEnv[key]; + } + } + childEnv[THEIA_PARENT_PID] = String(process.pid); childEnv[THEIA_ENTRY_POINT] = op.entryPoint; + return childEnv; } diff --git a/packages/editor/src/browser/editor-command.ts b/packages/editor/src/browser/editor-command.ts index 18f596b8ba151..6dcf8f865c99e 100644 --- a/packages/editor/src/browser/editor-command.ts +++ b/packages/editor/src/browser/editor-command.ts @@ -84,6 +84,14 @@ export namespace EditorCommands { category: EDITOR_CATEGORY, label: 'Go to Last Edit Location' }; + /** + * Command that clears the editor navigation history. + */ + export const CLEAR_EDITOR_HISTORY: Command = { + id: 'textEditor.commands.clear.history', + category: EDITOR_CATEGORY, + label: 'Clear Editor History' + }; } @injectable() @@ -121,6 +129,7 @@ export class EditorCommandContribution implements CommandContribution { registry.registerCommand(EditorCommands.GO_BACK); registry.registerCommand(EditorCommands.GO_FORWARD); registry.registerCommand(EditorCommands.GO_LAST_EDIT); + registry.registerCommand(EditorCommands.CLEAR_EDITOR_HISTORY); registry.registerCommand(CommonCommands.AUTO_SAVE, { isToggled: () => this.isAutoSaveOn(), diff --git a/packages/editor/src/browser/editor-navigation-contribution.ts b/packages/editor/src/browser/editor-navigation-contribution.ts index 4ba11f2beba1f..be18abc565f3d 100644 --- a/packages/editor/src/browser/editor-navigation-contribution.ts +++ b/packages/editor/src/browser/editor-navigation-contribution.ts @@ -69,6 +69,10 @@ export class EditorNavigationContribution implements Disposable, FrontendApplica execute: () => this.locationStack.reveal(this.locationStack.lastEditLocation()), isEnabled: () => !!this.locationStack.lastEditLocation() }); + this.commandRegistry.registerHandler(EditorCommands.CLEAR_EDITOR_HISTORY.id, { + execute: () => this.locationStack.clearHistory(), + isEnabled: () => this.locationStack.locations().length > 0 + }); } async onStart(): Promise { diff --git a/packages/editor/src/browser/editor-preferences.ts b/packages/editor/src/browser/editor-preferences.ts index efb1fab549720..de3129f79d185 100644 --- a/packages/editor/src/browser/editor-preferences.ts +++ b/packages/editor/src/browser/editor-preferences.ts @@ -23,7 +23,23 @@ import { PreferenceSchema, PreferenceChangeEvent } from '@theia/core/lib/browser/preferences'; -import { isOSX } from '@theia/core/lib/common/os'; +import { isWindows, isOSX } from '@theia/core/lib/common/os'; + +const DEFAULT_WINDOWS_FONT_FAMILY = 'Consolas, \'Courier New\', monospace'; +const DEFAULT_MAC_FONT_FAMILY = 'Menlo, Monaco, \'Courier New\', monospace'; +const DEFAULT_LINUX_FONT_FAMILY = '\'Droid Sans Mono\', \'monospace\', monospace, \'Droid Sans Fallback\''; + +export const EDITOR_FONT_DEFAULTS = { + fontFamily: ( + isOSX ? DEFAULT_MAC_FONT_FAMILY : (isWindows ? DEFAULT_WINDOWS_FONT_FAMILY : DEFAULT_LINUX_FONT_FAMILY) + ), + fontWeight: 'normal', + fontSize: ( + isOSX ? 12 : 14 + ), + lineHeight: 0, + letterSpacing: 0, +}; export const editorPreferenceSchema: PreferenceSchema = { 'type': 'object', @@ -38,9 +54,24 @@ export const editorPreferenceSchema: PreferenceSchema = { }, 'editor.fontSize': { 'type': 'number', - 'default': (isOSX) ? 12 : 14, + 'default': EDITOR_FONT_DEFAULTS.fontSize, 'description': 'Configure the editor font size.' }, + 'editor.fontFamily': { + 'type': 'string', + 'default': EDITOR_FONT_DEFAULTS.fontFamily, + 'description': 'Controls the font family.' + }, + 'editor.lineHeight': { + 'type': 'number', + 'default': EDITOR_FONT_DEFAULTS.lineHeight, + 'description': 'Controls the line height. Use 0 to compute the line height from the font size.' + }, + 'editor.letterSpacing': { + 'type': 'number', + 'default': EDITOR_FONT_DEFAULTS.letterSpacing, + 'description': 'Controls the letter spacing in pixels.' + }, 'editor.lineNumbers': { 'enum': [ 'on', @@ -435,7 +466,7 @@ export const editorPreferenceSchema: PreferenceSchema = { '800', '900' ], - 'default': 'normal', + 'default': EDITOR_FONT_DEFAULTS.fontWeight, 'description': 'Controls the editor\'s font weight.' }, 'diffEditor.renderSideBySide': { @@ -488,6 +519,7 @@ export const editorPreferenceSchema: PreferenceSchema = { export interface EditorConfiguration { 'editor.tabSize': number + 'editor.fontFamily': string 'editor.fontSize': number 'editor.fontWeight'?: 'normal' | 'bold' | 'bolder' | 'lighter' | 'initial' | 'inherit' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' diff --git a/packages/editor/src/browser/navigation/navigation-location-service.ts b/packages/editor/src/browser/navigation/navigation-location-service.ts index 64d322cfefaff..2e2964ca24e68 100644 --- a/packages/editor/src/browser/navigation/navigation-location-service.ts +++ b/packages/editor/src/browser/navigation/navigation-location-service.ts @@ -164,6 +164,15 @@ export class NavigationLocationService { return this._lastEditLocation; } + /** + * Clears the navigation history. + */ + clearHistory(): void { + this.stack = []; + this.pointer = -1; + this._lastEditLocation = undefined; + } + /** * Reveals the location argument. If not given, reveals the `current location`. Does nothing, if the argument is `undefined`. */ diff --git a/packages/file-search/src/browser/quick-file-open.ts b/packages/file-search/src/browser/quick-file-open.ts index 9519b7f1fb033..b75e40a904a22 100644 --- a/packages/file-search/src/browser/quick-file-open.ts +++ b/packages/file-search/src/browser/quick-file-open.ts @@ -154,7 +154,7 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler { for (const location of locations) { const uriString = location.uri.toString(); if (!alreadyCollected.has(uriString) && fuzzy.test(lookFor, uriString)) { - recentlyUsedItems.push(await this.toItem(location.uri, recentlyUsedItems.length === 0 ? 'recently opened' : undefined)); + recentlyUsedItems.push(await this.toItem(location.uri, { groupLabel: recentlyUsedItems.length === 0 ? 'recently opened' : undefined, showBorder: false })); alreadyCollected.add(uriString); } } @@ -164,11 +164,24 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler { const fileSearchResultItems: QuickOpenItem[] = []; for (const fileUri of results) { if (!alreadyCollected.has(fileUri)) { - fileSearchResultItems.push(await this.toItem(fileUri, fileSearchResultItems.length === 0 ? 'file results' : undefined)); + fileSearchResultItems.push(await this.toItem(fileUri)); alreadyCollected.add(fileUri); } } - acceptor([...recentlyUsedItems, ...fileSearchResultItems]); + + // Create a copy of the file search results and sort. + const sortedResults = fileSearchResultItems.slice(); + sortedResults.sort((a, b) => this.compareItems(a, b)); + + // Extract the first element, and re-add it to the array with the group label. + const first = sortedResults[0]; + sortedResults.shift(); + if (first) { + sortedResults.unshift(await this.toItem(first.getUri()!, { groupLabel: 'file results', showBorder: true })); + } + + // Return the recently used items, followed by the search results. + acceptor([...recentlyUsedItems, ...sortedResults]); } }; this.fileSearchService.find(lookFor, { @@ -192,13 +205,100 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler { }; } + /** + * Compare two `QuickOpenItem`. + * + * @param a `QuickOpenItem` for comparison. + * @param b `QuickOpenItem` for comparison. + * @param member the `QuickOpenItem` object member for comparison. + */ + protected compareItems( + a: QuickOpenItem, + b: QuickOpenItem, + member: 'getLabel' | 'getUri' = 'getLabel'): number { + + /** + * Normalize a given string. + * + * @param str the raw string value. + * @returns the normalized string value. + */ + function normalize(str: string) { + return str.trim().toLowerCase(); + } + + // Normalize the user query. + const query: string = normalize(this.currentLookFor); + + /** + * Score a given string. + * + * @param str the string to score on. + * @returns the score. + */ + function score(str: string): number { + const match = fuzzy.match(query, str); + return (match === null) ? 0 : match.score; + } + + // Get the item's member values for comparison. + let itemA = a[member]()!; + let itemB = b[member]()!; + + // If the `URI` is used as a comparison member, perform the necessary string conversions. + if (typeof itemA !== 'string') { + itemA = itemA.path.toString(); + } + if (typeof itemB !== 'string') { + itemB = itemB.path.toString(); + } + + // Normalize the item labels. + itemA = normalize(itemA); + itemB = normalize(itemB); + + // Score the item labels. + const scoreA: number = score(itemA); + const scoreB: number = score(itemB); + + // If both label scores are identical, perform additional computation. + if (scoreA === scoreB) { + + // Favor the label which have the smallest substring index. + const indexA: number = itemA.indexOf(query); + const indexB: number = itemB.indexOf(query); + + if (indexA === indexB) { + + // Favor the result with the shortest label length. + if (itemA.length !== itemB.length) { + return (itemA.length < itemB.length) ? -1 : 1; + } + + // Fallback to the alphabetical order. + const comparison = itemB.localeCompare(itemA); + + // If the alphabetical comparison is equal, call `compareItems` recursively using the `URI` member instead. + if (comparison === 0) { + return this.compareItems(a, b, 'getUri'); + } + + return itemB.localeCompare(itemA); + } + + return indexA - indexB; + } + + return scoreB - scoreA; + } + openFile(uri: URI): void { this.openerService.getOpener(uri) .then(opener => opener.open(uri)) .catch(error => this.messageService.error(error)); } - private async toItem(uriOrString: URI | string, group?: string) { + private async toItem(uriOrString: URI | string, group?: QuickOpenGroupItemOptions) { const uri = uriOrString instanceof URI ? uriOrString : new URI(uriOrString); let description = this.labelProvider.getLongName(uri.parent); if (this.workspaceService.workspace && !this.workspaceService.workspace.isDirectory) { @@ -217,10 +317,7 @@ export class QuickFileOpenService implements QuickOpenModel, QuickOpenHandler { run: this.getRunFunction(uri) }; if (group) { - return new QuickOpenGroupItem({ - ...options, - groupLabel: group - }); + return new QuickOpenGroupItem({ ...options, ...group }); } else { return new QuickOpenItem(options); } diff --git a/packages/git/package.json b/packages/git/package.json index e399d8f575f86..ebf4c57ed94dc 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -13,7 +13,7 @@ "@types/fs-extra": "^4.0.2", "@types/p-queue": "^2.3.1", "diff": "^3.4.0", - "dugite-extra": "0.1.9", + "dugite-extra": "0.1.10", "find-git-exec": "^0.0.1-alpha.2", "find-git-repositories": "^0.1.0", "fs-extra": "^4.0.2", @@ -34,10 +34,6 @@ "backend": "lib/node/env/git-env-module", "backendElectron": "lib/electron-node/env/electron-git-env-module" }, - { - "backend": "lib/node/init/git-init-module", - "backendElectron": "lib/electron-node/init/electron-git-init-module" - }, { "frontend": "lib/browser/prompt/git-prompt-module", "frontendElectron": "lib/electron-browser/prompt/electron-git-prompt-module" diff --git a/packages/git/src/electron-node/init/electron-git-init-module.ts b/packages/git/src/electron-node/init/electron-git-init-module.ts deleted file mode 100644 index 25a2ea79eae93..0000000000000 --- a/packages/git/src/electron-node/init/electron-git-init-module.ts +++ /dev/null @@ -1,24 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { ContainerModule } from 'inversify'; -import { GitInit } from '../../node/init/git-init'; -import { ElectronGitInit } from './electron-git-init'; - -export default new ContainerModule(bind => { - bind(ElectronGitInit).toSelf(); - bind(GitInit).toService(ElectronGitInit); -}); diff --git a/packages/git/src/electron-node/init/electron-git-init.ts b/packages/git/src/electron-node/init/electron-git-init.ts deleted file mode 100644 index 739e02c0961c2..0000000000000 --- a/packages/git/src/electron-node/init/electron-git-init.ts +++ /dev/null @@ -1,67 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { inject, injectable } from 'inversify'; -import findGit from 'find-git-exec'; -import { dirname } from 'path'; -import { pathExists } from 'fs-extra'; -import { ILogger } from '@theia/core/lib/common/logger'; -import { DefaultGitInit } from '../../node/init/git-init'; - -/** - * The Git initializer for electron. If Git can be found on the `PATH`, it will be used instead of the embedded Git shipped with `dugite`. - */ -@injectable() -export class ElectronGitInit extends DefaultGitInit { - - @inject(ILogger) - protected readonly logger: ILogger; - - async init(): Promise { - const { env } = process; - if (typeof env.LOCAL_GIT_DIRECTORY !== 'undefined' || typeof env.GIT_EXEC_PATH !== 'undefined') { - await this.handleExternalNotFound('Cannot use Git from the PATH when the LOCAL_GIT_DIRECTORY or the GIT_EXEC_PATH environment variables are set.'); - } else { - try { - const { execPath, path, version } = await findGit(); - if (!!execPath && !!path && !!version) { - // https://github.com/desktop/dugite/issues/111#issuecomment-323222834 - // Instead of the executable path, we need the root directory of Git. - const dir = dirname(dirname(path)); - const [execPathOk, pathOk, dirOk] = await Promise.all([pathExists(execPath), pathExists(path), pathExists(dir)]); - if (execPathOk && pathOk && dirOk) { - process.env.LOCAL_GIT_DIRECTORY = dir; - process.env.GIT_EXEC_PATH = execPath; - this.logger.info(`Using Git [${version}] from the PATH. (${path})`); - return; - } - } - await this.handleExternalNotFound(); - } catch (err) { - await this.handleExternalNotFound(err); - } - } - } - - // tslint:disable-next-line:no-any - protected async handleExternalNotFound(err?: any): Promise { - if (err) { - this.logger.error(err); - } - this.logger.info('Could not find Git on the PATH. Falling back to the embedded Git.'); - } - -} diff --git a/packages/git/src/node/dugite-git.spec.ts b/packages/git/src/node/dugite-git.spec.ts index 12de38792839c..563a2715b9c8d 100644 --- a/packages/git/src/node/dugite-git.spec.ts +++ b/packages/git/src/node/dugite-git.spec.ts @@ -47,10 +47,10 @@ describe('git', async function () { fs.mkdirSync(path.join(root, 'A')); fs.mkdirSync(path.join(root, 'B')); fs.mkdirSync(path.join(root, 'C')); + const git = await createGit(); await initRepository(path.join(root, 'A')); await initRepository(path.join(root, 'B')); await initRepository(path.join(root, 'C')); - const git = await createGit(); const workspaceRootUri = FileUri.create(root).toString(); const repositories = await git.repositories(workspaceRootUri, { maxCount: 1 }); expect(repositories.length).to.deep.equal(1); @@ -63,10 +63,10 @@ describe('git', async function () { fs.mkdirSync(path.join(root, 'A')); fs.mkdirSync(path.join(root, 'B')); fs.mkdirSync(path.join(root, 'C')); + const git = await createGit(); await initRepository(path.join(root, 'A')); await initRepository(path.join(root, 'B')); await initRepository(path.join(root, 'C')); - const git = await createGit(); const workspaceRootUri = FileUri.create(root).toString(); const repositories = await git.repositories(workspaceRootUri, {}); expect(repositories.map(r => path.basename(FileUri.fsPath(r.localUri))).sort()).to.deep.equal(['A', 'B', 'C']); @@ -80,11 +80,11 @@ describe('git', async function () { fs.mkdirSync(path.join(root, 'BASE', 'A')); fs.mkdirSync(path.join(root, 'BASE', 'B')); fs.mkdirSync(path.join(root, 'BASE', 'C')); + const git = await createGit(); await initRepository(path.join(root, 'BASE')); await initRepository(path.join(root, 'BASE', 'A')); await initRepository(path.join(root, 'BASE', 'B')); await initRepository(path.join(root, 'BASE', 'C')); - const git = await createGit(); const workspaceRootUri = FileUri.create(path.join(root, 'BASE')).toString(); const repositories = await git.repositories(workspaceRootUri, {}); expect(repositories.map(r => path.basename(FileUri.fsPath(r.localUri))).sort()).to.deep.equal(['A', 'B', 'BASE', 'C']); @@ -99,11 +99,11 @@ describe('git', async function () { fs.mkdirSync(path.join(root, 'BASE', 'WS_ROOT', 'A')); fs.mkdirSync(path.join(root, 'BASE', 'WS_ROOT', 'B')); fs.mkdirSync(path.join(root, 'BASE', 'WS_ROOT', 'C')); + const git = await createGit(); await initRepository(path.join(root, 'BASE')); await initRepository(path.join(root, 'BASE', 'WS_ROOT', 'A')); await initRepository(path.join(root, 'BASE', 'WS_ROOT', 'B')); await initRepository(path.join(root, 'BASE', 'WS_ROOT', 'C')); - const git = await createGit(); const workspaceRootUri = FileUri.create(path.join(root, 'BASE', 'WS_ROOT')).toString(); const repositories = await git.repositories(workspaceRootUri, {}); const repositoryNames = repositories.map(r => path.basename(FileUri.fsPath(r.localUri))); @@ -741,8 +741,8 @@ describe('git', async function () { before(async () => { root = track.mkdirSync('ls-files'); localUri = FileUri.create(root).toString(); - await createTestRepository(root); git = await createGit(); + await createTestRepository(root); }); ([ @@ -765,39 +765,17 @@ describe('git', async function () { describe('log', function () { - this.timeout(10000); - - async function testLogFromRepoRoot(testLocalGit: string) { - const savedValue = process.env.USE_LOCAL_GIT; - try { - process.env.USE_LOCAL_GIT = testLocalGit; - const root = await createTestRepository(track.mkdirSync('log-test')); - const localUri = FileUri.create(root).toString(); - const repository = { localUri }; - const git = await createGit(); - const result = await git.log(repository, { uri: localUri }); - expect(result.length === 1).to.be.true; - expect(result[0].author.email === 'jon@doe.com').to.be.true; - } catch (err) { - throw err; - } finally { - process.env.USE_LOCAL_GIT = savedValue; - } - } - - // See https://github.com/theia-ide/theia/issues/2143 - it('should not fail with embedded git when executed from the repository root', async () => { - await testLogFromRepoRoot('false'); - }); - // See https://github.com/theia-ide/theia/issues/2143 - it('should not fail with local git when executed from the repository root', async () => { - await testLogFromRepoRoot('true'); + it('should not fail when executed from the repository root', async () => { + const root = await createTestRepository(track.mkdirSync('log-test')); + const localUri = FileUri.create(root).toString(); + const repository = { localUri }; + const git = await createGit(); + const result = await git.log(repository, { uri: localUri }); + expect(result.length === 1).to.be.true; + expect(result[0].author.email === 'jon@doe.com').to.be.true; }); - // THE ABOVE TEST SHOULD ALWAYS BE THE LAST GIT TEST RUN. - // It changes the underlying git to be the local git, which can't be - // undone. (See https://github.com/theia-ide/theia/issues/2246). }); function toPathSegment(repository: Repository, uri: string): string { diff --git a/packages/git/src/node/git-backend-module.ts b/packages/git/src/node/git-backend-module.ts index 884472de4f178..49ee27e128c52 100644 --- a/packages/git/src/node/git-backend-module.ts +++ b/packages/git/src/node/git-backend-module.ts @@ -29,6 +29,8 @@ import { GitLocatorImpl } from './git-locator/git-locator-impl'; import { GitExecProvider } from './git-exec-provider'; import { GitPromptServer, GitPromptClient, GitPrompt } from '../common/git-prompt'; import { DugiteGitPromptServer } from './dugite-git-prompt'; +import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module'; +import { DefaultGitInit, GitInit } from './init/git-init'; export interface GitBindingOptions { readonly bindManager: (binding: interfaces.BindingToSyntax<{}>) => interfaces.BindingWhenOnSyntax<{}>; @@ -62,15 +64,22 @@ export function bindGit(bind: interfaces.Bind, bindingOptions: GitBindingOptions } else { bind(GitLocator).to(GitLocatorClient); } - bind(DugiteGit).toSelf().inSingletonScope(); bind(OutputParser).toSelf().inSingletonScope(); bind(NameStatusParser).toSelf().inSingletonScope(); bind(CommitDetailsParser).toSelf().inSingletonScope(); bind(GitBlameParser).toSelf().inSingletonScope(); bind(GitExecProvider).toSelf().inSingletonScope(); - bind(Git).toService(DugiteGit); + bind(ConnectionContainerModule).toConstantValue(gitConnectionModule); } +const gitConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => { + bind(DefaultGitInit).toSelf(); + bind(GitInit).toService(DefaultGitInit); + bind(DugiteGit).toSelf().inSingletonScope(); + bind(Git).toService(DugiteGit); + bindBackendService(GitPath, Git); +}); + export function bindRepositoryWatcher(bind: interfaces.Bind): void { bind(DugiteGitWatcherServer).toSelf(); bind(GitWatcherServer).toService(DugiteGitWatcherServer); @@ -83,13 +92,6 @@ export function bindPrompt(bind: interfaces.Bind): void { export default new ContainerModule(bind => { bindGit(bind); - bind(ConnectionHandler).toDynamicValue(context => - new JsonRpcConnectionHandler(GitPath, client => { - const server = context.container.get(Git); - client.onDidCloseConnection(() => server.dispose()); - return server; - }) - ).inSingletonScope(); bindRepositoryWatcher(bind); bind(ConnectionHandler).toDynamicValue(context => diff --git a/packages/git/src/node/init/git-init-module.ts b/packages/git/src/node/init/git-init-module.ts deleted file mode 100644 index c006d53cb6785..0000000000000 --- a/packages/git/src/node/init/git-init-module.ts +++ /dev/null @@ -1,23 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 TypeFox and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ - -import { ContainerModule } from 'inversify'; -import { GitInit, DefaultGitInit } from './git-init'; - -export default new ContainerModule(bind => { - bind(DefaultGitInit).toSelf(); - bind(GitInit).toService(DefaultGitInit); -}); diff --git a/packages/git/src/node/init/git-init.ts b/packages/git/src/node/init/git-init.ts index 89cb9c128a4be..e2774077e874c 100644 --- a/packages/git/src/node/init/git-init.ts +++ b/packages/git/src/node/init/git-init.ts @@ -14,8 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable } from 'inversify'; +import { injectable, inject } from 'inversify'; +import findGit from 'find-git-exec'; +import { dirname } from 'path'; +import { pathExists } from 'fs-extra'; +import { ILogger } from '@theia/core/lib/common/logger'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { MessageService } from '@theia/core'; /** * Initializer hook for Git. @@ -25,23 +30,58 @@ export interface GitInit extends Disposable { /** * Called before `Git` is ready to be used in Theia. Git operations cannot be executed before the returning promise is not resolved or rejected. - * - * This implementation does nothing at all. */ init(): Promise; } /** - * The default initializer. It is used in the browser. Does nothing at all. + * The default initializer. It is used in the browser. + * + * Configures the Git extension to use the Git executable from the `PATH`. */ @injectable() export class DefaultGitInit implements GitInit { protected readonly toDispose = new DisposableCollection(); + @inject(ILogger) + protected readonly logger: ILogger; + + @inject(MessageService) + protected readonly messages: MessageService; + async init(): Promise { - // NOOP + const { env } = process; + try { + const { execPath, path, version } = await findGit(); + if (!!execPath && !!path && !!version) { + // https://github.com/desktop/dugite/issues/111#issuecomment-323222834 + // Instead of the executable path, we need the root directory of Git. + const dir = dirname(dirname(path)); + const [execPathOk, pathOk, dirOk] = await Promise.all([pathExists(execPath), pathExists(path), pathExists(dir)]); + if (execPathOk && pathOk && dirOk) { + if (typeof env.LOCAL_GIT_DIRECTORY !== 'undefined' && env.LOCAL_GIT_DIRECTORY !== dir) { + this.logger.error(`Misconfigured env.LOCAL_GIT_DIRECTORY: ${env.LOCAL_GIT_DIRECTORY}. dir was: ${dir}`); + this.messages.error('The LOCAL_GIT_DIRECTORY env variable was already set to a different value.', { timeout: 0 }); + return; + } + if (typeof env.GIT_EXEC_PATH !== 'undefined' && env.GIT_EXEC_PATH !== execPath) { + this.logger.error(`Misconfigured env.GIT_EXEC_PATH: ${env.GIT_EXEC_PATH}. execPath was: ${execPath}`); + this.messages.error('The GIT_EXEC_PATH env variable was already set to a different value.', { timeout: 0 }); + return; + } + process.env.LOCAL_GIT_DIRECTORY = dir; + process.env.GIT_EXEC_PATH = execPath; + this.logger.info(`Using Git [${version}] from the PATH. (${path})`); + return; + } + } + this.messages.error('Could not find Git on the PATH.', { timeout: 0 }); + } catch (err) { + this.logger.error(err); + this.messages.error('An unexpected error occurred when locating the Git executable.', { timeout: 0 }); + } } dispose(): void { diff --git a/packages/git/src/node/test/binding-helper.ts b/packages/git/src/node/test/binding-helper.ts index d3f04aec066ff..72380a33f0471 100644 --- a/packages/git/src/node/test/binding-helper.ts +++ b/packages/git/src/node/test/binding-helper.ts @@ -22,6 +22,9 @@ import { bindLogger } from '@theia/core/lib/node/logger-backend-module'; import { NoSyncRepositoryManager } from '.././test/no-sync-repository-manager'; import { GitEnvProvider, DefaultGitEnvProvider } from '../env/git-env-provider'; import { GitInit, DefaultGitInit } from '../init/git-init'; +import { MessageService, LogLevel } from '@theia/core/lib/common'; +import { MessageClient } from '@theia/core'; +import { ILogger } from '@theia/core/lib/common/logger'; // tslint:disable-next-line:no-any export function initializeBindings(): { container: Container, bind: interfaces.Bind } { @@ -31,10 +34,17 @@ export function initializeBindings(): { container: Container, bind: interfaces.B bind(GitEnvProvider).toService(DefaultGitEnvProvider); bind(DefaultGitInit).toSelf(); bind(GitInit).toService(DefaultGitInit); + bind(MessageService).toSelf(); + bind(MessageClient).toSelf(); + bind(DugiteGit).toSelf(); + bind(Git).toService(DugiteGit); bindLogger(bind); return { container, bind }; } +/** + * For testing only. + */ export async function createGit(bindingOptions: GitBindingOptions = GitBindingOptions.Default): Promise { const { container, bind } = initializeBindings(); bindGit(bind, { @@ -42,5 +52,8 @@ export async function createGit(bindingOptions: GitBindingOptions = GitBindingOp return binding.to(NoSyncRepositoryManager).inSingletonScope(); } }); - return container.get(DugiteGit); + (container.get(ILogger) as ILogger).setLogLevel(LogLevel.ERROR); + const git = container.get(DugiteGit); + await git.exec({ localUri: '' }, ['--version']); // Enforces eager Git initialization by setting the `LOCAL_GIT_DIRECTORY` and `GIT_EXEC_PATH` env variables. + return git; } diff --git a/packages/languages/src/node/language-server-contribution.ts b/packages/languages/src/node/language-server-contribution.ts index 773ed8ee4fa01..c5a903d47edf3 100644 --- a/packages/languages/src/node/language-server-contribution.ts +++ b/packages/languages/src/node/language-server-contribution.ts @@ -112,7 +112,7 @@ export abstract class BaseLanguageServerContribution implements LanguageServerCo rawProcess.onError((error: ProcessErrorEvent) => { this.onDidFailSpawnProcess(error); if (error.code === 'ENOENT') { - const guess = command.split('\S').shift(); + const guess = command.split(/\s+/).shift(); if (guess) { reject(new Error(`Failed to spawn ${guess}\nPerhaps it is not on the PATH.`)); return; diff --git a/packages/mini-browser/src/browser/location-mapper-service.ts b/packages/mini-browser/src/browser/location-mapper-service.ts index 4e20997b7363e..1a135c80863b0 100644 --- a/packages/mini-browser/src/browser/location-mapper-service.ts +++ b/packages/mini-browser/src/browser/location-mapper-service.ts @@ -105,10 +105,20 @@ export class HttpsLocationMapper implements LocationMapper { } -export class MiniBrowserEndpoint extends Endpoint { - constructor() { - super({ path: 'mini-browser' }); +/** + * Location mapper for locations without a scheme. + */ +@injectable() +export class LocationWithoutSchemeMapper implements LocationMapper { + + canHandle(location: string): MaybePromise { + return new URI(location).scheme === '' ? 1 : 0; + } + + map(location: string): MaybePromise { + return `http://${location}`; } + } /** @@ -134,3 +144,9 @@ export class FileLocationMapper implements LocationMapper { } } + +export class MiniBrowserEndpoint extends Endpoint { + constructor() { + super({ path: 'mini-browser' }); + } +} diff --git a/packages/mini-browser/src/browser/mini-browser-frontend-module.ts b/packages/mini-browser/src/browser/mini-browser-frontend-module.ts index 66fb06c85e321..06ad9e86d7cc2 100644 --- a/packages/mini-browser/src/browser/mini-browser-frontend-module.ts +++ b/packages/mini-browser/src/browser/mini-browser-frontend-module.ts @@ -29,7 +29,14 @@ import { MiniBrowserOpenHandler } from './mini-browser-open-handler'; import { MiniBrowserService, MiniBrowserServicePath } from '../common/mini-browser-service'; import { MiniBrowser, MiniBrowserOptions } from './mini-browser'; import { MiniBrowserProps, MiniBrowserContentFactory, MiniBrowserMouseClickTracker, MiniBrowserContent } from './mini-browser-content'; -import { LocationMapperService, FileLocationMapper, HttpLocationMapper, HttpsLocationMapper, LocationMapper } from './location-mapper-service'; +import { + LocationMapperService, + FileLocationMapper, + HttpLocationMapper, + HttpsLocationMapper, + LocationMapper, + LocationWithoutSchemeMapper, +} from './location-mapper-service'; import '../../src/browser/style/index.css'; @@ -68,6 +75,8 @@ export default new ContainerModule(bind => { bind(LocationMapper).to(FileLocationMapper).inSingletonScope(); bind(LocationMapper).to(HttpLocationMapper).inSingletonScope(); bind(LocationMapper).to(HttpsLocationMapper).inSingletonScope(); + bind(LocationWithoutSchemeMapper).toSelf().inSingletonScope(); + bind(LocationMapper).toService(LocationWithoutSchemeMapper); bind(LocationMapperService).toSelf().inSingletonScope(); bind(MiniBrowserService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, MiniBrowserServicePath)).inSingletonScope(); diff --git a/packages/mini-browser/src/browser/mini-browser-open-handler.ts b/packages/mini-browser/src/browser/mini-browser-open-handler.ts index 0ddadae09b361..70283195b0ba7 100644 --- a/packages/mini-browser/src/browser/mini-browser-open-handler.ts +++ b/packages/mini-browser/src/browser/mini-browser-open-handler.ts @@ -30,6 +30,7 @@ import { FrontendApplicationContribution } from '@theia/core/lib/browser/fronten import { WidgetOpenerOptions } from '@theia/core/lib/browser/widget-open-handler'; import { MiniBrowserService } from '../common/mini-browser-service'; import { MiniBrowser, MiniBrowserProps } from './mini-browser'; +import { LocationMapperService } from './location-mapper-service'; export namespace MiniBrowserCommands { export const PREVIEW: Command = { @@ -57,6 +58,8 @@ export interface MiniBrowserOpenerOptions extends WidgetOpenerOptions, MiniBrows export class MiniBrowserOpenHandler extends NavigatableWidgetOpenHandler implements FrontendApplicationContribution, CommandContribution, MenuContribution, TabBarToolbarContribution { + static PREVIEW_URI = new URI().withScheme('__minibrowser__preview__'); + /** * Instead of going to the backend with each file URI to ask whether it can handle the current file or not, * we have this map of extension and priority pairs that we populate at application startup. @@ -82,6 +85,9 @@ export class MiniBrowserOpenHandler extends NavigatableWidgetOpenHandler (await this.miniBrowserService.supportedFileExtensions()).forEach(entry => { const { extension, priority } = entry; @@ -92,7 +98,7 @@ export class MiniBrowserOpenHandler extends NavigatableWidgetOpenHandler { // Get the default options. let result = await this.defaultOptions(); @@ -253,18 +260,21 @@ export class MiniBrowserOpenHandler extends NavigatableWidgetOpenHandler { - return this.open(MiniBrowserOpenHandler.PREVIEW_URI, this.getOpenPreviewProps(startPage)); + const props = await this.getOpenPreviewProps(await this.locationMapperService.map(startPage)); + return this.open(MiniBrowserOpenHandler.PREVIEW_URI, props); } - protected getOpenPreviewProps(startPage: string): MiniBrowserOpenerOptions { + + protected async getOpenPreviewProps(startPage: string): Promise { + const resetBackground = await this.resetBackground(new URI(startPage)); return { name: 'Preview', startPage, toolbar: 'read-only', widgetOptions: { area: 'right' - } + }, + resetBackground }; } diff --git a/packages/mini-browser/src/browser/style/index.css b/packages/mini-browser/src/browser/style/index.css index b358a0af69921..b9970f307704a 100644 --- a/packages/mini-browser/src/browser/style/index.css +++ b/packages/mini-browser/src/browser/style/index.css @@ -96,7 +96,6 @@ } .theia-mini-browser-content-area { - background-color: white; position: relative; display: flex; height: 100%; diff --git a/packages/monaco/src/browser/monaco-editor-model.ts b/packages/monaco/src/browser/monaco-editor-model.ts index d9fe71152716d..80595b2b082b6 100644 --- a/packages/monaco/src/browser/monaco-editor-model.ts +++ b/packages/monaco/src/browser/monaco-editor-model.ts @@ -39,6 +39,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { autoSave: 'on' | 'off' = 'on'; autoSaveDelay: number = 500; + readonly onWillSaveLoopTimeOut = 1500; protected model: monaco.editor.IModel; protected readonly resolveModel: Promise; @@ -344,7 +345,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument { const timeoutPromise = new Promise((resolve, reject) => timeoutHandle = setTimeout(() => { didTimeout = true; reject(new Error('onWillSave listener loop timeout')); - }, 1000)); + }, this.onWillSaveLoopTimeOut)); const firing = this.onWillSaveModelEmitter.sequence(async listener => { if (shouldStop()) { diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index b9e4b52100943..c9c459996c84b 100644 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -20,6 +20,11 @@ import { CommandService } from '@theia/core/lib/common/command'; import TheiaURI from '@theia/core/lib/common/uri'; import URI from 'vscode-uri'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; +import { DiffService } from '@theia/workspace/lib/browser/diff-service'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { WebviewWidget } from '@theia/plugin-ext/lib/main/browser/webview/webview'; +import { ApplicationShell } from '@theia/core/lib/browser'; +import { ResourceProvider } from '@theia/core'; export namespace VscodeCommands { export const OPEN: Command = { @@ -27,9 +32,18 @@ export namespace VscodeCommands { label: 'VSCode open link' }; + export const DIFF: Command = { + id: 'vscode.diff', + label: 'VSCode diff' + }; + export const SET_CONTEXT: Command = { id: 'setContext' }; + + export const PREVIEW_HTML: Command = { + id: 'vscode.previewHtml' + }; } @injectable() @@ -38,6 +52,14 @@ export class PluginVscodeCommandsContribution implements CommandContribution { protected readonly commandService: CommandService; @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; + @inject(EditorManager) + protected readonly editorManager: EditorManager; + @inject(ApplicationShell) + protected readonly shell: ApplicationShell; + @inject(ResourceProvider) + protected readonly resources: ResourceProvider; + @inject(DiffService) + protected readonly diffService: DiffService; registerCommands(commands: CommandRegistry): void { commands.registerCommand(VscodeCommands.OPEN, { @@ -47,6 +69,15 @@ export class PluginVscodeCommandsContribution implements CommandContribution { } }); + commands.registerCommand(VscodeCommands.DIFF, { + isVisible: () => true, + // tslint:disable-next-line: no-any + execute: async uris => { + const [left, right] = uris; + await this.diffService.openDiffEditor(left, right); + } + }); + commands.registerCommand(VscodeCommands.SET_CONTEXT, { isVisible: () => false, // tslint:disable-next-line: no-any @@ -54,5 +85,31 @@ export class PluginVscodeCommandsContribution implements CommandContribution { this.contextKeyService.createKey(String(contextKey), contextValue); } }); + commands.registerCommand(VscodeCommands.PREVIEW_HTML, { + isVisible: () => true, + // tslint:disable-next-line: no-any + execute: async (resource: URI, position?: any, label?: string, options?: any) => { + label = label || resource.fsPath; + const view = new WebviewWidget(label, { allowScripts: true }, {}); + const res = await this.resources(new TheiaURI(resource)); + const str = await res.readContents(); + const html = this.getHtml(str); + this.shell.addWidget(view, { area: 'main', mode: 'split-right' }); + this.shell.activateWidget(view.id); + view.setHTML(html); + + const editorWidget = await this.editorManager.getOrCreateByUri(new TheiaURI(resource)); + editorWidget.editor.onDocumentContentChanged(listener => { + view.setHTML(this.getHtml(editorWidget.editor.document.getText())); + }); + + } + } + ); } + + private getHtml(body: String) { + return `${body}`; + } + } diff --git a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts index 8dcc36d7b8925..1e017b9fd7416 100644 --- a/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts +++ b/packages/plugin-ext-vscode/src/node/plugin-vscode-init.ts @@ -49,7 +49,7 @@ export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIF // redefine property Object.defineProperty(panel.webview, 'html', { set: function (html: string) { - const newHtml = html.replace('vscode-resource:/', '/webview/'); + const newHtml = html.replace(new RegExp('vscode-resource:/', 'g'), '/webview/'); this.checkIsDisposed(); if (this._html !== newHtml) { this._html = newHtml; diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index f63bb9bc432e4..af739b9979f9c 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -698,6 +698,7 @@ export interface ModelChangedEvent { export interface DocumentsExt { $acceptModelModeChanged(startUrl: UriComponents, oldModeId: string, newModeId: string): void; $acceptModelSaved(strUrl: UriComponents): void; + $acceptModelWillSave(strUrl: UriComponents, reason: theia.TextDocumentSaveReason): Promise; $acceptDirtyStateChanged(strUrl: UriComponents, isDirty: boolean): void; $acceptModelChanged(strUrl: UriComponents, e: ModelChangedEvent, isDirty: boolean): void; } @@ -828,7 +829,7 @@ export interface TaskDto { label: string; source?: string; // tslint:disable-next-line:no-any - properties?: { [key: string]: any }; + [key: string]: any; } export interface TaskExecutionDto { @@ -919,11 +920,6 @@ export interface WebviewPanelViewState { readonly position: number; } -export interface WebviewPanelShowOptions { - readonly viewColumn?: number; - readonly preserveFocus?: boolean; -} - export interface WebviewsExt { $onMessage(handle: string, message: any): void; $onDidChangeWebviewPanelViewState(handle: string, newState: WebviewPanelViewState): void; @@ -940,11 +936,11 @@ export interface WebviewsMain { $createWebviewPanel(handle: string, viewType: string, title: string, - showOptions: WebviewPanelShowOptions, + showOptions: theia.WebviewPanelShowOptions, options: theia.WebviewPanelOptions & theia.WebviewOptions | undefined, pluginLocation: UriComponents): void; $disposeWebview(handle: string): void; - $reveal(handle: string, showOptions: WebviewPanelShowOptions): void; + $reveal(handle: string, showOptions: theia.WebviewPanelShowOptions): void; $setTitle(handle: string, value: string): void; $setIconPath(handle: string, value: { light: string, dark: string } | string | undefined): void; $setHtml(handle: string, value: string): void; @@ -1046,6 +1042,7 @@ export interface TasksExt { export interface TasksMain { $registerTaskProvider(handle: number, type: string): void; + $taskExecutions(): Promise; $unregister(handle: number): void; $terminateTask(id: number): void; } diff --git a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts index ef09d88ab1f03..6a3ad8bbf3994 100644 --- a/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts +++ b/packages/plugin-ext/src/hosted/browser/hosted-plugin.ts @@ -58,9 +58,6 @@ export class HostedPluginSupport { @inject(PluginServer) protected readonly pluginServer: PluginServer; - @inject(WorkspaceService) - protected readonly workspaceService: WorkspaceService; - @inject(PreferenceProviderProvider) protected readonly preferenceProviderProvider: PreferenceProviderProvider; @@ -72,8 +69,9 @@ export class HostedPluginSupport { @inject(PreferenceServiceImpl) private readonly preferenceServiceImpl: PreferenceServiceImpl, @inject(PluginPathsService) private readonly pluginPathsService: PluginPathsService, @inject(StoragePathService) private readonly storagePathService: StoragePathService, + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService, ) { - this.theiaReadyPromise = Promise.all([this.preferenceServiceImpl.ready]); + this.theiaReadyPromise = Promise.all([this.preferenceServiceImpl.ready, this.workspaceService.roots]); this.storagePathService.onStoragePathChanged(path => { this.updateStoragePath(path); @@ -154,6 +152,7 @@ export class HostedPluginSupport { pluginID = getPluginId(plugins[0].model); } const rpc = this.createServerRpc(pluginID, hostKey); + setUpPluginApi(rpc, container); const hostedExtManager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT); hostedExtManager.$init({ plugins: plugins, @@ -163,7 +162,6 @@ export class HostedPluginSupport { env: { queryParams: getQueryParameters() }, extApi: initData.pluginAPIs }, confStorage); - setUpPluginApi(rpc, container); this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, container)); this.backendExtManagerProxy = hostedExtManager; }); diff --git a/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts b/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts index 7dbc3b9a08172..ac5fef9eec337 100644 --- a/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts +++ b/packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts @@ -114,13 +114,16 @@ export class HostedPluginProcess implements ServerPluginRunner { } + readonly HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION = new RegExp('HOSTED_PLUGIN*'); private fork(options: IPCConnectionOptions): cp.ChildProcess { // create env and add PATH to it so any executable from root process is available - const env = createIpcEnv(); - env.PATH = process.env.PATH; - // add HOME to env since some plug-ins need to read files from user's home dir - env.HOME = process.env.HOME; + const env = createIpcEnv({ env: process.env }); + for (const key of Object.keys(env)) { + if (this.HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION.test(key)) { + delete env[key]; + } + } if (this.cli.extensionTestsPath) { env.extensionTestsPath = this.cli.extensionTestsPath; } diff --git a/packages/plugin-ext/src/main/browser/documents-main.ts b/packages/plugin-ext/src/main/browser/documents-main.ts index 5127df4fd464f..26045207e0649 100644 --- a/packages/plugin-ext/src/main/browser/documents-main.ts +++ b/packages/plugin-ext/src/main/browser/documents-main.ts @@ -42,7 +42,7 @@ export class DocumentsMainImpl implements DocumentsMain { modelService: EditorModelService, rpc: RPCProtocol, private editorManger: EditorManager, - private openerService: OpenerService + private openerService: OpenerService, ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DOCUMENTS_EXT); @@ -53,6 +53,18 @@ export class DocumentsMainImpl implements DocumentsMain { this.toDispose.push(modelService.onModelSaved(m => { this.proxy.$acceptModelSaved(m.textEditorModel.uri); })); + this.toDispose.push(modelService.onModelWillSave(onWillSaveModelEvent => { + onWillSaveModelEvent.waitUntil(new Promise(async resolve => { + const edits = await this.proxy.$acceptModelWillSave(onWillSaveModelEvent.model.textEditorModel.uri, onWillSaveModelEvent.reason); + const transformedEdits = edits.map((edit): monaco.editor.IIdentifiedSingleEditOperation => + ({ + range: monaco.Range.lift(edit.range), + text: edit.text!, + forceMoveMarkers: edit.forceMoveMarkers + })); + resolve(transformedEdits); + })); + })); this.toDispose.push(modelService.onModelDirtyChanged(m => { this.proxy.$acceptDirtyStateChanged(m.textEditorModel.uri, m.dirty); })); diff --git a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts index 1639da7024871..4543ea48bc047 100644 --- a/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts @@ -131,16 +131,10 @@ export class PluginContributionHandler { for (const location in contributions.viewsContainers) { if (contributions.viewsContainers!.hasOwnProperty(location)) { const viewContainers = contributions.viewsContainers[location]; - viewContainers.forEach(container => this.viewRegistry.registerViewContainer(location, container)); - } - } - } - - if (contributions.views) { - for (const location in contributions.views) { - if (contributions.views.hasOwnProperty(location)) { - const views = contributions.views[location]; - views.forEach(view => this.viewRegistry.registerView(location, view)); + viewContainers.forEach(container => { + const views = contributions.views && contributions.views[container.id] ? contributions.views[container.id] : []; + this.viewRegistry.registerViewContainer(location, container, views); + }); } } } @@ -185,6 +179,7 @@ export class PluginContributionHandler { } private updateConfigurationSchema(schema: PreferenceSchema): void { + this.validateConfigurationSchema(schema); this.preferenceSchemaProvider.setSchema(schema); } @@ -290,4 +285,43 @@ export class PluginContributionHandler { } return result; } + + protected validateConfigurationSchema(schema: PreferenceSchema): void { + // tslint:disable-next-line:forin + for (const p in schema.properties) { + const property = schema.properties[p]; + if (property.type !== 'object') { + continue; + } + + if (!property.default) { + this.validateDefaultValue(property); + } + + const properties = property['properties']; + if (properties) { + // tslint:disable-next-line:forin + for (const key in properties) { + if (typeof properties[key] !== 'object') { + delete properties[key]; + } + } + } + } + } + + private validateDefaultValue(property: PreferenceSchemaProperties): void { + property.default = {}; + + const properties = property['properties']; + if (properties) { + // tslint:disable-next-line:forin + for (const key in properties) { + if (properties[key].default) { + property.default[key] = properties[key].default; + delete properties[key].default; + } + } + } + } } diff --git a/packages/plugin-ext/src/main/browser/tasks-main.ts b/packages/plugin-ext/src/main/browser/tasks-main.ts index 7bb76e9ebec84..639037d56c5ff 100644 --- a/packages/plugin-ext/src/main/browser/tasks-main.ts +++ b/packages/plugin-ext/src/main/browser/tasks-main.ts @@ -88,6 +88,14 @@ export class TasksMainImpl implements TasksMain { } } + async $taskExecutions() { + const runningTasks = await this.taskService.getRunningTasks(); + return runningTasks.map(taskInfo => ({ + id: taskInfo.taskId, + task: taskInfo.config + })); + } + $terminateTask(id: number): void { this.taskService.kill(id); } diff --git a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts index baf57591ab58f..e14410f513295 100644 --- a/packages/plugin-ext/src/main/browser/text-editor-model-service.ts +++ b/packages/plugin-ext/src/main/browser/text-editor-model-service.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ import { Event, Emitter } from '@theia/core'; -import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { MonacoEditorModel, WillSaveMonacoModelEvent } from '@theia/monaco/lib/browser/monaco-editor-model'; import { injectable, inject } from 'inversify'; import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace'; @@ -25,6 +25,7 @@ export interface EditorModelService { onModelRemoved: Event; onModelModeChanged: Event<{ model: MonacoEditorModel, oldModeId: string }>; + onModelWillSave: Event; onModelDirtyChanged: Event; onModelSaved: Event; @@ -39,11 +40,13 @@ export class EditorModelServiceImpl implements EditorModelService { private onModelRemovedEmitter = new Emitter(); private modelDirtyEmitter = new Emitter(); private modelSavedEmitter = new Emitter(); + private onModelWillSavedEmitter = new Emitter(); onModelDirtyChanged: Event = this.modelDirtyEmitter.event; onModelSaved: Event = this.modelSavedEmitter.event; onModelModeChanged = this.modelModeChangedEmitter.event; onModelRemoved = this.onModelRemovedEmitter.event; + onModelWillSave = this.onModelWillSavedEmitter.event; constructor(@inject(MonacoTextModelService) monacoModelService: MonacoTextModelService, @inject(MonacoWorkspace) monacoWorkspace: MonacoWorkspace) { @@ -70,6 +73,9 @@ export class EditorModelServiceImpl implements EditorModelService { model.onDirtyChanged(_ => { this.modelDirtyEmitter.fire(model); }); + model.onWillSaveModel(willSaveModelEvent => { + this.onModelWillSavedEmitter.fire(willSaveModelEvent); + }); } get onModelAdded(): Event { diff --git a/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx b/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx index a99c0988207d9..8b6d4b43ca894 100644 --- a/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx +++ b/packages/plugin-ext/src/main/browser/view/tree-views-main.tsx @@ -79,7 +79,7 @@ export class TreeViewsMainImpl implements TreeViewsMain { this.treeViewWidgets.set(treeViewId, treeViewWidget); - this.viewRegistry.onRegisterTreeView(treeViewId, treeViewWidget); + this.viewRegistry.registerTreeView(treeViewId, treeViewWidget); this.handleTreeEvents(treeViewId, treeViewWidget); } diff --git a/packages/plugin-ext/src/main/browser/view/view-registry.ts b/packages/plugin-ext/src/main/browser/view/view-registry.ts index ad1abf63b74aa..8821a3750faab 100644 --- a/packages/plugin-ext/src/main/browser/view/view-registry.ts +++ b/packages/plugin-ext/src/main/browser/view/view-registry.ts @@ -17,16 +17,15 @@ import { injectable, inject, postConstruct } from 'inversify'; import { ViewContainer, View } from '../../../common'; import { ApplicationShell } from '@theia/core/lib/browser'; -import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -import { Widget } from '@theia/core/lib/browser/widgets/widget'; +import { + FrontendApplicationState, + FrontendApplicationStateService +} from '@theia/core/lib/browser/frontend-application-state'; import { ViewsContainerWidget } from './views-container-widget'; import { TreeViewWidget } from './tree-views-main'; -export interface ViewContainerRegistry { - container: ViewContainer; - area: ApplicationShell.Area; - views: View[] -} +const READY: FrontendApplicationState = 'ready'; +const DEFAULT_LOCATION: ApplicationShell.Area = 'left'; @injectable() export class ViewRegistry { @@ -37,85 +36,54 @@ export class ViewRegistry { @inject(FrontendApplicationStateService) protected applicationStateService: FrontendApplicationStateService; - private containers: ViewContainerRegistry[] = new Array(); - - private containersWidgets: Map = new Map(); - - private treeViewWidgets: Map = new Map(); + private treeViewWidgets: Map = new Map(); + private containerWidgets: Map = new Map(); + private updateContainerOnApplicationReady: Promise; @postConstruct() init() { - this.applicationStateService.reachedState('ready').then(() => { - this.showContainers(); - this.showTreeViewWidgets(); - }); + this.updateContainerOnApplicationReady = this.applicationStateService.reachedState(READY); } - getArea(location: string): ApplicationShell.Area { - switch (location) { - case 'right': return 'right'; - case 'bottom': return 'bottom'; - case 'top': return 'top'; + registerViewContainer(location: string, viewsContainer: ViewContainer, containerViews: View[]): void { + if (this.containerWidgets.has(viewsContainer.id)) { + return; } + const containerWidget = new ViewsContainerWidget(viewsContainer, containerViews); + this.containerWidgets.set(viewsContainer.id, containerWidget); - return 'left'; - } - - registerViewContainer(location: string, viewContainer: ViewContainer) { - const registry: ViewContainerRegistry = { - container: viewContainer, - area: this.getArea(location), - views: [] - }; - this.containers.push(registry); - } - - registerView(location: string, view: View) { - this.containers.forEach(containerRegistry => { - if (location === containerRegistry.container.id) { - containerRegistry.views.push(view); + // add to the promise chain + this.updateContainerOnApplicationReady = this.updateContainerOnApplicationReady.then(() => { + if (this.applicationShell.getTabBarFor(containerWidget)) { + return; } - }); - } - - private showContainers() { - // Remember the currently active widget - const activeWidget: Widget | undefined = this.applicationShell.activeWidget; - - // Show views containers - this.containers.forEach(registry => { - const widget = new ViewsContainerWidget(registry.container, registry.views); - this.containersWidgets.set(registry.container.id, widget); - - const tabBar = this.applicationShell.getTabBarFor(widget); - if (!tabBar) { - const widgetArgs: ApplicationShell.WidgetOptions = { - area: registry.area - }; + this.applicationShell.addWidget(containerWidget, { + area: ApplicationShell.isSideArea(location) ? location : DEFAULT_LOCATION + }); - this.applicationShell.addWidget(widget, widgetArgs); - } + // update container + this.treeViewWidgets.forEach((treeViewWidget: TreeViewWidget, viewId: string) => { + this.addTreeViewWidget(viewsContainer.id, viewId, treeViewWidget); + }); }); - - // Restore active widget - if (activeWidget) { - this.applicationShell.activateWidget(activeWidget.id); - } } - onRegisterTreeView(treeViewid: string, treeViewWidget: TreeViewWidget) { - this.treeViewWidgets.set(treeViewid, treeViewWidget); - } + registerTreeView(viewId: string, treeViewWidget: TreeViewWidget): void { + this.treeViewWidgets.set(viewId, treeViewWidget); - showTreeViewWidgets(): void { - this.treeViewWidgets.forEach((treeViewWidget, treeViewId) => { - this.containersWidgets.forEach((viewsContainerWidget, viewsContainerId) => { - if (viewsContainerWidget.hasView(treeViewId)) { - viewsContainerWidget.addWidget(treeViewId, treeViewWidget); - this.applicationShell.activateWidget(viewsContainerWidget.id); - } - }); + if (this.applicationStateService.state !== READY) { + return; + } + // update containers + this.containerWidgets.forEach((containerWidget: ViewsContainerWidget, viewsContainerId: string) => { + this.addTreeViewWidget(viewsContainerId, viewId, treeViewWidget); }); } + private addTreeViewWidget(viewsContainerId: string, viewId: string, treeViewWidget: TreeViewWidget) { + const containerWidget = this.containerWidgets.get(viewsContainerId); + if (containerWidget && containerWidget.hasView(viewId)) { + containerWidget.addWidget(viewId, treeViewWidget); + } + } } diff --git a/packages/plugin-ext/src/main/browser/view/views-container-widget.ts b/packages/plugin-ext/src/main/browser/view/views-container-widget.ts index 43fa090660254..153a33a978757 100644 --- a/packages/plugin-ext/src/main/browser/view/views-container-widget.ts +++ b/packages/plugin-ext/src/main/browser/view/views-container-widget.ts @@ -16,7 +16,7 @@ import { ViewContainer, View } from '../../../common'; import { TreeViewWidget } from './tree-views-main'; -import { BaseWidget, Widget } from '@theia/core/lib/browser'; +import { Widget } from '@theia/core/lib/browser'; export function createElement(className?: string): HTMLDivElement { const div = document.createElement('div'); @@ -26,19 +26,14 @@ export function createElement(className?: string): HTMLDivElement { return div; } -export interface SectionParams { - view: View, - container: ViewsContainerWidget -} - -export class ViewsContainerWidget extends BaseWidget { +export class ViewsContainerWidget extends Widget { private sections: Map = new Map(); + private childrenId: string[] = []; sectionTitle: HTMLElement; - constructor(protected viewContainer: ViewContainer, - protected views: View[]) { + constructor(protected viewContainer: ViewContainer, protected views: View[]) { super(); this.id = `views-container-widget-${viewContainer.id}`; @@ -52,25 +47,27 @@ export class ViewsContainerWidget extends BaseWidget { this.sectionTitle.innerText = viewContainer.title; this.node.appendChild(this.sectionTitle); - // update sections - const instance = this; - - this.views.forEach(view => { - const section = new ViewContainerSection(view, instance); + views.forEach((view: View) => { + if (this.hasView(view.id)) { + return; + } + const section = new ViewContainerSection(view, () => { + this.updateDimensions(); + }); this.sections.set(view.id, section); this.node.appendChild(section.node); }); } public hasView(viewId: string): boolean { - const result = this.views.find(view => view.id === viewId); - return result !== undefined; + return this.sections.has(viewId); } public addWidget(viewId: string, viewWidget: TreeViewWidget) { const section = this.sections.get(viewId); - if (section) { + if (section && this.childrenId.indexOf(viewId) === -1) { section.addViewWidget(viewWidget); + this.childrenId.push(viewId); this.updateDimensions(); } } @@ -83,66 +80,49 @@ export class ViewsContainerWidget extends BaseWidget { public updateDimensions() { let visibleSections = 0; let availableHeight = this.node.offsetHeight; - availableHeight -= this.sectionTitle.offsetHeight; - // Determine available space for sections and how much sections are opened - this.sections.forEach((section, key) => { + this.sections.forEach((section: ViewContainerSection) => { availableHeight -= section.header.offsetHeight; - if (section.opened) { visibleSections++; } }); - // Do nothing if there is no opened sections if (visibleSections === 0) { return; } - // Get section height const sectionHeight = availableHeight / visibleSections; - // Update height of opened sections - this.sections.forEach((section, key) => { + this.sections.forEach((section: ViewContainerSection) => { if (section.opened) { - section.content.style.height = sectionHeight + 'px'; + section.content.style.height = `${sectionHeight}px`; + section.update(); } }); - - setTimeout(() => { - // Update content of visible sections - this.sections.forEach((section, key) => { - if (section.opened) { - section.update(); - } - }); - }, 1); } } export class ViewContainerSection { - node: HTMLDivElement; - header: HTMLDivElement; control: HTMLDivElement; title: HTMLDivElement; content: HTMLDivElement; - opened: boolean = true; private viewWidget: TreeViewWidget; - constructor(public view: View, protected container: ViewsContainerWidget) { + constructor(public view: View, private updateDimensionsCallback: Function) { this.node = createElement('theia-views-container-section'); this.createTitle(); this.createContent(); } - createTitle() { + createTitle(): void { this.header = createElement('theia-views-container-section-title'); this.node.appendChild(this.header); @@ -157,46 +137,42 @@ export class ViewContainerSection { this.header.onclick = () => { this.handleClick(); }; } - createContent() { + createContent(): void { this.content = createElement('theia-views-container-section-content'); this.content.setAttribute('opened', '' + this.opened); this.node.appendChild(this.content); - this.content.innerHTML = '
' + this.view.name + '
'; + this.content.innerHTML = `
${this.view.name}
`; } - handleClick() { + handleClick(): void { this.opened = !this.opened; this.control.setAttribute('opened', '' + this.opened); this.content.setAttribute('opened', '' + this.opened); - this.container.updateDimensions(); + this.updateDimensionsCallback(); - setTimeout(() => { - if (this.opened) { - this.update(); - } - }, 1); + if (this.opened) { + this.update(); + } } - addViewWidget(viewWidget: TreeViewWidget) { + addViewWidget(viewWidget: TreeViewWidget): void { this.content.innerHTML = ''; this.viewWidget = viewWidget; Widget.attach(viewWidget, this.content); - viewWidget.model.onChanged(e => { + viewWidget.model.onChanged(() => { this.update(); }); - this.update(); } - update() { + update(): void { if (this.viewWidget) { this.viewWidget.updateWidget(); } } - } diff --git a/packages/plugin-ext/src/main/browser/webview/webview.ts b/packages/plugin-ext/src/main/browser/webview/webview.ts index b933e836c2d19..96f653dbd50fa 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview.ts @@ -115,7 +115,6 @@ export class WebviewWidget extends BaseWidget { this.handleMessage(e); }; newFrame.style.visibility = 'visible'; - newFrame.contentWindow!.focus(); } }; @@ -140,6 +139,10 @@ export class WebviewWidget extends BaseWidget { this.updateSandboxAttribute(newFrame); } + focus() { + this.iframe.contentWindow!.focus(); + } + private reloadFrame() { if (!this.iframe || !this.iframe.contentDocument || !this.iframe.contentDocument.documentElement) { return; diff --git a/packages/plugin-ext/src/main/browser/webviews-main.ts b/packages/plugin-ext/src/main/browser/webviews-main.ts index b4fc291c22ba8..0140e77ac3bf6 100644 --- a/packages/plugin-ext/src/main/browser/webviews-main.ts +++ b/packages/plugin-ext/src/main/browser/webviews-main.ts @@ -14,11 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { WebviewsMain, WebviewPanelShowOptions, MAIN_RPC_CONTEXT, WebviewsExt } from '../../api/plugin-api'; +import { WebviewsMain, MAIN_RPC_CONTEXT, WebviewsExt } from '../../api/plugin-api'; import { interfaces } from 'inversify'; import { RPCProtocol } from '../../api/rpc-protocol'; import { UriComponents } from '../../common/uri-components'; -import { WebviewOptions, WebviewPanelOptions } from '@theia/plugin'; +import { WebviewOptions, WebviewPanelOptions, WebviewPanelShowOptions } from '@theia/plugin'; import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell'; import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; import { WebviewWidget } from './webview/webview'; @@ -85,8 +85,11 @@ export class WebviewsMainImpl implements WebviewsMain { this.onCloseView(viewId); }); this.views.set(viewId, view); - this.shell.addWidget(view, { area: 'main' }); + this.shell.addWidget(view, { area: showOptions.area ? showOptions.area : 'main' }); this.shell.activateWidget(view.id); + if (showOptions.preserveFocus) { + view.focus(); + } } $disposeWebview(handle: string): void { const view = this.views.get(handle); diff --git a/packages/plugin-ext/src/main/browser/workspace-main.ts b/packages/plugin-ext/src/main/browser/workspace-main.ts index 76b5d39d62f37..c964ded16bb74 100644 --- a/packages/plugin-ext/src/main/browser/workspace-main.ts +++ b/packages/plugin-ext/src/main/browser/workspace-main.ts @@ -67,10 +67,7 @@ export class WorkspaceMainImpl implements WorkspaceMain { this.inPluginFileSystemWatcherManager = new InPluginFileSystemWatcherManager(this.proxy, container); - this.workspaceService.roots.then(roots => { - this.processWorkspaceFoldersChanged(roots); - }); - + this.processWorkspaceFoldersChanged(this.workspaceService.tryGetRoots()); this.workspaceService.onWorkspaceChanged(roots => { this.processWorkspaceFoldersChanged(roots); }); @@ -81,13 +78,13 @@ export class WorkspaceMainImpl implements WorkspaceMain { return; } this.roots = roots; + this.proxy.$onWorkspaceFoldersChanged({ roots }); await this.storagePathService.updateStoragePath(roots); const keyValueStorageWorkspacesData = await this.pluginServer.keyValueStorageGetAll(false); this.storageProxy.$updatePluginsWorkspaceData(keyValueStorageWorkspacesData); - this.proxy.$onWorkspaceFoldersChanged({ roots }); } private isAnyRootChanged(roots: FileStat[]): boolean { diff --git a/packages/plugin-ext/src/plugin/command-registry.ts b/packages/plugin-ext/src/plugin/command-registry.ts index 47194b77d7e29..2ef2b2428687f 100644 --- a/packages/plugin-ext/src/plugin/command-registry.ts +++ b/packages/plugin-ext/src/plugin/command-registry.ts @@ -29,14 +29,8 @@ export class CommandRegistryImpl implements CommandRegistryExt { private readonly commands = new Set(); private readonly handlers = new Map(); - // tslint:disable-next-line:no-any - private static EMPTY_HANDLER(...args: any[]): Promise { return Promise.resolve(undefined); } - constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(Ext.COMMAND_REGISTRY_MAIN); - - // register internal VS Code commands - this.registerCommand({ id: 'vscode.previewHtml' }, CommandRegistryImpl.EMPTY_HANDLER); } // tslint:disable-next-line:no-any diff --git a/packages/plugin-ext/src/plugin/documents.ts b/packages/plugin-ext/src/plugin/documents.ts index 43f5324195a6c..0eb296e216825 100644 --- a/packages/plugin-ext/src/plugin/documents.ts +++ b/packages/plugin-ext/src/plugin/documents.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { DocumentsExt, ModelChangedEvent, PLUGIN_RPC_CONTEXT, DocumentsMain } from '../api/plugin-api'; +import { DocumentsExt, ModelChangedEvent, PLUGIN_RPC_CONTEXT, DocumentsMain, SingleEditOperation } from '../api/plugin-api'; import URI from 'vscode-uri'; import { UriComponents } from '../common/uri-components'; import { RPCProtocol } from '../api/rpc-protocol'; @@ -24,6 +24,7 @@ import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; import * as Converter from './type-converters'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; import { Range, TextDocumentShowOptions } from '../api/model'; +import { TextEdit } from './types-impl'; export class DocumentsExtImpl implements DocumentsExt { private toDispose = new DisposableCollection(); @@ -31,11 +32,13 @@ export class DocumentsExtImpl implements DocumentsExt { private _onDidRemoveDocument = new Emitter(); private _onDidChangeDocument = new Emitter(); private _onDidSaveTextDocument = new Emitter(); + private _onWillSaveTextDocument = new Emitter(); readonly onDidAddDocument: Event = this._onDidAddDocument.event; readonly onDidRemoveDocument: Event = this._onDidRemoveDocument.event; readonly onDidChangeDocument: Event = this._onDidChangeDocument.event; readonly onDidSaveTextDocument: Event = this._onDidSaveTextDocument.event; + readonly onWillSaveTextDocument: Event = this._onWillSaveTextDocument.event; private proxy: DocumentsMain; private loadingDocuments = new Map>(); @@ -78,6 +81,35 @@ export class DocumentsExtImpl implements DocumentsExt { this._onDidSaveTextDocument.fire(data.document); } } + $acceptModelWillSave(strUrl: UriComponents, reason: theia.TextDocumentSaveReason): Promise { + return new Promise((resolve, reject) => { + const uri = URI.revive(strUrl); + const uriString = uri.toString(); + const data = this.editorsAndDocuments.getDocument(uriString); + if (data) { + const onWillSaveEvent: theia.TextDocumentWillSaveEvent = { + document: data.document, + reason: reason, + /* tslint:disable:no-any */ + waitUntil: async (editsPromise: PromiseLike) => { + const editsObjs = await editsPromise; + if (this.isTextEditArray(editsObjs)) { + const editOperations: SingleEditOperation[] = (editsObjs as theia.TextEdit[]).map(textEdit => Converter.fromTextEdit(textEdit)); + resolve(editOperations); + } else { + resolve([]); + } + } + }; + this._onWillSaveTextDocument.fire(onWillSaveEvent); + } + }); + } + + isTextEditArray(obj: any): obj is theia.TextEdit[] { + return Array.isArray(obj) && obj.every((elem: any) => TextEdit.isTextEdit(elem)); + } + $acceptDirtyStateChanged(strUrl: UriComponents, isDirty: boolean): void { const uri = URI.revive(strUrl); const uriString = uri.toString(); diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 931a3eb129385..dd224054cdc3e 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -97,6 +97,7 @@ import { ColorInformation, ColorPresentation, OperatingSystem, + WebviewPanelTargetArea } from './types-impl'; import { SymbolKind } from '../api/model'; import { EditorsAndDocumentsExtImpl } from './editors-and-documents'; @@ -312,7 +313,7 @@ export function createAPIFactory( }, createWebviewPanel(viewType: string, title: string, - showOptions: theia.ViewColumn | { viewColumn: theia.ViewColumn, preserveFocus?: boolean }, + showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, options: theia.WebviewPanelOptions & theia.WebviewOptions): theia.WebviewPanel { return webviewExt.createWebview(viewType, title, showOptions, options, Uri.file(plugin.pluginPath)); }, @@ -379,8 +380,7 @@ export function createAPIFactory( return documents.onDidAddDocument(listener, thisArg, disposables); }, onWillSaveTextDocument(listener, thisArg?, disposables?) { - // TODO to implement - return { dispose: () => { } }; + return documents.onWillSaveTextDocument(listener, thisArg, disposables); }, onDidSaveTextDocument(listener, thisArg?, disposables?) { return documents.onDidSaveTextDocument(listener, thisArg, disposables); @@ -619,6 +619,10 @@ export function createAPIFactory( return tasksExt.registerTaskProvider(type, provider); }, + get taskExecutions(): ReadonlyArray { + return tasksExt.taskExecutions; + }, + onDidStartTask(listener, thisArg?, disposables?) { return tasksExt.onDidStartTask(listener, thisArg, disposables); }, @@ -712,7 +716,8 @@ export function createAPIFactory( ColorPresentation, FoldingRange, FoldingRangeKind, - OperatingSystem + OperatingSystem, + WebviewPanelTargetArea }; }; } diff --git a/packages/plugin-ext/src/plugin/tasks/tasks.ts b/packages/plugin-ext/src/plugin/tasks/tasks.ts index 80b818bf63383..1d500bf451c44 100644 --- a/packages/plugin-ext/src/plugin/tasks/tasks.ts +++ b/packages/plugin-ext/src/plugin/tasks/tasks.ts @@ -33,13 +33,18 @@ export class TasksExtImpl implements TasksExt { private callId = 0; private adaptersMap = new Map(); - private taskExecutions = new Map(); + private executions = new Map(); private readonly onDidExecuteTask: Emitter = new Emitter(); private readonly onDidTerminateTask: Emitter = new Emitter(); constructor(rpc: RPCProtocol) { this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TASKS_MAIN); + this.fetchTaskExecutions(); + } + + get taskExecutions(): ReadonlyArray { + return [...this.executions.values()]; } get onDidStartTask(): Event { @@ -57,12 +62,12 @@ export class TasksExtImpl implements TasksExt { } $onDidEndTask(id: number): void { - const taskExecution = this.taskExecutions.get(id); + const taskExecution = this.executions.get(id); if (!taskExecution) { throw new Error(`Task execution with id ${id} is not found`); } - this.taskExecutions.delete(id); + this.executions.delete(id); this.onDidTerminateTask.fire({ execution: taskExecution @@ -110,9 +115,18 @@ export class TasksExtImpl implements TasksExt { }); } + private async fetchTaskExecutions() { + try { + const taskExecutions = await this.proxy.$taskExecutions(); + taskExecutions.forEach(execution => this.getTaskExecution(execution)); + } catch (error) { + console.error(`Can not fetch running tasks: ${error}`); + } + } + private getTaskExecution(execution: TaskExecutionDto): theia.TaskExecution { const executionId = execution.id; - let result: theia.TaskExecution | undefined = this.taskExecutions.get(executionId); + let result: theia.TaskExecution | undefined = this.executions.get(executionId); if (result) { return result; } @@ -123,7 +137,7 @@ export class TasksExtImpl implements TasksExt { this.proxy.$terminateTask(executionId); } }; - this.taskExecutions.set(executionId, result); + this.executions.set(executionId, result); return result; } } diff --git a/packages/plugin-ext/src/plugin/type-converters.spec.ts b/packages/plugin-ext/src/plugin/type-converters.spec.ts index 61bb6fbfbd2eb..f4469d022f34a 100644 --- a/packages/plugin-ext/src/plugin/type-converters.spec.ts +++ b/packages/plugin-ext/src/plugin/type-converters.spec.ts @@ -175,54 +175,55 @@ describe('Type converters:', () => { const command = 'yarn'; const args = ['run', 'build']; const cwd = '/projects/theia'; + const additionalProperty = 'some property'; const shellTaskDto: ProcessTaskDto = { - type: type, - label: label, + type, + label, source, - command: command, - args: args, - cwd: cwd, + command, + args, + cwd, options: {}, - properties: {} + additionalProperty }; const shellPluginTask: theia.Task = { name: label, source, definition: { - type: type + type, + additionalProperty }, execution: { - command: command, - args: args, + command, + args, options: { - cwd: cwd + cwd } } }; const taskDtoWithCommandLine: ProcessTaskDto = { - type: type, - label: label, + type, + label, source, - command: command, - args: args, - cwd: cwd, - options: {}, - properties: {} + command, + args, + cwd, + options: {} }; const pluginTaskWithCommandLine: theia.Task = { name: label, source, definition: { - type: type + type }, execution: { commandLine: 'yarn run build', options: { - cwd: cwd + cwd } } }; @@ -254,4 +255,64 @@ describe('Type converters:', () => { assert.deepEqual(result, taskDtoWithCommandLine); }); }); + + describe('Webview Panel Show Options:', () => { + it('should create options from view column ', () => { + const viewColumn = types.ViewColumn.Five; + + const showOptions: theia.WebviewPanelShowOptions = { + area: types.WebviewPanelTargetArea.Main, + viewColumn: types.ViewColumn.Four, + preserveFocus: false + }; + + // when + const result: theia.WebviewPanelShowOptions = Converter.toWebviewPanelShowOptions(viewColumn); + + // then + assert.notEqual(result, undefined); + assert.deepEqual(result, showOptions); + }); + + it('should create options from given "WebviewPanelShowOptions" object ', () => { + const incomingObject: theia.WebviewPanelShowOptions = { + area: types.WebviewPanelTargetArea.Main, + viewColumn: types.ViewColumn.Five, + preserveFocus: true + }; + + const showOptions: theia.WebviewPanelShowOptions = { + area: types.WebviewPanelTargetArea.Main, + viewColumn: types.ViewColumn.Four, + preserveFocus: true + }; + + // when + const result: theia.WebviewPanelShowOptions = Converter.toWebviewPanelShowOptions(incomingObject); + + // then + assert.notEqual(result, undefined); + assert.deepEqual(result, showOptions); + }); + + it('should set default "main" area', () => { + const incomingObject: theia.WebviewPanelShowOptions = { + viewColumn: types.ViewColumn.Five, + preserveFocus: false + }; + + const showOptions: theia.WebviewPanelShowOptions = { + area: types.WebviewPanelTargetArea.Main, + viewColumn: types.ViewColumn.Four, + preserveFocus: false + }; + + // when + const result: theia.WebviewPanelShowOptions = Converter.toWebviewPanelShowOptions(incomingObject); + + // then + assert.notEqual(result, undefined); + assert.deepEqual(result, showOptions); + }); + }); }); diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 5f2ce87d49b9b..e3309a24b1043 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -54,6 +54,23 @@ export function fromViewColumn(column?: theia.ViewColumn): number { return ACTIVE_GROUP; } +export function toWebviewPanelShowOptions(options: theia.ViewColumn | theia.WebviewPanelShowOptions): theia.WebviewPanelShowOptions { + if (typeof options === 'object') { + const showOptions = options as theia.WebviewPanelShowOptions; + return { + area: showOptions.area ? showOptions.area : types.WebviewPanelTargetArea.Main, + viewColumn: showOptions.viewColumn ? fromViewColumn(showOptions.viewColumn) : undefined, + preserveFocus: showOptions.preserveFocus ? showOptions.preserveFocus : false + }; + } + + return { + area: types.WebviewPanelTargetArea.Main, + viewColumn: fromViewColumn(options as theia.ViewColumn), + preserveFocus: false + }; +} + export function toSelection(selection: Selection): types.Selection { const { selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn } = selection; const start = new types.Position(selectionStartLineNumber - 1, selectionStartColumn - 1); @@ -568,10 +585,10 @@ export function fromTask(task: theia.Task): TaskDto | undefined { } taskDto.type = taskDefinition.type; - taskDto.properties = {}; - for (const key in taskDefinition) { - if (key !== 'type' && taskDefinition.hasOwnProperty(key)) { - taskDto.properties[key] = taskDefinition[key]; + const { type, ...properties } = taskDefinition; + for (const key in properties) { + if (properties.hasOwnProperty(key)) { + taskDto[key] = properties[key]; } } @@ -597,11 +614,12 @@ export function toTask(taskDto: TaskDto): theia.Task { throw new Error('Task should be provided for converting'); } + const { type, label, source, command, args, options, windows, cwd, ...properties } = taskDto; const result = {} as theia.Task; - result.name = taskDto.label; - result.source = taskDto.source; + result.name = label; + result.source = source; - const taskType = taskDto.type; + const taskType = type; const taskDefinition: theia.TaskDefinition = { type: taskType }; @@ -616,7 +634,6 @@ export function toTask(taskDto: TaskDto): theia.Task { result.execution = getShellExecution(taskDto as ProcessTaskDto); } - const properties = taskDto.properties; if (!properties) { return result; } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index beaf1514e6ecf..878d1052c78da 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -666,7 +666,7 @@ export class TextEdit { if (!thing) { return false; } - return Range.isRange((thing)) + return Range.isRange((thing).range) && typeof (thing).newText === 'string'; } @@ -1801,3 +1801,11 @@ export enum OperatingSystem { Linux = 'Linux', OSX = 'OSX' } + +/** The areas of the application shell where webview panel can reside. */ +export enum WebviewPanelTargetArea { + Main = 'main', + Left = 'left', + Right = 'right', + Bottom = 'bottom' +} diff --git a/packages/plugin-ext/src/plugin/webviews.ts b/packages/plugin-ext/src/plugin/webviews.ts index 4576c2378c86c..2b4da44fb2f0f 100644 --- a/packages/plugin-ext/src/plugin/webviews.ts +++ b/packages/plugin-ext/src/plugin/webviews.ts @@ -19,9 +19,9 @@ import * as theia from '@theia/plugin'; import { RPCProtocol } from '../api/rpc-protocol'; import URI from 'vscode-uri/lib/umd'; import { Emitter, Event } from '@theia/core/lib/common/event'; -import { fromViewColumn, toViewColumn } from './type-converters'; +import { fromViewColumn, toViewColumn, toWebviewPanelShowOptions } from './type-converters'; import { IdGenerator } from '../common/id-generator'; -import { Disposable } from './types-impl'; +import { Disposable, WebviewPanelTargetArea } from './types-impl'; export class WebviewsExtImpl implements WebviewsExt { private readonly proxy: WebviewsMain; @@ -80,21 +80,16 @@ export class WebviewsExtImpl implements WebviewsExt { createWebview(viewType: string, title: string, - showOptions: theia.ViewColumn | { viewColumn: theia.ViewColumn, preserveFocus?: boolean }, + showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, options: (theia.WebviewPanelOptions & theia.WebviewOptions) | undefined, extensionLocation: URI): theia.WebviewPanel { - const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; - const webviewShowOptions = { - viewColumn: fromViewColumn(viewColumn), - preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus - }; - + const webviewShowOptions = toWebviewPanelShowOptions(showOptions); const viewId = this.idGenerator.nextId(); this.proxy.$createWebviewPanel(viewId, viewType, title, webviewShowOptions, options, extensionLocation); const webview = new WebviewImpl(viewId, this.proxy, options); - const panel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, viewColumn, options, webview); + const panel = new WebviewPanelImpl(viewId, this.proxy, viewType, title, webviewShowOptions, options, webview); this.webviewPanels.set(viewId, panel); return panel; @@ -189,6 +184,7 @@ export class WebviewPanelImpl implements theia.WebviewPanel { private isDisposed = false; private _active = true; private _visible = true; + private _showOptions: theia.WebviewPanelShowOptions; readonly onDisposeEmitter = new Emitter(); public readonly onDidDispose: Event = this.onDisposeEmitter.event; @@ -200,11 +196,11 @@ export class WebviewPanelImpl implements theia.WebviewPanel { private readonly proxy: WebviewsMain, private readonly _viewType: string, private _title: string, - private _viewColumn: theia.ViewColumn, + showOptions: theia.ViewColumn | theia.WebviewPanelShowOptions, private readonly _options: theia.WebviewPanelOptions | undefined, private readonly _webview: WebviewImpl ) { - + this._showOptions = typeof showOptions === 'object' ? showOptions : { viewColumn: showOptions as theia.ViewColumn }; } dispose() { @@ -262,14 +258,24 @@ export class WebviewPanelImpl implements theia.WebviewPanel { return this._options!; } - get viewColumn(): theia.ViewColumn { + get viewColumn(): theia.ViewColumn | undefined { this.checkIsDisposed(); - return this._viewColumn; + return this._showOptions.viewColumn; } setViewColumn(value: theia.ViewColumn) { this.checkIsDisposed(); - this._viewColumn = value; + this._showOptions.viewColumn = value; + } + + get showOptions(): theia.WebviewPanelShowOptions { + this.checkIsDisposed(); + return this._showOptions; + } + + setShowOptions(value: theia.WebviewPanelShowOptions) { + this.checkIsDisposed(); + this._showOptions = value; } get active(): boolean { @@ -292,9 +298,10 @@ export class WebviewPanelImpl implements theia.WebviewPanel { this._visible = value; } - reveal(viewColumn?: theia.ViewColumn, preserveFocus?: boolean): void { + reveal(area?: WebviewPanelTargetArea, viewColumn?: theia.ViewColumn, preserveFocus?: boolean): void { this.checkIsDisposed(); this.proxy.$reveal(this.viewId, { + area: area, viewColumn: viewColumn ? fromViewColumn(viewColumn) : undefined, preserveFocus: !!preserveFocus }); diff --git a/packages/plugin/API.md b/packages/plugin/API.md index 47c3769833bae..dbd9b8d3fba83 100644 --- a/packages/plugin/API.md +++ b/packages/plugin/API.md @@ -249,7 +249,7 @@ Where are: - "shellPath" - path to the executable shell, for example "/bin/bash", "bash", "sh" or so on. - "shellArgs" - shell command arguments, for example without login: "-l". If you defined shell command "/bin/bash" and set up shell arguments "-l" than will be created terminal process with command "/bin/bash -l". And client side will connect to stdin/stdout of this process to interaction with user. - "cwd" - current working directory; - - "env"- enviroment variables for terminal process, for example TERM - identifier terminal window capabilities. + - "env"- environment variables for terminal process, for example TERM - identifier terminal window capabilities. Function to create new terminal with defined theia.TerminalOptions described above: @@ -279,7 +279,7 @@ Where are: - first argument - text content. - second argument - in case true, terminal will apply new line after the text, otherwise will send only the text. -Distroy terminal: +Destroy terminal: ```typescript terminal.dispose(); @@ -355,7 +355,7 @@ If no diagnostics found empty array will be returned. Note, that returned array from `getDiagnostics` call are readonly. -To tracks changes in diagnostics `onDidChangeDiagnostics` event should be used. Within event handler list of uris with changed diadgnostics is available. Example: +To tracks changes in diagnostics `onDidChangeDiagnostics` event should be used. Within event handler list of uris with changed diagnostics is available. Example: ```typescript disposables.push( @@ -371,7 +371,7 @@ Also it is possible to add own diagnostics. To do this, one should create diagno const diagnosticsCollection = theia.languages.createDiagnosticCollection(collectionName); ``` -Collection name can be ommited. In such case the name will be auto-generated. +Collection name can be omitted. In such case the name will be auto-generated. When collection is created, one could operate with diagnostics. The collection object exposes all needed methods: `get`, `set`, `has`, `delete`, `clear`, `forEach` and `dispose`. @@ -397,7 +397,7 @@ changes.push([uri1, diagnostics4]); // uri1 again diagnosticsCollection.set(changes); ``` -If the same uri is used a few times, corresponding diagnostics will be merged. In case of `undefined` all previous, but not following, diagnostics will be cleared. If `undefined` is given insted of tuples array the whole collection will be cleared. +If the same uri is used a few times, corresponding diagnostics will be merged. In case of `undefined` all previous, but not following, diagnostics will be cleared. If `undefined` is given instead of tuples array the whole collection will be cleared. To iterate over all diagnostics within the collection `forEach` method could be used: @@ -407,7 +407,7 @@ diagnosticsCollection.forEach((uri, diagnostics) => { } ``` -`dispose` method should be used when the collection is not needed any more. In case of attempt to do an operaton after disposing an error will be thrown. +`dispose` method should be used when the collection is not needed any more. In case of attempt to do an operation after disposing an error will be thrown. #### Signature help diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 4d9b8378c21a5..b3a92df92bb6f 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -2569,6 +2569,36 @@ declare module '@theia/plugin' { readonly retainContextWhenHidden?: boolean; } + /** + * The areas of the application shell where webview panel can reside. + */ + export enum WebviewPanelTargetArea { + Main = 'main', + Left = 'left', + Right = 'right', + Bottom = 'bottom' + } + + /** + * Settings to determine where webview panel will be reside + */ + export interface WebviewPanelShowOptions { + /** + * Target area where webview panel will be resided. Shows in the 'WebviewPanelTargetArea.Main' area if undefined. + */ + area?: WebviewPanelTargetArea; + + /** + * Editor View column to show the panel in. Shows in the current `viewColumn` if undefined. + */ + viewColumn?: number; + + /** + * When `true`, the webview will not take focus. + */ + preserveFocus?: boolean; + } + /** * A panel that contains a webview. */ @@ -2598,6 +2628,10 @@ declare module '@theia/plugin' { */ readonly options: WebviewPanelOptions; + /** + * Settings to determine where webview panel will be reside + */ + readonly showOptions?: WebviewPanelShowOptions; /** * Editor position of the panel. This property is only set if the webview is in * one of the editor view columns. @@ -2630,15 +2664,16 @@ declare module '@theia/plugin' { readonly onDidDispose: Event; /** - * Show the webview panel in a given column. + * Show the webview panel according to a given options. * * A webview panel may only show in a single column at a time. If it is already showing, this * method moves it to a new column. * + * @param area target area where webview panel will be resided. Shows in the 'WebviewPanelTargetArea.Main' area if undefined. * @param viewColumn View column to show the panel in. Shows in the current `viewColumn` if undefined. * @param preserveFocus When `true`, the webview will not take focus. */ - reveal(viewColumn?: ViewColumn, preserveFocus?: boolean): void; + reveal(area?: WebviewPanelTargetArea, viewColumn?: ViewColumn, preserveFocus?: boolean): void; /** * Dispose of the webview panel. @@ -2999,12 +3034,12 @@ declare module '@theia/plugin' { * * @param viewType Identifies the type of the webview panel. * @param title Title of the panel. - * @param showOptions Where to show the webview in the editor. If preserveFocus is set, the new webview will not take focus. + * @param showOptions where webview panel will be reside. If preserveFocus is set, the new webview will not take focus. * @param options Settings for the new panel. * * @return New webview panel. */ - export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | { viewColumn: ViewColumn, preserveFocus?: boolean }, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; + export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | WebviewPanelShowOptions, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; /** * Registers a webview panel serializer. @@ -7121,6 +7156,11 @@ declare module '@theia/plugin' { */ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable; + /** + * The currently active task executions or an empty array. + */ + export const taskExecutions: ReadonlyArray; + /** Fires when a task starts. */ export const onDidStartTask: Event; diff --git a/packages/terminal/package.json b/packages/terminal/package.json index 01401b537092e..15c3c8b1e965f 100644 --- a/packages/terminal/package.json +++ b/packages/terminal/package.json @@ -4,6 +4,7 @@ "description": "Theia - Terminal Extension", "dependencies": { "@theia/core": "^0.3.19", + "@theia/editor": "^0.3.19", "@theia/filesystem": "^0.3.19", "@theia/process": "^0.3.19", "@theia/workspace": "^0.3.19", diff --git a/packages/terminal/src/browser/terminal-preferences.ts b/packages/terminal/src/browser/terminal-preferences.ts index 0303686935ae9..4155588369852 100644 --- a/packages/terminal/src/browser/terminal-preferences.ts +++ b/packages/terminal/src/browser/terminal-preferences.ts @@ -16,6 +16,7 @@ import { interfaces } from 'inversify'; import { createPreferenceProxy, PreferenceProxy, PreferenceService, PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser'; +import { EDITOR_FONT_DEFAULTS } from '@theia/editor/lib/browser'; export const TerminalConfigSchema: PreferenceSchema = { type: 'object', @@ -29,15 +30,55 @@ export const TerminalConfigSchema: PreferenceSchema = { type: 'boolean', description: 'Enable ctrl-v (cmd-v on macOS) to paste from clipboard', default: true - } + }, + 'terminal.integrated.fontFamily': { + type: 'string', + description: 'Controls the font family of the terminal.', + default: EDITOR_FONT_DEFAULTS.fontFamily + }, + 'terminal.integrated.fontSize': { + type: 'number', + description: 'Controls the font size in pixels of the terminal.', + default: EDITOR_FONT_DEFAULTS.fontSize + }, + 'terminal.integrated.fontWeight': { + type: 'string', + enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + description: 'The font weight to use within the terminal for non-bold text.', + default: 'normal' + }, + 'terminal.integrated.fontWeightBold': { + type: 'string', + enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + description: 'The font weight to use within the terminal for bold text.', + default: 'bold' + }, + 'terminal.integrated.letterSpacing': { + description: 'Controls the letter spacing of the terminal, this is an integer value which represents the amount of additional pixels to add between characters.', + type: 'number', + default: 1 + }, + 'terminal.integrated.lineHeight': { + description: 'Controls the line height of the terminal, this number is multiplied by the terminal font size to get the actual line-height in pixels.', + type: 'number', + default: 1 + }, } }; export interface TerminalConfiguration { 'terminal.enableCopy': boolean 'terminal.enablePaste': boolean + 'terminal.integrated.fontFamily': string + 'terminal.integrated.fontSize': number + 'terminal.integrated.fontWeight': FontWeigth + 'terminal.integrated.fontWeightBold': FontWeigth + 'terminal.integrated.letterSpacing': number + 'terminal.integrated.lineHeight': number } +type FontWeigth = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + export const TerminalPreferences = Symbol('TerminalPreferences'); export type TerminalPreferences = PreferenceProxy; diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index cf7800922d1cc..a20b3891cf598 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -39,19 +39,13 @@ export interface TerminalWidgetFactoryOptions extends Partial { + const lastSeparator = change.preferenceName.lastIndexOf('.'); + if (lastSeparator > 0) { + const preferenceName = change.preferenceName.substr(lastSeparator + 1); + this.term.setOption(preferenceName, this.preferences[change.preferenceName]); + this.needsResize = true; + this.update(); + } + })); this.toDispose.push(this.themeService.onThemeChange(c => { const changedProps = this.getCSSPropertiesFromPage(); @@ -201,21 +208,10 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget /* Get the CSS properties of (aka :root in css). */ const htmlElementProps = getComputedStyle(document.documentElement!); - const fontFamily = lookup(htmlElementProps, '--theia-terminal-font-family'); - const fontSizeStr = lookup(htmlElementProps, '--theia-code-font-size'); const foreground = lookup(htmlElementProps, '--theia-ui-font-color1'); const background = lookup(htmlElementProps, '--theia-layout-color0'); const selection = lookup(htmlElementProps, '--theia-transparent-accent-color2'); - /* The font size is returned as a string, such as ' 13px'). We want to - return just the number of px. */ - const fontSizeMatch = fontSizeStr.trim().match(/^(\d+)px$/); - if (!fontSizeMatch) { - throw new Error(`Unexpected format for --theia-code-font-size (${fontSizeStr})`); - } - - const fontSize = Number.parseInt(fontSizeMatch[1]); - /* xterm.js expects #XXX of #XXXXXX for colors. */ const colorRe = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; @@ -228,8 +224,6 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget } return { - fontSize, - fontFamily, foreground, background, selection diff --git a/packages/workspace/src/browser/diff-service.ts b/packages/workspace/src/browser/diff-service.ts new file mode 100644 index 0000000000000..814e39cdcb70e --- /dev/null +++ b/packages/workspace/src/browser/diff-service.ts @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (C) 2017 TypeFox and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import {inject, injectable} from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { DiffUris } from '@theia/core/lib/browser/diff-uris'; +import { FileSystem } from '@theia/filesystem/lib/common/filesystem'; +import { OpenerService } from '@theia/core/lib/browser'; +import { MessageService } from '@theia/core/lib/common/message-service'; + +@injectable() +export class DiffService { + + @inject(FileSystem) protected readonly fileSystem: FileSystem; + @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(MessageService) protected readonly messageService: MessageService; + + public async openDiffEditor(left: URI, right: URI) { + const [leftExists, rightExists] = await Promise.all([ + this.fileSystem.exists(left.toString()), + this.fileSystem.exists(right.toString()) + ]); + if (leftExists && rightExists) { + const [leftStat, rightStat] = await Promise.all([ + this.fileSystem.getFileStat(left.toString()), + this.fileSystem.getFileStat(right.toString()), + ]); + if (leftStat && rightStat) { + if (!leftStat.isDirectory && !rightStat.isDirectory) { + const uri = DiffUris.encode(left, right); + const opener = await this.openerService.getOpener(uri); + opener.open(uri); + } else { + const details = (() => { + if (leftStat.isDirectory && rightStat.isDirectory) { + return 'Both resource were a directory.'; + } else { + if (leftStat.isDirectory) { + return `'${left.path.base}' was a directory.`; + } else { + return `'${right.path.base}' was a directory.`; + } + } + }); + this.messageService.warn(`Directories cannot be compared. ${details()}`); + } + } + } + } +} diff --git a/packages/workspace/src/browser/workspace-commands.ts b/packages/workspace/src/browser/workspace-commands.ts index ff0c20c58c0d1..8643e50036380 100644 --- a/packages/workspace/src/browser/workspace-commands.ts +++ b/packages/workspace/src/browser/workspace-commands.ts @@ -16,7 +16,6 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; -import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import { SelectionService } from '@theia/core/lib/common/selection-service'; import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu'; @@ -32,6 +31,7 @@ import { WorkspacePreferences } from './workspace-preferences'; import { WorkspaceDeleteHandler } from './workspace-delete-handler'; import { WorkspaceDuplicateHandler } from './workspace-duplicate-handler'; import { FileSystemUtils } from '@theia/filesystem/lib/common'; +import { WorkspaceCompareHandler } from './workspace-compare-handler'; const validFilename: (arg: string) => boolean = require('valid-filename'); @@ -153,6 +153,7 @@ export class WorkspaceCommandContribution implements CommandContribution { @inject(FileDialogService) protected readonly fileDialogService: FileDialogService; @inject(WorkspaceDeleteHandler) protected readonly deleteHandler: WorkspaceDeleteHandler; @inject(WorkspaceDuplicateHandler) protected readonly duplicateHandler: WorkspaceDuplicateHandler; + @inject(WorkspaceCompareHandler) protected readonly compareHandler: WorkspaceCompareHandler; registerCommands(registry: CommandRegistry): void { this.openerService.getOpeners().then(openers => { @@ -242,43 +243,7 @@ export class WorkspaceCommandContribution implements CommandContribution { })); registry.registerCommand(WorkspaceCommands.FILE_DUPLICATE, this.newMultiUriAwareCommandHandler(this.duplicateHandler)); registry.registerCommand(WorkspaceCommands.FILE_DELETE, this.newMultiUriAwareCommandHandler(this.deleteHandler)); - registry.registerCommand(WorkspaceCommands.FILE_COMPARE, this.newMultiUriAwareCommandHandler({ - isVisible: uris => uris.length === 2, - isEnabled: uris => uris.length === 2, - execute: async uris => { - const [left, right] = uris; - const [leftExists, rightExists] = await Promise.all([ - this.fileSystem.exists(left.toString()), - this.fileSystem.exists(right.toString()) - ]); - if (leftExists && rightExists) { - const [leftStat, rightStat] = await Promise.all([ - this.fileSystem.getFileStat(left.toString()), - this.fileSystem.getFileStat(right.toString()), - ]); - if (leftStat && rightStat) { - if (!leftStat.isDirectory && !rightStat.isDirectory) { - const uri = DiffUris.encode(left, right); - const opener = await this.openerService.getOpener(uri); - opener.open(uri); - } else { - const details = (() => { - if (leftStat.isDirectory && rightStat.isDirectory) { - return 'Both resource were a directory.'; - } else { - if (leftStat.isDirectory) { - return `'${left.path.base}' was a directory.`; - } else { - return `'${right.path.base}' was a directory.`; - } - } - }); - this.messageService.warn(`Directories cannot be compared. ${details()}`); - } - } - } - } - })); + registry.registerCommand(WorkspaceCommands.FILE_COMPARE, this.newMultiUriAwareCommandHandler(this.compareHandler)); this.preferences.ready.then(() => { registry.registerCommand(WorkspaceCommands.ADD_FOLDER, this.newMultiUriAwareCommandHandler({ isEnabled: () => this.workspaceService.isMultiRootWorkspaceOpened, diff --git a/packages/workspace/src/browser/workspace-compare-handler.ts b/packages/workspace/src/browser/workspace-compare-handler.ts new file mode 100644 index 0000000000000..ffe68d18e4578 --- /dev/null +++ b/packages/workspace/src/browser/workspace-compare-handler.ts @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (C) 2019 Red Hat, Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { UriCommandHandler } from '@theia/core/lib/common/uri-command-handler'; +import { DiffService } from './diff-service'; + +@injectable() +export class WorkspaceCompareHandler implements UriCommandHandler { + + @inject(DiffService) protected readonly diffService: DiffService; + + /** + * Determine if the command is visible. + * + * @param uris URIs of selected resources. + * @returns `true` if the command is visible. + */ + isVisible(uris: URI[]): boolean { + return uris.length === 2; + } + + /** + * Determine if the command is enabled. + * + * @param uris URIs of selected resources. + * @returns `true` if the command is enabled. + */ + isEnabled(uris: URI[]): boolean { + return uris.length === 2; + } + + /** + * Execute the command. + * + * @param uris URIs of selected resources. + */ + async execute(uris: URI[]): Promise { + const [left, right] = uris; + await this.diffService.openDiffEditor(left, right); + } +} diff --git a/packages/workspace/src/browser/workspace-frontend-module.ts b/packages/workspace/src/browser/workspace-frontend-module.ts index de711db8b0468..a15a12fc1adec 100644 --- a/packages/workspace/src/browser/workspace-frontend-module.ts +++ b/packages/workspace/src/browser/workspace-frontend-module.ts @@ -42,6 +42,8 @@ import { QuickOpenWorkspace } from './quick-open-workspace'; import { WorkspaceDeleteHandler } from './workspace-delete-handler'; import { WorkspaceDuplicateHandler } from './workspace-duplicate-handler'; import { WorkspaceUtils } from './workspace-utils'; +import { WorkspaceCompareHandler } from './workspace-compare-handler'; +import { DiffService } from './diff-service'; export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => { bindWorkspacePreferences(bind); @@ -72,6 +74,8 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un bind(MenuContribution).to(FileMenuContribution).inSingletonScope(); bind(WorkspaceDeleteHandler).toSelf().inSingletonScope(); bind(WorkspaceDuplicateHandler).toSelf().inSingletonScope(); + bind(WorkspaceCompareHandler).toSelf().inSingletonScope(); + bind(DiffService).toSelf().inSingletonScope(); bind(WorkspaceStorageService).toSelf().inSingletonScope(); rebind(StorageService).toService(WorkspaceStorageService); diff --git a/packages/workspace/src/browser/workspace-service.ts b/packages/workspace/src/browser/workspace-service.ts index b2225bc21e67d..697bca3a11eec 100644 --- a/packages/workspace/src/browser/workspace-service.ts +++ b/packages/workspace/src/browser/workspace-service.ts @@ -93,8 +93,8 @@ export class WorkspaceService implements FrontendApplicationContribution { protected getDefaultWorkspacePath(): MaybePromise { // Prefer the workspace path specified as the URL fragment, if present. if (window.location.hash.length > 1) { - // Remove the leading #. - const wpPath = window.location.hash.substring(1); + // Remove the leading # and decode the URI. + const wpPath = decodeURI(window.location.hash.substring(1)); return new URI().withPath(wpPath).withScheme('file').toString(); } else { // Else, ask the server for its suggested workspace (usually the one diff --git a/packages/workspace/src/node/default-workspace-server.ts b/packages/workspace/src/node/default-workspace-server.ts index eeaf4126818b3..b2a9b632a109f 100644 --- a/packages/workspace/src/node/default-workspace-server.ts +++ b/packages/workspace/src/node/default-workspace-server.ts @@ -68,6 +68,11 @@ export class DefaultWorkspaceServer implements WorkspaceServer { @postConstruct() protected async init() { + const root = await this.getRoot(); + this.root.resolve(root); + } + + protected async getRoot(): Promise { let root = await this.getWorkspaceURIFromCli(); if (!root) { const data = await this.readRecentWorkspacePathsFromUserHome(); @@ -75,7 +80,7 @@ export class DefaultWorkspaceServer implements WorkspaceServer { root = data.recentRoots[0]; } } - this.root.resolve(root); + return root; } getMostRecentlyUsedWorkspace(): Promise { @@ -115,7 +120,7 @@ export class DefaultWorkspaceServer implements WorkspaceServer { return listUri; } - private workspaceStillExist(wspath: string): boolean { + protected workspaceStillExist(wspath: string): boolean { return fs.pathExistsSync(FileUri.fsPath(wspath)); } @@ -128,12 +133,12 @@ export class DefaultWorkspaceServer implements WorkspaceServer { * Writes the given uri as the most recently used workspace root to the user's home directory. * @param uri most recently used uri */ - private async writeToUserHome(data: RecentWorkspacePathsData): Promise { + protected async writeToUserHome(data: RecentWorkspacePathsData): Promise { const file = this.getUserStoragePath(); await this.writeToFile(file, data); } - private async writeToFile(filePath: string, data: object): Promise { + protected async writeToFile(filePath: string, data: object): Promise { if (!await fs.pathExists(filePath)) { await fs.mkdirs(path.resolve(filePath, '..')); } @@ -143,13 +148,13 @@ export class DefaultWorkspaceServer implements WorkspaceServer { /** * Reads the most recently used workspace root from the user's home directory. */ - private async readRecentWorkspacePathsFromUserHome(): Promise { + protected async readRecentWorkspacePathsFromUserHome(): Promise { const filePath = this.getUserStoragePath(); const data = await this.readJsonFromFile(filePath); return RecentWorkspacePathsData.is(data) ? data : undefined; } - private async readJsonFromFile(filePath: string): Promise { + protected async readJsonFromFile(filePath: string): Promise { if (await fs.pathExists(filePath)) { const rawContent = await fs.readFile(filePath, 'utf-8'); const strippedContent = jsoncparser.stripComments(rawContent); @@ -168,6 +173,7 @@ interface RecentWorkspacePathsData { namespace RecentWorkspacePathsData { export function is(data: Object | undefined): data is RecentWorkspacePathsData { - return !!data && typeof data === 'object' && ('recentRoots' in data) && Array.isArray(data['recentRoots']); + // tslint:disable-next-line:no-any + return !!data && typeof data === 'object' && ('recentRoots' in data) && Array.isArray((data as any)['recentRoots']); } } diff --git a/yarn.lock b/yarn.lock index ecf0603238d9e..94f2d6afeacea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,7 +121,6 @@ "@theia/node-pty@0.7.8-theia004": version "0.7.8-theia004" resolved "https://registry.yarnpkg.com/@theia/node-pty/-/node-pty-0.7.8-theia004.tgz#0fe31b958df9315352d5fbeea7075047cf69c935" - integrity sha512-GetaD2p1qVPq/xbNCHCwKYjIr9IWjksf9V2iiv/hV6f885cJ+ie0Osr4+C159PrwzGRYW2jQVUtXghBJoyOCLg== dependencies: nan "2.10.0" @@ -1741,7 +1740,6 @@ browser-stdout@1.3.0: browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.2.0" @@ -2387,7 +2385,6 @@ commander@*, commander@^2.11.0, commander@^2.12.1, commander@^2.8.1, commander@^ commander@2.15.1: version "2.15.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== commander@2.6.0: version "2.6.0" @@ -2460,6 +2457,16 @@ concurrently@^3.5.0: supports-color "^3.2.3" tree-kill "^1.1.0" +conf@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/conf/-/conf-2.2.0.tgz#ee282efafc1450b61e205372041ad7d866802d9a" + dependencies: + dot-prop "^4.1.0" + env-paths "^1.0.0" + make-dir "^1.0.0" + pkg-up "^2.0.0" + write-file-atomic "^2.3.0" + console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -2836,7 +2843,6 @@ css-loader@~0.26.1: css-parse@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" - integrity sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q= dependencies: css "^2.0.0" @@ -3017,7 +3023,6 @@ debug@3.1.0, debug@^3.1.0: debug@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== dependencies: ms "^2.1.1" @@ -3285,6 +3290,12 @@ dot-prop@^3.0.0: dependencies: is-obj "^1.0.0" +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + dependencies: + is-obj "^1.0.0" + drivelist@^6.4.3: version "6.4.3" resolved "https://registry.yarnpkg.com/drivelist/-/drivelist-6.4.3.tgz#b6bf640d26e77ccba2f90c47134bd3b6a34f9709" @@ -3301,18 +3312,20 @@ dtrace-provider@~0.8: dependencies: nan "^2.10.0" -dugite-extra@0.1.9: - version "0.1.9" - resolved "https://registry.yarnpkg.com/dugite-extra/-/dugite-extra-0.1.9.tgz#31f73c683804e3c059a5dba512e5159de18975df" +dugite-extra@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/dugite-extra/-/dugite-extra-0.1.10.tgz#145a3c85f59b468effa73c70a1dd4a03d9af3889" + integrity sha512-J0FXzulIKl0NchW/B03OPy9AqHo3fEkqNXxhjsiHWHuoxMpvkj7WZ2Uj2+kZO2enKtTPx6uag89A77cACx3yvA== dependencies: byline "^5.0.0" - dugite "1.67.0" + dugite-no-gpl "1.69.0" find-git-exec "0.0.1-alpha.2" upath "^1.0.0" -dugite@1.67.0: - version "1.67.0" - resolved "https://registry.yarnpkg.com/dugite/-/dugite-1.67.0.tgz#389f95051aa1fb2bc78dcee4294f913ae3438c47" +dugite-no-gpl@1.69.0: + version "1.69.0" + resolved "https://registry.yarnpkg.com/dugite-no-gpl/-/dugite-no-gpl-1.69.0.tgz#bc9007cf5a595180f563ccc0e4f2cc80ebbaa52e" + integrity sha512-9NzPMyWW1uWEm+rEGivfQ0+zZ9soXrtk/zb6FIVpPa5CLoUdhMkLY4jHc0DDyayarxivJgrI/rHDdTUej4Zhrw== dependencies: checksum "^0.1.1" mkdirp "^0.5.1" @@ -3417,6 +3430,12 @@ electron-rebuild@^1.5.11: spawn-rx "^2.0.10" yargs "^7.0.2" +electron-store@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-2.0.0.tgz#1035cca2a95409d1f54c7466606345852450d64a" + dependencies: + conf "^2.0.0" + electron-to-chromium@^1.2.7: version "1.3.58" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.58.tgz#8267a4000014e93986d9d18c65a8b4022ca75188" @@ -3489,6 +3508,10 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" +env-paths@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" + env-variable@0.0.x: version "0.0.4" resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.4.tgz#0d6280cf507d84242befe35a512b5ae4be77c54e" @@ -3890,7 +3913,6 @@ fecha@^2.3.3: fibers@~2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/fibers/-/fibers-2.0.2.tgz#36db63ea61c543174e2264675fea8c2783371366" - integrity sha512-HfVRxhYG7C8Jl9FqtrlElMR2z/8YiLQVDKf67MLY25Ic+ILx3ecmklfT1v3u+7P5/4vEFjuxaAFXhr2/Afwk5g== figures@^1.7.0: version "1.7.0" @@ -4539,7 +4561,6 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.2 grapheme-splitter@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== grouped-queue@^0.3.3: version "0.3.3" @@ -4550,7 +4571,6 @@ grouped-queue@^0.3.3: growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== growl@1.9.2: version "1.9.2" @@ -4848,7 +4868,6 @@ https-proxy-agent@^2.2.1: humanize-duration@~3.15.0: version "3.15.3" resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.15.3.tgz#600a939bd9d9a16b696e907b3fc08d1a4f15e8c9" - integrity sha512-BMz6w8p3NVa6QP9wDtqUkXfwgBqDaZ5z/np0EYdoWrLqL849Onp6JWMXMhbHtuvO9jUThLN5H1ThRQ8dUWnYkA== iconv-lite@0.4.19: version "0.4.19" @@ -5554,7 +5573,6 @@ jsonc-parser@^2.0.0-next.1, jsonc-parser@^2.0.1: jsonc-parser@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.0.2.tgz#42fcf56d70852a043fadafde51ddb4a85649978d" - integrity sha512-TSU435K5tEKh3g7bam1AFf+uZrISheoDsLlpmAo6wWZYqjsnd09lHYK1Qo+moK4Ikifev1Gdpa69g4NELKnCrQ== jsonfile@^2.1.0: version "2.4.0" @@ -6432,7 +6450,6 @@ mocha@^3.4.2: mocha@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" - integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== dependencies: browser-stdout "1.3.1" commander "2.15.1" @@ -7313,6 +7330,12 @@ pkg-dir@^2.0.0: dependencies: find-up "^2.1.0" +pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + dependencies: + find-up "^2.1.0" + pn@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" @@ -7649,7 +7672,6 @@ progress-stream@^1.1.0: progress@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" - integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== progress@^2.0.0: version "2.0.0" @@ -8319,7 +8341,6 @@ ret@~0.1.10: rgb2hex@^0.1.9: version "0.1.9" resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.1.9.tgz#5d3e0e14b0177b568e6f0d5b43e34fbfdb670346" - integrity sha512-32iuQzhOjyT+cv9aAFRBJ19JgHwzQwbjUhH3Fj2sWW2EEGAW8fpFrDFP5ndoKDxJaLO06x1hE3kyuIFrUQtybQ== right-align@^0.1.1: version "0.1.3" @@ -8475,7 +8496,6 @@ seek-bzip@^1.0.5: selenium-standalone@^6.15.4: version "6.15.4" resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.15.4.tgz#9f9056f625bd7d2558483562b3e8be80947e9faf" - integrity sha512-J4FZzbkgnQ0D148ZgR9a+SqdnXPyKEhWLHP4pg5dP8b3U0CZmfzXL2gp/R4c1FrmXujosueVE57XO9//l4sEaA== dependencies: async "^2.1.4" commander "^2.9.0" @@ -9095,7 +9115,6 @@ supports-color@3.1.2: supports-color@5.4.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" - integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== dependencies: has-flag "^3.0.0" @@ -9165,7 +9184,6 @@ tar-fs@^1.13.0, tar-fs@^1.16.2: tar-stream@1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" - integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== dependencies: bl "^1.0.0" buffer-alloc "^1.2.0" @@ -9358,7 +9376,6 @@ to-arraybuffer@^1.0.0: to-buffer@^1.1.0, to-buffer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" - integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== to-fast-properties@^1.0.3: version "1.0.3" @@ -9983,7 +10000,6 @@ wdio-dot-reporter@~0.0.8: wdio-mocha-framework@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/wdio-mocha-framework/-/wdio-mocha-framework-0.5.13.tgz#f4da119456cb673b8c058fb60936132ec752a9d4" - integrity sha512-sg9EXWJLVTaLgJBOESvfhz3IWN8ujYFU35bazLtHCfmH3t8G9Fy7nNezJ/QdMBB5Ug1yyISynXkn2XWFJ+Tvgw== dependencies: babel-runtime "^6.23.0" mocha "^5.0.0" @@ -9992,7 +10008,6 @@ wdio-mocha-framework@0.5.13: wdio-selenium-standalone-service@0.0.12: version "0.0.12" resolved "https://registry.yarnpkg.com/wdio-selenium-standalone-service/-/wdio-selenium-standalone-service-0.0.12.tgz#f472d00d3a7800b2dbedb781bff0f5e726a21e9d" - integrity sha512-R8iUL30SkFfZictAG5wRofeCsHQ4bIucDtaArCQWZkUqS+DlGTStIk3TaIOCaX7dS7UW1YN/lJt9Vsn4Ekmoxg== dependencies: fs-extra "^0.30.0" selenium-standalone "^6.15.4" @@ -10000,7 +10015,6 @@ wdio-selenium-standalone-service@0.0.12: wdio-spec-reporter@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/wdio-spec-reporter/-/wdio-spec-reporter-0.1.5.tgz#6d6f865deac6b36f96988c1204cc81099b75fc7e" - integrity sha512-MqvgTow8hFwhFT47q67JwyJyeynKodGRQCxF7ijKPGfsaG1NLssbXYc0JhiL7SiAyxnQxII0UxzTCd3I6sEdkg== dependencies: babel-runtime "~6.26.0" chalk "^2.3.0" @@ -10009,7 +10023,6 @@ wdio-spec-reporter@0.1.5: wdio-sync@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/wdio-sync/-/wdio-sync-0.7.1.tgz#00847fbbce16826c3225618f4259d28b60a42483" - integrity sha512-7BTWoBbDZsIVR67mx3cqkYiE3gZid5OJPBcjje1SlC28uXJA73YVxKPBR3SzY+iQy4dk0vSyqUcGkuQBjUNQew== dependencies: babel-runtime "6.26.0" fibers "~2.0.0" @@ -10018,7 +10031,6 @@ wdio-sync@0.7.1: webdriverio@4.14.1: version "4.14.1" resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-4.14.1.tgz#50fdb010d37233c77c48e5f0497a63ab875cdfc1" - integrity sha512-Gjb5ft6JtO7WdoZifedeM6U941UZi03IlG0t3Xq9M9SxSm6FuyqMEmNZ4HI3UcBRkSbWxdOWGAvpFShYxVr7iA== dependencies: archiver "~2.1.0" babel-runtime "^6.26.0" @@ -10326,7 +10338,6 @@ xtend@~2.1.1: xterm@3.9.2: version "3.9.2" resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.9.2.tgz#e94bfbb84217b19bc1c16ed43d303b8245c9313d" - integrity sha512-fpQJQFTosY97EK4eB7UOrlFAwwqv1rSqlXgttEVD0S1v4MlevsUkRwrM/ew5X73jQXc+vdglRtccIhcXg5wtGg== y18n@^3.2.1: version "3.2.1"