Hirse / brackets-outline-list

Extension for Brackets and Phoenix to display a list of the functions or definitions in the currently opened document.
MIT License
79 stars 30 forks source link

Support class properties in JavaScript #86

Open papigers opened 7 years ago

papigers commented 7 years ago

Hi, I'm writing a react project written with JSX and ES6, and I've got this error on every file I open, even the plain javascript files (without JSX). The error is in the title of this issue.

Here are some files for example:

export const OKAY = 1;

export const WARNING = 2;

export const ERROR = 3;

export const translate = {
  [OKAY]: 'OKAY',
  [WARNING]: 'WARNING',
  [ERROR]: 'ERROR',
};

and a more complex file:

import React from 'react';
import { latLngBounds, latLng } from 'leaflet';

import polygonIcon from 'img/toolbar-polygon.svg';
import searchIcon from 'img/toolbar-search.svg';
import focusIcon from 'img/pointer.svg';
import settingsIcon from 'img/toolbar-settings.svg';
import chatIcon from 'img/toolbar-chat.svg';
import rulerIcon from 'img/toolbar-ruler.svg';
import layersIcon from 'img/toolbar-layers.svg';

import * as MODE from 'constants/mode';
import {
  locationType,
  sensorType,
  ellipseType,
  hyperboleType,
  applicationPropType,
} from 'constants/propTypes';
import { ellipses as ellipsesConfig } from 'config';

import LayersContainer from 'containers/LayerContainer';
import ConfigContainer from 'containers/ConfigContainer';

import s from './Toolbar.css';

function getSensorBounds(sensor) {
  return latLngBounds([sensor.location]);
}

function getEllipseBounds(ellipse) {
  function getPointInCircle(offset) {
    const lat = ellipse.center.lat;
    const lon = ellipse.center.lng;

    const R = 6378137;
    const distance = (offset ? -1 : 1) * ellipse.semiMajor;

    const dLat = distance / R;
    const dLon = distance / (R * Math.cos((Math.PI * lat) / 180));

    const lat2 = lat + ((dLat * 180) / Math.PI);
    const lon2 = lon + ((dLon * 180) / Math.PI);

    return latLng(lat2, lon2);
  }

  return latLngBounds([getPointInCircle(), getPointInCircle(true)]);
}

function getHyperboleBounds(hyperbole) {
  return hyperbole.points.reduce((prev, current) => (
    prev.extend ?
      prev.extend(latLngBounds([
        latLng(current[0]),
        latLng(current[1]),
      ]))
    : latLngBounds([
      latLng(prev[0]),
      latLng(prev[1]),
    ])
  ));
}

export default class Toolbar extends React.Component {

  constructor() {
    super();
    this.state = {
      active: 0,
      searchComplete: [],
      search: '',
    };
  }

  componentWillUpdate(next) {
    if (!this.props.leafletMap && next.leafletMap) {
      next.leafletMap.on('click', () => this.setState({ active: 0 }));
    }
    if (this.props.leafletMap) {
      if (!next.mode) {
        this.cleanListeners();
      }
      else if (this.props.mode !== MODE.ANNOTATION && next.mode === MODE.ANNOTATION) {
        this.cleanListeners(true);
        this.props.leafletMap.on('click', this.addAnnotation);
      }
      else if (this.props.mode !== MODE.RULER && next.mode === MODE.RULER) {
        this.cleanListeners(true);
        this.props.leafletMap.on('click', this.rulerClick);
      }
      else if (this.props.mode !== MODE.RULER_CLICK && next.mode === MODE.RULER_CLICK) {
        this.cleanListeners(true);
        this.props.leafletMap.on('click', this.rulerEnd);
      }
      else if (this.props.mode !== MODE.POLYGON && next.mode === MODE.POLYGON) {
        this.cleanListeners(true);
        this.props.leafletMap.on('click', this.polygonClick);
      }
      else if (this.props.mode !== MODE.POLYGON_CLICK && next.mode === MODE.POLYGON_CLICK) {
        this.cleanListeners(true);
        this.props.leafletMap.on('click', this.polygonClick);
        this.props.leafletMap.on('contextmenu', this.polygonEnd);
      }
    }
  }

