Thom1729 / Sublime-JS-Custom

Customizable JavaScript syntax highlighting for Sublime Text.
MIT License
137 stars 9 forks source link

Broken syntax highlighting with React.forwardRef and generics #125

Closed 2Pacalypse- closed 3 years ago

2Pacalypse- commented 3 years ago

I don't really know what's the issue here and how to make the minimum reproducible example, but removing the generics argument here fixes the styling:

image

Version: 4.1.0 Sublime Text 4, Build 4113

Thom1729 commented 3 years ago

Can you post the complete text of the file up to the point of the screenshot, and also your preferences?

2Pacalypse- commented 3 years ago

Sure, here you go:

import React, { useCallback, useMemo } from 'react'
import styled from 'styled-components'
import { amberA400, colorDividers, colorTextFaint, colorTextSecondary } from '../styles/colors'
import { buttonText, singleLine } from '../styles/typography'
import { useButtonState } from './button'
import { buttonReset } from './button-reset'
import { Ripple } from './ripple'

const Container = styled.div`
  position: relative;
  height: 48px;
  margin: 0;
  /* 36px + 12px = 48px total height */
  padding: 6px 24px;

  display: flex;
  flex-direction: row;
  contain: content;
`

export const TabTitle = styled.span`
  ${buttonText};
  ${singleLine};
`

export const TabItemContainer = styled.button<{ $isActiveTab: boolean }>`
  ${buttonReset};

  flex: 1 1 auto;
  min-width: 64px;
  height: 36px;

  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;

  padding: 0 16px;

  color: ${props => (props.$isActiveTab ? amberA400 : colorTextSecondary)};
  background-color: ${props => (props.$isActiveTab ? 'rgba(255, 255, 255, 0.08)' : 'transparent')};
  border-radius: 4px;
  transition: background-color 15ms linear, color 15ms linear;

  &:disabled {
    color: ${colorTextFaint};
    background-color: transparent;
  }
`

export const TabSpacer = styled.div`
  height: 1px;
  min-width: 0px;
  max-width: 24px;

  flex: 1 1 0;
`

const BottomDivider = styled.div`
  position: absolute;
  height: 1px;
  bottom: 0;
  left: 0;
  right: 0;

  background-color: ${colorDividers};
`

export interface TabItemProps<T> {
  text: string
  value: T
  disabled?: boolean
  className?: string
  /**
   * Whether or not the tab is the active one. This will be set by the containing Tabs component and
   * should not be passed directly.
   */
  active?: boolean
  /**
   * Called whenever this tab is selected. This will be set by the containing Tabs component and
   * should not be passed directly.
   */
  onSelected?: (value: T) => void
}

export const TabItem = React.memo(
  React.forwardRef(
    <T,>(
      { text, value, active, disabled, onSelected, className }: TabItemProps<T>,
      ref: React.ForwardedRef<HTMLButtonElement>,
    ) => {
      const onClick = useCallback(() => {
        if (!disabled && onSelected) {
          onSelected(value)
        }
      }, [disabled, value, onSelected])
      const [buttonProps, rippleRef] = useButtonState({
        disabled,
        onClick,
      })

      return (
        <TabItemContainer
          ref={ref}
          className={className}
          $isActiveTab={active ?? false}
          title={text}
          {...buttonProps}>
          <TabTitle>{text}</TabTitle>
          <Ripple ref={rippleRef} disabled={disabled} />
        </TabItemContainer>
      )
    },
  ),
)

export interface TabsProps<T> {
  children: ReturnType<typeof TabItem>[]
  activeTab: T
  onChange?: (value: T) => void
  bottomDivider?: boolean
  className?: string
}

export function Tabs<T>({ children, activeTab, onChange, bottomDivider, className }: TabsProps<T>) {
  const tabElems = useMemo(() => {
    const tabs = React.Children.map(children, (child, i) => {
      const isActive = activeTab === (child!.props as TabItemProps<T>).value
      return React.cloneElement(child!, {
        key: `tab-${i}`,
        active: isActive,
        onSelected: onChange,
      })
    })

    const tabElems: React.ReactNode[] = []
    for (let i = 0; i < tabs!.length; i++) {
      tabElems.push(tabs![i])
      tabElems.push(<TabSpacer key={`spacer-${i}`} />)
    }
    // Remove the last spacer since we don't want spacers on the outside
    tabElems.pop()
    return tabElems
  }, [activeTab, children, onChange])

  return (
    <Container className={className}>
      {tabElems}
      {bottomDivider ? <BottomDivider /> : null}
    </Container>
  )
}

Preferences:

{
    // Each configuration will be compiled into a custom syntax definition.
    // The keys are the names of the configurations,
    // and the values are objects specifying syntax options.
    "configurations": {
        "Default": {},
        "React": {
            "file_extensions": [ "js", "jsx" ],
            "flow_types": true,
            "jsx": true,
            "custom_templates": {
                "styled_components": true,
                "tags": {
                    "css": "scope:source.js.css",
                    "createGlobalStyle": "scope:source.css", // v4 and above
                    "injectGlobal": "scope:source.css", // before v4
                }
            }
        },
        "TypeScript": {
            "scope": "source.ts",
            "file_extensions": [ "ts" ],
            "typescript": true,
            "custom_templates": {
                "styled_components": true,
                "tags": {
                    "css": "scope:source.js.css",
                    "createGlobalStyle": "scope:source.css", // v4 and above
                    "injectGlobal": "scope:source.css", // before v4
                }
            }
        },
        "TypeScript (JSX)": {
            "scope": "source.tsx",
            "file_extensions": [ "tsx" ],
            "typescript": true,
            "jsx": true,
            "custom_templates": {
                "styled_components": true,
                "tags": {
                    "css": "scope:source.js.css",
                    "createGlobalStyle": "scope:source.css", // v4 and above
                    "injectGlobal": "scope:source.css", // before v4
                }
            }
        },
    },

    // These options will be used for all of your configurations, unless you override them.
    "defaults": {
        "custom_template_tags": false,
        "flow_types": false,
        "jsx": false,
    },

    // A special configuration that will be used when other syntaxes embed the `source.js` scope.
    // This exists to prevent infinite embedding loops in certain situations.
    "embed_configuration": {
        "name": "JS Custom (Embedded)",
        "scope": "source.js",
        "hidden": true,
        "file_extensions": [],
        "custom_template_tags": false,
        "custom_templates": false,
    },

    // Whenever you change one or more configurations, automatically rebuild those configurations.
    "auto_build": true,

    // Whenever you run the close_tag command in these scopes, run jsx_close_tag instead.
    // Set to `false` to disable.
    "jsx_close_tag": "source.js, source.jsx, source.ts, source.tsx",

    // When you remove a configuration, reassign all views from that syntax to this one. 
    // Set to `false` to disable.
    "reassign_when_deleting": "scope:source.js",
}
Thom1729 commented 3 years ago

At first glance it looks like it's parsing the arrow function type parameters as a JSX element. I think this actually might not be supported yet. I probably need to add this to core.

Thom1729 commented 3 years ago

See https://github.com/sublimehq/Packages/pull/2923. I've verified that fix with your code.

Once that's merged, it will take a bit more work to integrate it, but it hopefully shouldn't be too long.

Thom1729 commented 3 years ago

Fixed in v4.2.0.