/** * Copyright (c) 2013-2016 Angelo ZERR. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation */ package tern.eclipse.ide.linter.ui.properties; import java.io.File; import java.util.Collection; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.layout.TreeColumnLayout; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.viewers.ColumnWeightData; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.TreeViewerColumn; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; import tern.TernException; import tern.eclipse.ide.core.IWorkingCopy; import tern.eclipse.ide.core.IWorkingCopyListener; import tern.eclipse.ide.linter.core.ITernLinterConfig; import tern.eclipse.ide.linter.core.ITernLinterOption; import tern.eclipse.ide.linter.core.TernLinterCorePlugin; import tern.eclipse.ide.linter.internal.ui.TernLinterUIMessages; import tern.eclipse.ide.linter.internal.ui.TernLinterUIPlugin; import tern.eclipse.ide.linter.internal.ui.Trace; import tern.eclipse.ide.linter.ui.viewers.LinterConfigContentProvider; import tern.eclipse.ide.linter.ui.viewers.LinterConfigLabelProvider; import tern.eclipse.ide.linter.ui.viewers.LinterOptionEditingSupport; import tern.eclipse.ide.ui.controls.AbstractTreeBlock; import tern.eclipse.ide.ui.dialogs.OpenResourceDialog; import tern.server.ITernModule; import tern.server.ITernModuleConfigurable; import tern.utils.StringUtils; /** * Block which hosts the Tree of the Tern linter options. * */ public class TernLinterOptionsBlock extends AbstractTreeBlock implements IWorkingCopyListener { // JSON field for linter config. private static final String CONFIG_FILE_FIELD = "configFile"; private static final String CONFIG_FIELD = "config"; private final String linterId; private String linterConfigFilename; private final IWorkingCopy workingCopy; private final PreferencePage preferencePage; private TreeViewer treeViewer; private TernLinterOptionsPanel optionsPanel; private Button enableCheckbox; private Button useConfigFilesCheckbox; // Use config private Text linterConfigFileText; private Button projectBrowserButton; private Button workspaceBrowserButton; private Button filesystemBrowserButton; // Composite panel stack. private Composite contentPanel; private Composite configPage; private Composite configFilePage; public TernLinterOptionsBlock(String linterId, IWorkingCopy workingCopy, PreferencePage preferencePage) { this.linterId = linterId; this.workingCopy = workingCopy; this.preferencePage = preferencePage; this.linterConfigFilename = TernLinterCorePlugin.getDefault() .getTernLinterConfigurationsManager().getFilename(linterId); workingCopy.addWorkingCopyListener(this); } public Control createControl(Composite ancestor) { Composite parent = new Composite(ancestor, SWT.NONE); GridLayout layout = new GridLayout(); layout.numColumns = 1; layout.marginHeight = 0; layout.marginWidth = 0; parent.setLayout(layout); Font font = ancestor.getFont(); parent.setFont(font); createHeader(parent); createSeparator(parent); this.contentPanel = createBody(parent); // Display page displayPage(); Dialog.applyDialogFont(parent); return parent; } protected void createSeparator(Composite parent) { Label line = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); line.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_FILL)); } private void createHeader(Composite parent) { Composite header = new Composite(parent, SWT.NONE); GridData data = new GridData(GridData.FILL_HORIZONTAL); data.horizontalSpan = 2; header.setLayoutData(data); header.setLayout(new GridLayout(6, false)); // Create "Enable" checkbox enableCheckbox = new Button(header, SWT.CHECK); enableCheckbox .setText(TernLinterUIMessages.TernLinterOptionsBlock_enable); enableCheckbox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent event) { boolean checked = enableCheckbox.getSelection(); // Update UI updateEnabled(); try { // update working copy ITernModule module = workingCopy.getTernModule(linterId); if (module != null) { if (checked) { workingCopy.getCheckedModules().add(module); } else { workingCopy.getCheckedModules().remove(module); } } } catch (TernException e) { Trace.trace(Trace.SEVERE, "Error while updating working copy", e); } } }); if (canUseConfigFiles()) { // Create "Use config files" checkbox useConfigFilesCheckbox = new Button(header, SWT.CHECK); useConfigFilesCheckbox .setText(TernLinterUIMessages.TernLinterOptionsBlock_useConfigFiles); useConfigFilesCheckbox.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { displayPage(); } }); } // Update "Enable" checkbox according if the linter exists in the // .tern-project. enableCheckbox.setSelection(hasLinter()); } protected boolean canUseConfigFiles() { return !StringUtils.isEmpty(linterConfigFilename); } private void displayPage() { ((StackLayout) contentPanel.getLayout()).topControl = isUseConfigFiles() ? configFilePage : configPage; contentPanel.layout(); } private void updateEnabled() { Boolean checked = enableCheckbox.getSelection(); if (useConfigFilesCheckbox != null) { useConfigFilesCheckbox.setEnabled(checked); } if (linterConfigFileText != null) { linterConfigFileText.setEnabled(checked); projectBrowserButton.setEnabled(checked); workspaceBrowserButton.setEnabled(checked); filesystemBrowserButton.setEnabled(checked); } if (optionsPanel != null) { optionsPanel.updateEnabled(checked); } treeViewer.getTree().setEnabled(checked); } protected Composite createBody(Composite parent) { Composite contentPanel = new Composite(parent, SWT.NONE); GridData data = new GridData(GridData.FILL_HORIZONTAL); data.heightHint = 400; contentPanel.setLayoutData(data); StackLayout layout = new StackLayout(); contentPanel.setLayout(layout); this.configPage = createConfigPage(contentPanel); if (canUseConfigFiles()) { this.configFilePage = createConfigFilePage(contentPanel); } return contentPanel; } // -------------------- Config Page /** * * @param parent */ protected Composite createConfigPage(Composite parent) { SashForm sashForm = new SashForm(parent, SWT.HORIZONTAL | SWT.SMOOTH); GridData data = new GridData(GridData.FILL_HORIZONTAL); sashForm.setLayoutData(data); createOptionsMaster(sashForm); createOptionsDetails(sashForm); return sashForm; } /** * Create tree of tern linter config options. * * @param ancestor */ private void createOptionsMaster(Composite ancestor) { Composite parent = new Composite(ancestor, SWT.NONE); parent.setLayoutData(new GridData(GridData.FILL_BOTH)); TreeColumnLayout layout = new TreeColumnLayout(); parent.setLayout(layout); Font font = ancestor.getFont(); parent.setFont(font); // Create Tree treeViewer = new TreeViewer(parent, SWT.BORDER | SWT.FULL_SELECTION | SWT.V_SCROLL | SWT.MULTI); Tree tree = treeViewer.getTree(); tree.setHeaderVisible(false); tree.setLinesVisible(true); GridData data = new GridData(GridData.FILL_BOTH); data.minimumHeight= 400; tree.setLayoutData(data); treeViewer .setContentProvider(LinterConfigContentProvider.getInstance()); // Create label column TreeViewerColumn labelColumnViewer = new TreeViewerColumn(treeViewer, SWT.LEFT); labelColumnViewer.setLabelProvider(new TreeColumnViewerLabelProvider( LinterConfigLabelProvider.getInstance())); layout.setColumnData(labelColumnViewer.getColumn(), new ColumnWeightData(1, 150)); // Create value column TreeViewerColumn valueColumnViewer = new TreeViewerColumn(treeViewer, SWT.LEFT); valueColumnViewer.setLabelProvider(new TreeColumnViewerLabelProvider( LinterConfigLabelProvider.getInstance())); valueColumnViewer.setEditingSupport(new LinterOptionEditingSupport( valueColumnViewer.getViewer())); layout.setColumnData(valueColumnViewer.getColumn(), new ColumnWeightData(1, 100)); // Add tree selection listener to refresh the detail information of teh // selected option treeViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent e) { if (!e.getSelection().isEmpty()) { ITernLinterOption option = (ITernLinterOption) ((IStructuredSelection) e .getSelection()).getFirstElement(); refreshOption(option); } else { refreshOption(null); } } }); restoreColumnSettings(); } protected void refreshOption(ITernLinterOption option) { optionsPanel.refresh(option); } private void createOptionsDetails(Composite parent) { this.optionsPanel = new TernLinterOptionsPanel(parent, null); } // -------------------- Config File Page /** * * @param parent */ protected Composite createConfigFilePage(Composite parent) { Composite page = new Composite(parent, SWT.NONE); page.setLayoutData(new GridData(GridData.FILL_BOTH)); page.setLayout(new GridLayout(3, false)); createConfigFileText(page); createBrowseButtons(page); return page; } protected void createConfigFileText(Composite parent) { Composite container = new Composite(parent, SWT.NONE); container.setLayout(new GridLayout(2, false)); GridData data = new GridData(GridData.FILL_BOTH); data.verticalAlignment = SWT.BEGINNING; container.setLayoutData(data); Label linterConfigFileLabel = new Label(container, SWT.NONE); linterConfigFileLabel.setText(new StringBuilder(linterConfigFilename) .append(":").toString()); linterConfigFileText = new Text(container, SWT.BORDER); linterConfigFileText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { validate(); } }); linterConfigFileText.setLayoutData(new GridData( GridData.FILL_HORIZONTAL)); } private void createBrowseButtons(Composite parent) { Composite container = new Composite(parent, SWT.NONE); container.setLayout(new GridLayout(1, true)); container.setLayoutData(new GridData(GridData.FILL_VERTICAL)); final Shell shell = parent.getShell(); final IProject project = workingCopy .getProject().getProject(); // Browse files config of Project projectBrowserButton = new Button(container, SWT.NONE); projectBrowserButton.setText(TernLinterUIMessages.Button_browse_project); projectBrowserButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { OpenResourceDialog dialog = new OpenResourceDialog( shell, false, project, IResource.FILE); dialog.setInitialPattern(linterConfigFilename); if (dialog.open() != Window.OK) { return; } IResource result = (IResource) dialog.getFirstResult(); if (result != null && linterConfigFilename.equals(result.getName())) { linterConfigFileText.setText(result .getProjectRelativePath().toString()); } } }); projectBrowserButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); // Browse files config of Workspace workspaceBrowserButton = new Button(container, SWT.NONE); workspaceBrowserButton.setText(TernLinterUIMessages.Button_browse_workspace); workspaceBrowserButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { OpenResourceDialog dialog = new OpenResourceDialog( shell, false, ResourcesPlugin.getWorkspace().getRoot(), IResource.FILE); dialog.setInitialPattern(linterConfigFilename); if (dialog.open() != Window.OK) { return; } IResource result = (IResource) dialog.getFirstResult(); if (result != null && linterConfigFilename.equals(result.getName())) { linterConfigFileText.setText(result.getFullPath().makeRelativeTo(project.getFullPath()).toString()); } } }); // Browse files config of File System filesystemBrowserButton = new Button(container, SWT.NONE); filesystemBrowserButton.setText(TernLinterUIMessages.Button_browse_filesystem); filesystemBrowserButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { FileDialog dialog = new FileDialog(shell, SWT.NONE); dialog.setFileName(linterConfigFileText.getText()); dialog.setFileName(linterConfigFilename); String file = dialog.open(); if (file != null) { file = file.trim(); if (file.length() > 0) { linterConfigFileText.setText(file); } } } }); } public void setLinterConfig(ITernLinterConfig config) throws TernException { // load value for each option from the .tern-project if (workingCopy.hasCheckedTernModule(linterId)) { ITernModuleConfigurable module = (ITernModuleConfigurable) workingCopy .getTernModule(linterId); JsonObject jsonOptions = module.getOptionsObject(); if (jsonOptions != null) { JsonValue jsonConfig = jsonOptions.get(CONFIG_FIELD); if (jsonConfig != null && jsonConfig.isObject()) { // It's a config options updateConfig((JsonObject) jsonOptions.get(CONFIG_FIELD), config.getOptions()); } else { JsonValue jsonConfigFile = jsonOptions .get(CONFIG_FILE_FIELD); if (canUseConfigFiles() && jsonConfigFile != null && jsonConfigFile.isString()) { // it's a config file setConfigFile(jsonConfigFile.asString()); } } } } treeViewer.setInput(config); treeViewer.expandAll(); updateEnabled(); } private void setConfigFile(String confiFile) { linterConfigFileText.setText(confiFile); useConfigFilesCheckbox.setSelection(true); displayPage(); } private void updateConfig(JsonObject jsonOptions, Collection<ITernLinterOption> options) { for (ITernLinterOption option : options) { if (option.isCategoryType()) { updateConfig(jsonOptions, option.getOptions()); } else { String name = option.getId(); JsonValue jsonValue = jsonOptions.get(name); if (jsonValue != null) { if (jsonValue.isBoolean()) { option.setValue(jsonValue.asBoolean()); } else if (jsonValue.isNumber()) { option.setValue(jsonValue.asLong()); } else if (jsonValue.isString()) { option.setValue(jsonValue.asString()); } } } } } @Override protected Tree getTree() { return treeViewer.getTree(); } @Override protected IDialogSettings getDialogSettings() { return TernLinterUIPlugin.getDefault().getDialogSettings(); } @Override protected String getQualifier() { return TernLinterUIPlugin.PLUGIN_ID; } private boolean hasLinter() { return workingCopy.hasCheckedTernModule(linterId); } @Override public void moduleSelectionChanged(ITernModule module, boolean selected) { if (linterId.equals(module.getName())) { enableCheckbox.setSelection(selected); updateEnabled(); } } @Override public void dispose() { super.dispose(); workingCopy.removeWorkingCopyListener(this); } /** * Update the .tern-project : * * <ul> * <li>add tern linter in the .tern-project by updating JSON options.</li> * <li>or remove the tern linter</li> * </ul> * * @throws TernException */ public void updateTenProject() throws TernException { // To be sure that update of checked modules doesn't fire event. workingCopy.removeWorkingCopyListener(this); ITernModuleConfigurable module = (ITernModuleConfigurable) workingCopy .getTernModule(linterId); if (enableCheckbox.getSelection()) { // add tern linter + options in the .tern-project ITernLinterConfig config = (ITernLinterConfig) treeViewer .getInput(); JsonObject jsonOptions = new JsonObject(); if (isUseConfigFiles()) { // configFile : save the referenced file path linter config (eg // : .jshintrc, eslint.json) in the .tern-project. String configFile = linterConfigFileText.getText(); jsonOptions.add(CONFIG_FILE_FIELD, configFile); } else { // config : save the linter options inside the .tern-project JsonObject jsonConfig = new JsonObject(); jsonOptions.add(CONFIG_FIELD, jsonConfig); updateJSONOptions(config.getOptions(), jsonConfig); } module.setOptions(jsonOptions); } else { // remove the tern linter from the .tern-project. workingCopy.getCheckedModules().remove(module); } } private void updateJSONOptions(Collection<ITernLinterOption> options, JsonObject json) { for (ITernLinterOption option : options) { if (option.isCategoryType()) { updateJSONOptions(option.getOptions(), json); } else if (option.hasValue()) { if (option.isBooleanType()) { json.add(option.getId(), option.getBooleanValue()); } else if (option.isNumberType()) { json.add(option.getId(), option.getNumberValue()); } else { json.add(option.getId(), option.getStringValue()); } } } } private boolean isUseConfigFiles() { return useConfigFilesCheckbox != null && useConfigFilesCheckbox.getSelection(); } private void validate() { preferencePage.setErrorMessage(null); if (linterConfigFileText != null) { String fileConfig = linterConfigFileText.getText(); // Validate if file config is valid. if (!isProjectFile(fileConfig) && !(new File(fileConfig).exists())) { preferencePage.setErrorMessage(fileConfig + " is not a valid path."); } } } private boolean isProjectFile(String fileConfig) { try { IProject project = workingCopy .getProject().getProject(); return project.getFile(fileConfig).exists(); } catch(Throwable e) { return false; } } }