  setActiveTool = (i) => {
    const set = this.state.active === i ? 0 : i;
    this.setState({ active: set });
    if (i !== 0) {
      switch (this.props.mode) {
        case MODE.RULER:
          return this.props.startRuler();
        case MODE.RULER_CLICK:
          return this.rulerEnd();
        case MODE.ANNOTATION:
          return this.props.startAddAnnotation();
        case MODE.POLYGON:
        case MODE.POLYGON_CLICK:
          return this.polygonEnd();
        default:
      }
    }
    return null;
  }

  cleanListeners = (disableTool) => {
    this.props.leafletMap.off('click', this.addAnnotation);
    this.props.leafletMap.off('click', this.rulerClick);
    this.props.leafletMap.off('click', this.rulerEnd);
    this.props.leafletMap.off('click', this.polygonClick);
    this.props.leafletMap.off('contextmenu', this.polygonEnd);
    if (disableTool) {
      this.setActiveTool(0);
    }
  }

  rulerClick = (event) => {
    this.props.clickRuler(event.latlng);
  }

  rulerEnd = () => {
    this.props.endRuler();
  }

  polygonClick = (event) => {
    this.props.clickPolygon(event.latlng);
  }

  polygonEnd = (event) => {
    this.props.endPolygon(event && event.latlng);
  }

  addAnnotation = (event) => {
    this.props.finishAddAnnotation(event.latlng);
  }

  searchShapes = (eventOrValue) => {
    const value = typeof eventOrValue === 'string' ?
          eventOrValue : eventOrValue.target.value;
    const lowerCaseValue = value.toLowerCase();
    if (!value.length) {
      return this.setState({
        searchComplete: [],
        search: value,
      });
    }
    const { sensors, ellipses, hyperboles } = this.props.search;
    const filteredSensorKeys = Object.keys(sensors).filter(
      sensorName => `${sensorName}`.toLowerCase().indexOf(lowerCaseValue) !== -1,
    );
    const filteredEllipseKeys = Object.keys(ellipses).filter(
      id => `Target ${id}`.toLowerCase().indexOf(lowerCaseValue) !== -1 ||
        (ellipsesConfig.targetId[id] &&
         ellipsesConfig.targetId[id].name.toLowerCase().indexOf(lowerCaseValue) !== -1),
    );
    const filteredHyperboleKeys = Object.keys(hyperboles).filter(
      id => `Target ${id}`.toLowerCase().indexOf(lowerCaseValue) !== -1,
    );

    function targetIdStringify(id) {
      return `Target ${id}`;
    }

    function ellipseTargetIdStringify(id) {
      return `Target ${id}${ellipsesConfig.targetId[id] && ellipsesConfig.targetId[id].name ? `: ${ellipsesConfig.targetId[id].name}` : ''}`;
    }

    const searchComplete = filteredSensorKeys
    .concat(filteredEllipseKeys.map(ellipseTargetIdStringify))
    .concat(filteredHyperboleKeys.map(targetIdStringify));

    if (searchComplete.length) {
      let bounds = filteredSensorKeys.map(key => getSensorBounds(sensors[key]));
      bounds = bounds.concat(filteredEllipseKeys.map(key => getEllipseBounds(ellipses[key])));
      bounds = bounds.concat(filteredHyperboleKeys.map(key => getHyperboleBounds(hyperboles[key])));
      const bound = bounds.reduce((prev, curr) => prev.extend(curr));

      this.props.leafletMap.fitBounds(bound, {
        padding: [75, 75],
      });
    }

    return this.setState({
      searchComplete,
      search: value,
    });
  }

  clearSearch = () => {
    const { sensors, ellipses, hyperboles } = this.props.search;
    let bounds = Object.keys(sensors).map(key => getSensorBounds(sensors[key]));
    bounds = bounds.concat(Object.keys(ellipses).map(key => getEllipseBounds(ellipses[key])));
    bounds = bounds.concat(Object.keys(hyperboles).map(key => getHyperboleBounds(hyperboles[key])));
    const bound = bounds.reduce((prev, curr) => prev.extend(curr));

    this.props.leafletMap.fitBounds(bound, {
      padding: [100, 100],
    });

    this.setState({
      active: 0,
      searchComplete: [],
      search: '',
    });
  }

  render() {
    const {
      mode,
      startAddAnnotation,
      startRuler,
      startPolygon,
      polygon,
      applicationType,
    } = this.props;

    const { active, searchComplete, search } = this.state;
    return (
      <div className={s.toolbar}>
        <div className={`${s.buttonBox} ${s.searchBox} ${active === 1 ? s.active : ''}`}>
          <img src={searchIcon} alt="Search" title="Search" className={s.icon} onClick={() => this.setActiveTool(1)} />
          <div className={s.close} onClick={this.clearSearch}>×</div>
          <input
            type="text"
            placeholder="Search"
            value={search}
            onChange={this.searchShapes}
          />
        </div>
        <div className={`${s.buttonBox} ${s.poppingBox}`}>
          <img src={focusIcon} alt="Focus Center" title="Focus Center" className={`${s.icon} ${s.focus}`} onClick={this.clearSearch} />
        </div>
        {applicationType !== 'DRONES' ?
          <div className={`${s.buttonBox} ${s.poppingBox} ${active === 2 ? s.active : ''}`}>
            <img src={settingsIcon} alt="Configuration" title="Configuration" className={s.icon} onClick={() => this.setActiveTool(2)} />
            <h2>Configuration</h2>
            <ConfigContainer />
          </div>
          : null
        }
        <div className={`${s.buttonBox} ${s.poppingBox} ${s.layers} ${active === 3 ? s.active : ''}`}>
          <img src={layersIcon} alt="Layers" title="Layers" className={s.icon} onClick={() => this.setActiveTool(3)} />
          <h2>Layers</h2>
          <LayersContainer />
        </div>
        <div className={s.buttonBox}>
          <div className={s.seperator} />
          <img
            src={rulerIcon}
            alt="Measure Distance"
            title="Measure Distance"
            className={`${s.icon} ${mode === MODE.RULER || mode === MODE.RULER_CLICK ? s.active : ''}`}
            onClick={startRuler}
          />
          <img
            src={chatIcon}
            alt="Add Annotation"
            title="Add Annotation"
            className={`${s.icon} ${mode === MODE.ANNOTATION ? s.active : ''}`}
            onClick={startAddAnnotation}
          />
          <img
            src={polygonIcon}
            alt="Draw Shape"
            title="Draw Shape"
            className={`${s.icon} ${mode === MODE.POLYGON || mode === MODE.POLYGON_CLICK ? s.active : ''}`}
            onClick={mode === MODE.POLYGON || mode === MODE.POLYGON_CLICK ?
              this.polygonEnd : startPolygon}
          />
        </div>
        {polygon.length >= 2 ?
          <div className={s.polygonMessage}>
            <h5>Right click to finish drawing</h5>
          </div>
          : null
        }
        {searchComplete.length && active ?
          <ul className={s.searchCompleteList}>
            {
              searchComplete.map(
                (complete, i) =>
                  <li
                    key={i}
                    onClick={() => this.searchShapes(complete)}
                  >
                    {complete}
                  </li>,
              )
            }

          </ul>
          : null
        }
      </div>
    );
  }
}

Toolbar.propTypes = {
  leafletMap: React.PropTypes.shape(),
  startAddAnnotation: React.PropTypes.func.isRequired,
  finishAddAnnotation: React.PropTypes.func.isRequired,
  startRuler: React.PropTypes.func.isRequired,
  clickRuler: React.PropTypes.func.isRequired,
  endRuler: React.PropTypes.func.isRequired,
  startPolygon: React.PropTypes.func.isRequired,
  clickPolygon: React.PropTypes.func.isRequired,
  endPolygon: React.PropTypes.func.isRequired,
  polygon: React.PropTypes.arrayOf(locationType),
  mode: React.PropTypes.string,
  search: React.PropTypes.shape({
    sensors: React.PropTypes.objectOf(sensorType),
    ellipses: React.PropTypes.objectOf(ellipseType),
    hyperboles: React.PropTypes.objectOf(hyperboleType),
  }),
  applicationType: applicationPropType.isRequired,
};

Both with the same error, as most of the other files. Any idea how to fix this?

Hirse commented 7 years ago

It seems like I missed a parser flag. When using import and export statements, sourceType has to be set to "module" in Espree.

I have to check if setting that value brings unwanted side-effects, but until then, you could do the change manually to keep using the extension: (https://github.com/Hirse/brackets-outline-list/blob/master/src/lexers/JSParser.js#L140)

papigers commented 7 years ago

It solved the error for the first, simpler files, but it still seems to have a problem with the other one, or, for what I've seen, jsx files.

Hirse commented 7 years ago

I didn't check your second example before. Is that one of the files still having problems?

papigers commented 7 years ago

Yes, as well as other jsx files (maybe all, but I didn't check).

papigers commented 7 years ago

ok, so I've came across this file in my project, with which the extension has no proble:

import React from 'react';
import { DivIcon } from 'leaflet';
import { Marker, Popup } from 'react-leaflet';

import { sensorType } from 'constants/propTypes';
import * as STATE from 'constants/sensorStates';
import { translate as healthTranslate } from 'constants/health';

import prettifyConstant from 'utils/prettifyConstant';

import s from './SensorMarker.css';

export default class SensorMarker extends React.Component {
  render() {
    const { sensor, hideSensor, resetSensor } = this.props;

    return (
      <Marker
        key={sensor.sensorName}
        position={[sensor.location.lat, sensor.location.lng]}
        icon={new DivIcon({
          iconSize: [30, 30],
          className: `${s.marker} ${sensor.active ? s.active : ''} ${sensor.sensorState === STATE.RESET || sensor.sensorState === STATE.INIT ? s.loading : ''} ${sensor.mobile ? s.mobile : s.satellite} ${s[healthTranslate[sensor.health].toLowerCase()]}`,
        })}
        ref={(marker) => {
          this.marker = marker;
        }}
      >
        <Popup>
          <div>
            <div className={s.header}>
              <h3>{sensor.sensorName}</h3>
            </div>
            <div className={s.stats}>
              <div>
                <h4>Location:</h4>
                <span>{` ${sensor.location.lng} Lng, ${sensor.location.lat} Lat, ${sensor.location.alt} Alt`}</span>
              </div>
              <div>
                <h4>State:</h4>
                <span>{prettifyConstant(STATE.translate[sensor.sensorState])}</span>
              </div>
              <div>
                <h4>Health:</h4>
                <span>{prettifyConstant(healthTranslate[sensor.health])}</span>
              </div>
              <div>
                <h4>Health Info:</h4>
                <span>{sensor.healthInfo}</span>
              </div>
              <div>
                <h4>Comm Quality:</h4>
                <span>{sensor.commQuality}</span>
              </div>
              <div>
                <h4>Signal Quality:</h4>
                <span>{sensor.signalQuality}</span>
              </div>
              <div>
                <h4>GPS Quality:</h4>
                <span>{sensor.gpsQuality}</span>
              </div>
            </div>
            <div className={s.buttons}>
              <button onClick={() => hideSensor(sensor.sensorName)}>Hide</button>
              {sensor.sensorState !== STATE.ACTIVE && sensor.sensorState !== STATE.NOT_AVAILABLE ?
                <button
                  onClick={() => {
                    if (this.marker) {
                      this.marker.leafletElement.closePopup();
                    }
                    resetSensor(sensor.sensorName);
                  }}
                >
                  Restart
                </button>
                : null
              }
            </div>
          </div>
        </Popup>
      </Marker>
    );
  }
}

SensorMarker.propTypes = {
  sensor: sensorType.isRequired,
  hideSensor: React.PropTypes.func.isRequired,
  resetSensor: React.PropTypes.func.isRequired,
};
import React from 'react';
import { DivIcon } from 'leaflet';
import { Marker, Popup } from 'react-leaflet';

import { sensorType } from 'constants/propTypes';
import * as STATE from 'constants/sensorStates';
import { translate as healthTranslate } from 'constants/health';

import prettifyConstant from 'utils/prettifyConstant';

import s from './SensorMarker.css';

export default class SensorMarker extends React.Component {
  render() {
    const { sensor, hideSensor, resetSensor } = this.props;

    return (
      <Marker
        key={sensor.sensorName}
        position={[sensor.location.lat, sensor.location.lng]}
        icon={new DivIcon({
          iconSize: [30, 30],
          className: `${s.marker} ${sensor.active ? s.active : ''} ${sensor.sensorState === STATE.RESET || sensor.sensorState === STATE.INIT ? s.loading : ''} ${sensor.mobile ? s.mobile : s.satellite} ${s[healthTranslate[sensor.health].toLowerCase()]}`,
        })}
        ref={(marker) => {
          this.marker = marker;
        }}
      >
        <Popup>
          <div>
            <div className={s.header}>
              <h3>{sensor.sensorName}</h3>
            </div>
            <div className={s.stats}>
              <div>
                <h4>Location:</h4>
                <span>{` ${sensor.location.lng} Lng, ${sensor.location.lat} Lat, ${sensor.location.alt} Alt`}</span>
              </div>
              <div>
                <h4>State:</h4>
                <span>{prettifyConstant(STATE.translate[sensor.sensorState])}</span>
              </div>
              <div>
                <h4>Health:</h4>
                <span>{prettifyConstant(healthTranslate[sensor.health])}</span>
              </div>
              <div>
                <h4>Health Info:</h4>
                <span>{sensor.healthInfo}</span>
              </div>
              <div>
                <h4>Comm Quality:</h4>
                <span>{sensor.commQuality}</span>
              </div>
              <div>
                <h4>Signal Quality:</h4>
                <span>{sensor.signalQuality}</span>
              </div>
              <div>
                <h4>GPS Quality:</h4>
                <span>{sensor.gpsQuality}</span>
              </div>
            </div>
            <div className={s.buttons}>
              <button onClick={() => hideSensor(sensor.sensorName)}>Hide</button>
              {sensor.sensorState !== STATE.ACTIVE && sensor.sensorState !== STATE.NOT_AVAILABLE ?
                <button
                  onClick={() => {
                    if (this.marker) {
                      this.marker.leafletElement.closePopup();
                    }
                    resetSensor(sensor.sensorName);
                  }}
                >
                  Restart
                </button>
                : null
              }
            </div>
          </div>
        </Popup>
      </Marker>
    );
  }
}

SensorMarker.propTypes = {
  sensor: sensorType.isRequired,
  hideSensor: React.PropTypes.func.isRequired,
  resetSensor: React.PropTypes.func.isRequired,
};
papigers commented 7 years ago

After some checks, it seems that the error comes from class properties. For example:

rulerEnd = () => {
    this.props.endRuler();
  }

Any idea how this can be supported as well?

Hirse commented 7 years ago

The Espree parser this extension is using doesn't support class properties and probably isn't going to as the are not in the standard yet. I guess the only way to support it would be to change to a different parser.