mui 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +18 -10
- data/CHANGELOG.md +162 -0
- data/README.md +309 -6
- data/docs/_config.yml +56 -0
- data/docs/configuration.md +314 -0
- data/docs/getting-started.md +140 -0
- data/docs/index.md +55 -0
- data/docs/jobs.md +297 -0
- data/docs/keybindings.md +229 -0
- data/docs/plugins.md +285 -0
- data/docs/syntax-highlighting.md +155 -0
- data/lib/mui/color_manager.rb +140 -6
- data/lib/mui/color_scheme.rb +1 -0
- data/lib/mui/command_completer.rb +11 -2
- data/lib/mui/command_history.rb +89 -0
- data/lib/mui/command_line.rb +32 -2
- data/lib/mui/command_registry.rb +21 -2
- data/lib/mui/config.rb +3 -1
- data/lib/mui/editor.rb +90 -2
- data/lib/mui/floating_window.rb +53 -1
- data/lib/mui/handler_result.rb +13 -7
- data/lib/mui/highlighters/search_highlighter.rb +2 -1
- data/lib/mui/highlighters/syntax_highlighter.rb +4 -1
- data/lib/mui/insert_completion_state.rb +15 -2
- data/lib/mui/key_handler/base.rb +87 -0
- data/lib/mui/key_handler/command_mode.rb +68 -0
- data/lib/mui/key_handler/insert_mode.rb +10 -41
- data/lib/mui/key_handler/normal_mode.rb +24 -51
- data/lib/mui/key_handler/operators/paste_operator.rb +9 -3
- data/lib/mui/key_handler/search_mode.rb +10 -7
- data/lib/mui/key_handler/visual_mode.rb +15 -10
- data/lib/mui/key_notation_parser.rb +159 -0
- data/lib/mui/key_sequence.rb +67 -0
- data/lib/mui/key_sequence_buffer.rb +85 -0
- data/lib/mui/key_sequence_handler.rb +163 -0
- data/lib/mui/key_sequence_matcher.rb +79 -0
- data/lib/mui/line_renderer.rb +52 -1
- data/lib/mui/mode_manager.rb +3 -2
- data/lib/mui/screen.rb +30 -6
- data/lib/mui/search_state.rb +74 -27
- data/lib/mui/syntax/language_detector.rb +33 -1
- data/lib/mui/syntax/lexers/c_lexer.rb +2 -0
- data/lib/mui/syntax/lexers/css_lexer.rb +121 -0
- data/lib/mui/syntax/lexers/go_lexer.rb +207 -0
- data/lib/mui/syntax/lexers/html_lexer.rb +118 -0
- data/lib/mui/syntax/lexers/javascript_lexer.rb +219 -0
- data/lib/mui/syntax/lexers/markdown_lexer.rb +210 -0
- data/lib/mui/syntax/lexers/ruby_lexer.rb +3 -0
- data/lib/mui/syntax/lexers/rust_lexer.rb +150 -0
- data/lib/mui/syntax/lexers/typescript_lexer.rb +225 -0
- data/lib/mui/terminal_adapter/base.rb +21 -0
- data/lib/mui/terminal_adapter/curses.rb +37 -11
- data/lib/mui/themes/default.rb +263 -132
- data/lib/mui/version.rb +1 -1
- data/lib/mui/window.rb +105 -39
- data/lib/mui/window_manager.rb +7 -0
- data/lib/mui/wrap_cache.rb +40 -0
- data/lib/mui/wrap_helper.rb +84 -0
- data/lib/mui.rb +15 -0
- metadata +26 -3
data/lib/mui/screen.rb
CHANGED
|
@@ -25,13 +25,19 @@ module Mui
|
|
|
25
25
|
@adapter.clear
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Force a complete redraw of the screen
|
|
29
|
+
# This is needed when multibyte characters may have been corrupted
|
|
30
|
+
def touchwin
|
|
31
|
+
@adapter.touchwin
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
def put(y, x, text)
|
|
29
35
|
return if y.negative?
|
|
30
36
|
return if y >= @height || x >= @width
|
|
31
37
|
|
|
32
38
|
@adapter.setpos(y, x)
|
|
33
|
-
|
|
34
|
-
@adapter.addstr(text
|
|
39
|
+
max_width = @width - x
|
|
40
|
+
@adapter.addstr(truncate_to_width(text, max_width))
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
def put_with_highlight(y, x, text)
|
|
@@ -39,9 +45,9 @@ module Mui
|
|
|
39
45
|
return if y >= @height || x >= @width
|
|
40
46
|
|
|
41
47
|
@adapter.setpos(y, x)
|
|
42
|
-
|
|
48
|
+
max_width = @width - x
|
|
43
49
|
@adapter.with_highlight do
|
|
44
|
-
@adapter.addstr(text
|
|
50
|
+
@adapter.addstr(truncate_to_width(text, max_width))
|
|
45
51
|
end
|
|
46
52
|
end
|
|
47
53
|
|
|
@@ -51,8 +57,8 @@ module Mui
|
|
|
51
57
|
return put(y, x, text) unless @color_manager && style
|
|
52
58
|
|
|
53
59
|
@adapter.setpos(y, x)
|
|
54
|
-
|
|
55
|
-
truncated_text = text
|
|
60
|
+
max_width = @width - x
|
|
61
|
+
truncated_text = truncate_to_width(text, max_width)
|
|
56
62
|
|
|
57
63
|
pair_index = ensure_color_pair(style[:fg], style[:bg])
|
|
58
64
|
@adapter.with_color(pair_index, bold: style[:bold], underline: style[:underline]) do
|
|
@@ -81,5 +87,23 @@ module Mui
|
|
|
81
87
|
@width = @adapter.width
|
|
82
88
|
@height = @adapter.height
|
|
83
89
|
end
|
|
90
|
+
|
|
91
|
+
# Truncates text to fit within max_width display columns
|
|
92
|
+
def truncate_to_width(text, max_width)
|
|
93
|
+
return text if max_width <= 0
|
|
94
|
+
|
|
95
|
+
current_width = 0
|
|
96
|
+
result = String.new
|
|
97
|
+
|
|
98
|
+
text.each_char do |char|
|
|
99
|
+
char_w = UnicodeWidth.char_width(char)
|
|
100
|
+
break if current_width + char_w > max_width
|
|
101
|
+
|
|
102
|
+
result << char
|
|
103
|
+
current_width += char_w
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
result
|
|
107
|
+
end
|
|
84
108
|
end
|
|
85
109
|
end
|
data/lib/mui/search_state.rb
CHANGED
|
@@ -2,87 +2,134 @@
|
|
|
2
2
|
|
|
3
3
|
module Mui
|
|
4
4
|
class SearchState
|
|
5
|
-
attr_reader :pattern, :direction
|
|
5
|
+
attr_reader :pattern, :direction
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
8
|
@pattern = nil
|
|
9
9
|
@direction = :forward
|
|
10
|
-
@
|
|
10
|
+
@pattern_version = 0
|
|
11
|
+
@buffer_matches = {} # { buffer_object_id => { version:, matches: [] } }
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def set_pattern(pattern, direction)
|
|
14
15
|
@pattern = pattern
|
|
15
16
|
@direction = direction
|
|
16
|
-
@
|
|
17
|
+
@pattern_version += 1
|
|
18
|
+
@buffer_matches.clear # Invalidate all cached matches
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
# Calculate matches for a specific buffer (used for n/N navigation)
|
|
19
22
|
def find_all_matches(buffer)
|
|
20
|
-
@
|
|
21
|
-
return if @pattern.nil? || @pattern.empty?
|
|
23
|
+
return [] if @pattern.nil? || @pattern.empty? || buffer.nil?
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
regex = Regexp.new(@pattern)
|
|
25
|
-
buffer.line_count.times do |row|
|
|
26
|
-
line = buffer.line(row)
|
|
27
|
-
scan_line_matches(line, row, regex)
|
|
28
|
-
end
|
|
29
|
-
rescue RegexpError
|
|
30
|
-
# Invalid regex pattern - no matches
|
|
31
|
-
@matches = []
|
|
32
|
-
end
|
|
25
|
+
get_or_calculate_matches(buffer)
|
|
33
26
|
end
|
|
34
27
|
|
|
35
|
-
def find_next(current_row, current_col)
|
|
36
|
-
|
|
28
|
+
def find_next(current_row, current_col, buffer: nil)
|
|
29
|
+
matches = buffer ? get_or_calculate_matches(buffer) : []
|
|
30
|
+
return nil if matches.empty?
|
|
37
31
|
|
|
38
32
|
# Find next match after current position
|
|
39
|
-
match =
|
|
33
|
+
match = matches.find do |m|
|
|
40
34
|
m[:row] > current_row || (m[:row] == current_row && m[:col] > current_col)
|
|
41
35
|
end
|
|
42
36
|
|
|
43
37
|
# Wrap around to beginning if no match found
|
|
44
|
-
match ||
|
|
38
|
+
match || matches.first
|
|
45
39
|
end
|
|
46
40
|
|
|
47
|
-
def find_previous(current_row, current_col)
|
|
48
|
-
|
|
41
|
+
def find_previous(current_row, current_col, buffer: nil)
|
|
42
|
+
matches = buffer ? get_or_calculate_matches(buffer) : []
|
|
43
|
+
return nil if matches.empty?
|
|
49
44
|
|
|
50
45
|
# Find previous match before current position
|
|
51
|
-
match =
|
|
46
|
+
match = matches.reverse.find do |m|
|
|
52
47
|
m[:row] < current_row || (m[:row] == current_row && m[:col] < current_col)
|
|
53
48
|
end
|
|
54
49
|
|
|
55
50
|
# Wrap around to end if no match found
|
|
56
|
-
match ||
|
|
51
|
+
match || matches.last
|
|
57
52
|
end
|
|
58
53
|
|
|
59
54
|
def clear
|
|
60
55
|
@pattern = nil
|
|
61
|
-
@
|
|
56
|
+
@pattern_version += 1
|
|
57
|
+
@buffer_matches.clear
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
def has_pattern?
|
|
65
61
|
!@pattern.nil? && !@pattern.empty?
|
|
66
62
|
end
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
# Get matches for a specific row in a specific buffer
|
|
65
|
+
# O(1) lookup using row_index
|
|
66
|
+
def matches_for_row(row, buffer: nil)
|
|
67
|
+
return [] if buffer.nil?
|
|
68
|
+
|
|
69
|
+
cache = get_or_calculate_cache(buffer)
|
|
70
|
+
cache[:row_index][row] || []
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
private
|
|
73
74
|
|
|
75
|
+
def get_or_calculate_matches(buffer)
|
|
76
|
+
get_or_calculate_cache(buffer)[:matches]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get_or_calculate_cache(buffer)
|
|
80
|
+
buffer_id = buffer.object_id
|
|
81
|
+
cached = @buffer_matches[buffer_id]
|
|
82
|
+
|
|
83
|
+
# Return cached data if valid (same pattern version and buffer hasn't changed)
|
|
84
|
+
return cached if cached && cached[:version] == @pattern_version && cached[:change_count] == buffer.change_count
|
|
85
|
+
|
|
86
|
+
# Calculate and cache matches for this buffer
|
|
87
|
+
matches, row_index = calculate_matches(buffer)
|
|
88
|
+
@buffer_matches[buffer_id] = {
|
|
89
|
+
version: @pattern_version,
|
|
90
|
+
change_count: buffer.change_count,
|
|
91
|
+
matches:,
|
|
92
|
+
row_index:
|
|
93
|
+
}
|
|
94
|
+
@buffer_matches[buffer_id]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def calculate_matches(buffer)
|
|
98
|
+
empty_result = [[], {}]
|
|
99
|
+
return empty_result if @pattern.nil? || @pattern.empty?
|
|
100
|
+
|
|
101
|
+
matches = []
|
|
102
|
+
row_index = {}
|
|
103
|
+
begin
|
|
104
|
+
regex = Regexp.new(@pattern)
|
|
105
|
+
buffer.line_count.times do |row|
|
|
106
|
+
line = buffer.line(row)
|
|
107
|
+
row_matches = scan_line_matches(line, row, regex)
|
|
108
|
+
unless row_matches.empty?
|
|
109
|
+
matches.concat(row_matches)
|
|
110
|
+
row_index[row] = row_matches
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
rescue RegexpError
|
|
114
|
+
# Invalid regex pattern - no matches
|
|
115
|
+
end
|
|
116
|
+
[matches, row_index]
|
|
117
|
+
end
|
|
118
|
+
|
|
74
119
|
def scan_line_matches(line, row, regex)
|
|
120
|
+
matches = []
|
|
75
121
|
offset = 0
|
|
76
122
|
while (match_data = line.match(regex, offset))
|
|
77
123
|
col = match_data.begin(0)
|
|
78
124
|
end_col = match_data.end(0) - 1
|
|
79
|
-
|
|
125
|
+
matches << { row:, col:, end_col: }
|
|
80
126
|
# Move offset past the end of the match to avoid overlapping matches
|
|
81
127
|
offset = match_data.end(0)
|
|
82
128
|
# Handle zero-length matches to prevent infinite loop
|
|
83
129
|
offset += 1 if match_data[0].empty?
|
|
84
130
|
break if offset >= line.length
|
|
85
131
|
end
|
|
132
|
+
matches
|
|
86
133
|
end
|
|
87
134
|
end
|
|
88
135
|
end
|
|
@@ -12,7 +12,25 @@ module Mui
|
|
|
12
12
|
".gemspec" => :ruby,
|
|
13
13
|
".c" => :c,
|
|
14
14
|
".h" => :c,
|
|
15
|
-
".y" => :c
|
|
15
|
+
".y" => :c,
|
|
16
|
+
".go" => :go,
|
|
17
|
+
".rs" => :rust,
|
|
18
|
+
".js" => :javascript,
|
|
19
|
+
".mjs" => :javascript,
|
|
20
|
+
".cjs" => :javascript,
|
|
21
|
+
".jsx" => :javascript,
|
|
22
|
+
".ts" => :typescript,
|
|
23
|
+
".tsx" => :typescript,
|
|
24
|
+
".mts" => :typescript,
|
|
25
|
+
".cts" => :typescript,
|
|
26
|
+
".md" => :markdown,
|
|
27
|
+
".markdown" => :markdown,
|
|
28
|
+
".html" => :html,
|
|
29
|
+
".htm" => :html,
|
|
30
|
+
".xhtml" => :html,
|
|
31
|
+
".css" => :css,
|
|
32
|
+
".scss" => :css,
|
|
33
|
+
".sass" => :css
|
|
16
34
|
}.freeze
|
|
17
35
|
|
|
18
36
|
# Map basenames (files without extension) to language symbols
|
|
@@ -50,6 +68,20 @@ module Mui
|
|
|
50
68
|
Lexers::RubyLexer.new
|
|
51
69
|
when :c
|
|
52
70
|
Lexers::CLexer.new
|
|
71
|
+
when :go
|
|
72
|
+
Lexers::GoLexer.new
|
|
73
|
+
when :rust
|
|
74
|
+
Lexers::RustLexer.new
|
|
75
|
+
when :javascript
|
|
76
|
+
Lexers::JavaScriptLexer.new
|
|
77
|
+
when :typescript
|
|
78
|
+
Lexers::TypeScriptLexer.new
|
|
79
|
+
when :markdown
|
|
80
|
+
Lexers::MarkdownLexer.new
|
|
81
|
+
when :html
|
|
82
|
+
Lexers::HtmlLexer.new
|
|
83
|
+
when :css
|
|
84
|
+
Lexers::CssLexer.new
|
|
53
85
|
end
|
|
54
86
|
end
|
|
55
87
|
|
|
@@ -30,6 +30,8 @@ module Mui
|
|
|
30
30
|
[:type, /\G\b(?:char|double|float|int|long|short|signed|unsigned|void|_Bool|_Complex|_Imaginary)\b/],
|
|
31
31
|
# Other keywords (if, for, return, const, static, etc.)
|
|
32
32
|
[:keyword, /\G\b(?:auto|break|case|const|continue|default|do|else|enum|extern|for|goto|if|register|return|sizeof|static|struct|switch|typedef|union|volatile|while|inline|restrict|_Alignas|_Alignof|_Atomic|_Generic|_Noreturn|_Static_assert|_Thread_local)\b/],
|
|
33
|
+
# Function call/definition (identifier followed by parenthesis)
|
|
34
|
+
[:function_definition, /\G\b[a-zA-Z_][a-zA-Z0-9_]*(?=\s*\()/],
|
|
33
35
|
# Identifiers
|
|
34
36
|
[:identifier, /\G\b[a-zA-Z_][a-zA-Z0-9_]*\b/],
|
|
35
37
|
# Operators
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
module Lexers
|
|
6
|
+
# Lexer for CSS source files
|
|
7
|
+
class CssLexer < LexerBase
|
|
8
|
+
# Pre-compiled patterns with \G anchor for position-specific matching
|
|
9
|
+
COMPILED_PATTERNS = [
|
|
10
|
+
# Single-line block comment /* ... */ on one line
|
|
11
|
+
[:comment, %r{\G/\*.*?\*/}],
|
|
12
|
+
# @rules (at-rules)
|
|
13
|
+
[:preprocessor, /\G@[a-zA-Z-]+/],
|
|
14
|
+
# Hex color (must be before ID selector - matches 3-8 hex digits only)
|
|
15
|
+
[:number, /\G#[0-9a-fA-F]{3,8}(?![a-zA-Z0-9_-])/],
|
|
16
|
+
# ID selector (starts with letter or underscore/hyphen after #)
|
|
17
|
+
[:constant, /\G#[a-zA-Z_-][a-zA-Z0-9_-]*/],
|
|
18
|
+
# Class selector
|
|
19
|
+
[:type, /\G\.[a-zA-Z_-][a-zA-Z0-9_-]*/],
|
|
20
|
+
# Pseudo-elements and pseudo-classes
|
|
21
|
+
[:keyword, /\G::?[a-zA-Z-]+(?:\([^)]*\))?/],
|
|
22
|
+
# Property name (followed by colon)
|
|
23
|
+
[:identifier, /\G[a-zA-Z-]+(?=\s*:)/],
|
|
24
|
+
# Double quoted string
|
|
25
|
+
[:string, /\G"(?:[^"\\]|\\.)*"/],
|
|
26
|
+
# Single quoted string
|
|
27
|
+
[:string, /\G'(?:[^'\\]|\\.)*'/],
|
|
28
|
+
# URL function
|
|
29
|
+
[:string, /\Gurl\([^)]*\)/i],
|
|
30
|
+
# Numbers with units
|
|
31
|
+
[:number, /\G-?\d+\.?\d*(?:px|em|rem|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc|deg|rad|grad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx|fr)?/i],
|
|
32
|
+
# Functions (calc, rgb, rgba, hsl, var, etc.)
|
|
33
|
+
[:keyword, /\G[a-zA-Z-]+(?=\()/],
|
|
34
|
+
# Property values / keywords (important, inherit, etc.)
|
|
35
|
+
[:constant, /\G!important\b/i],
|
|
36
|
+
[:constant, /\G\b(?:inherit|initial|unset|revert|none|auto|normal)\b/],
|
|
37
|
+
# Element selectors and identifiers
|
|
38
|
+
[:identifier, /\G[a-zA-Z_-][a-zA-Z0-9_-]*/],
|
|
39
|
+
# Operators and symbols
|
|
40
|
+
[:operator, /\G[{}():;,>+~*=\[\]]/]
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Multiline comment patterns
|
|
44
|
+
BLOCK_COMMENT_END = %r{\*/}
|
|
45
|
+
BLOCK_COMMENT_START = %r{/\*}
|
|
46
|
+
BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
|
|
47
|
+
|
|
48
|
+
protected
|
|
49
|
+
|
|
50
|
+
def compiled_patterns
|
|
51
|
+
COMPILED_PATTERNS
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Handle /* ... */ block comments that span multiple lines
|
|
55
|
+
def handle_multiline_state(line, pos, state)
|
|
56
|
+
return [nil, nil, pos] unless state == :block_comment
|
|
57
|
+
|
|
58
|
+
end_match = line[pos..].match(BLOCK_COMMENT_END)
|
|
59
|
+
if end_match
|
|
60
|
+
end_pos = pos + end_match.begin(0) + 1
|
|
61
|
+
text = line[pos..end_pos]
|
|
62
|
+
token = Token.new(
|
|
63
|
+
type: :comment,
|
|
64
|
+
start_col: pos,
|
|
65
|
+
end_col: end_pos,
|
|
66
|
+
text:
|
|
67
|
+
)
|
|
68
|
+
[token, nil, end_pos + 1]
|
|
69
|
+
else
|
|
70
|
+
text = line[pos..]
|
|
71
|
+
token = if text.empty?
|
|
72
|
+
nil
|
|
73
|
+
else
|
|
74
|
+
Token.new(
|
|
75
|
+
type: :comment,
|
|
76
|
+
start_col: pos,
|
|
77
|
+
end_col: line.length - 1,
|
|
78
|
+
text:
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
[token, :block_comment, line.length]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def check_multiline_start(line, pos)
|
|
86
|
+
rest = line[pos..]
|
|
87
|
+
|
|
88
|
+
start_match = rest.match(BLOCK_COMMENT_START)
|
|
89
|
+
return [nil, nil, pos] unless start_match
|
|
90
|
+
|
|
91
|
+
start_pos = pos + start_match.begin(0)
|
|
92
|
+
after_start = line[(start_pos + 2)..]
|
|
93
|
+
|
|
94
|
+
if after_start&.include?("*/")
|
|
95
|
+
[nil, nil, pos]
|
|
96
|
+
else
|
|
97
|
+
text = line[start_pos..]
|
|
98
|
+
token = Token.new(
|
|
99
|
+
type: :comment,
|
|
100
|
+
start_col: start_pos,
|
|
101
|
+
end_col: line.length - 1,
|
|
102
|
+
text:
|
|
103
|
+
)
|
|
104
|
+
[:block_comment, token, line.length]
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def match_token(line, pos)
|
|
111
|
+
if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
|
|
112
|
+
rest = line[(pos + 2)..]
|
|
113
|
+
return nil unless rest&.include?("*/")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
super
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mui
|
|
4
|
+
module Syntax
|
|
5
|
+
module Lexers
|
|
6
|
+
# Lexer for Go source code
|
|
7
|
+
class GoLexer < LexerBase
|
|
8
|
+
# Go keywords
|
|
9
|
+
KEYWORDS = %w[
|
|
10
|
+
break case chan const continue default defer else fallthrough
|
|
11
|
+
for func go goto if import interface map package range return
|
|
12
|
+
select struct switch type var
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# Go built-in types
|
|
16
|
+
TYPES = %w[
|
|
17
|
+
bool byte complex64 complex128 error float32 float64
|
|
18
|
+
int int8 int16 int32 int64 rune string
|
|
19
|
+
uint uint8 uint16 uint32 uint64 uintptr
|
|
20
|
+
any comparable
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
# Go constants
|
|
24
|
+
CONSTANTS = %w[true false nil iota].freeze
|
|
25
|
+
|
|
26
|
+
# Pre-compiled patterns with \G anchor for position-specific matching
|
|
27
|
+
COMPILED_PATTERNS = [
|
|
28
|
+
# Single line comment
|
|
29
|
+
[:comment, %r{\G//.*}],
|
|
30
|
+
# Single-line block comment /* ... */ on one line
|
|
31
|
+
[:comment, %r{\G/\*.*?\*/}],
|
|
32
|
+
# Raw string literal (backtick)
|
|
33
|
+
[:string, /\G`[^`]*`/],
|
|
34
|
+
# Double quoted string (with escape handling)
|
|
35
|
+
[:string, /\G"(?:[^"\\]|\\.)*"/],
|
|
36
|
+
# Character literal (rune)
|
|
37
|
+
[:char, /\G'(?:[^'\\]|\\.)*'/],
|
|
38
|
+
# Float numbers (must be before integer)
|
|
39
|
+
[:number, /\G\b\d+\.\d+(?:e[+-]?\d+)?\b/i],
|
|
40
|
+
# Hexadecimal
|
|
41
|
+
[:number, /\G\b0x[0-9a-fA-F]+\b/i],
|
|
42
|
+
# Octal
|
|
43
|
+
[:number, /\G\b0o[0-7]+\b/i],
|
|
44
|
+
# Binary
|
|
45
|
+
[:number, /\G\b0b[01]+\b/i],
|
|
46
|
+
# Integer
|
|
47
|
+
[:number, /\G\b\d+\b/],
|
|
48
|
+
# Constants (true, false, nil, iota)
|
|
49
|
+
[:constant, /\G\b(?:true|false|nil|iota)\b/],
|
|
50
|
+
# Types
|
|
51
|
+
[:type, /\G\b(?:bool|byte|complex64|complex128|error|float32|float64|int|int8|int16|int32|int64|rune|string|uint|uint8|uint16|uint32|uint64|uintptr|any|comparable)\b/],
|
|
52
|
+
# Keywords
|
|
53
|
+
[:keyword, /\G\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/],
|
|
54
|
+
# Function definition names (func の後)
|
|
55
|
+
[:function_definition, /\G(?<=func )[a-z_][a-zA-Z0-9_]*/],
|
|
56
|
+
# Exported identifiers (start with uppercase)
|
|
57
|
+
[:constant, /\G\b[A-Z][a-zA-Z0-9_]*\b/],
|
|
58
|
+
# Regular identifiers
|
|
59
|
+
[:identifier, /\G\b[a-z_][a-zA-Z0-9_]*\b/],
|
|
60
|
+
# Operators
|
|
61
|
+
[:operator, %r{\G(?:&&|\|\||<-|<<=?|>>=?|&\^=?|[+\-*/%&|^<>=!]=?|:=|\+\+|--)}]
|
|
62
|
+
].freeze
|
|
63
|
+
|
|
64
|
+
# Multiline comment patterns (pre-compiled)
|
|
65
|
+
BLOCK_COMMENT_END = %r{\*/}
|
|
66
|
+
BLOCK_COMMENT_START = %r{/\*}
|
|
67
|
+
BLOCK_COMMENT_START_ANCHOR = %r{\A/\*}
|
|
68
|
+
|
|
69
|
+
# Raw string patterns (pre-compiled)
|
|
70
|
+
RAW_STRING_START = /\A`/
|
|
71
|
+
RAW_STRING_END = /`/
|
|
72
|
+
|
|
73
|
+
protected
|
|
74
|
+
|
|
75
|
+
def compiled_patterns
|
|
76
|
+
COMPILED_PATTERNS
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Handle /* ... */ block comments and raw strings that span multiple lines
|
|
80
|
+
def handle_multiline_state(line, pos, state)
|
|
81
|
+
case state
|
|
82
|
+
when :block_comment
|
|
83
|
+
handle_block_comment(line, pos)
|
|
84
|
+
when :raw_string
|
|
85
|
+
handle_raw_string(line, pos)
|
|
86
|
+
else
|
|
87
|
+
[nil, nil, pos]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def check_multiline_start(line, pos)
|
|
92
|
+
rest = line[pos..]
|
|
93
|
+
|
|
94
|
+
# Check for raw string start
|
|
95
|
+
if rest.match?(RAW_STRING_START)
|
|
96
|
+
after_start = line[(pos + 1)..]
|
|
97
|
+
unless after_start&.include?("`")
|
|
98
|
+
# No closing on this line, enter raw string state
|
|
99
|
+
text = line[pos..]
|
|
100
|
+
token = Token.new(
|
|
101
|
+
type: :string,
|
|
102
|
+
start_col: pos,
|
|
103
|
+
end_col: line.length - 1,
|
|
104
|
+
text:
|
|
105
|
+
)
|
|
106
|
+
return [:raw_string, token, line.length]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check for /* that doesn't have a matching */ on this line
|
|
111
|
+
start_match = rest.match(BLOCK_COMMENT_START)
|
|
112
|
+
return [nil, nil, pos] unless start_match
|
|
113
|
+
|
|
114
|
+
start_pos = pos + start_match.begin(0)
|
|
115
|
+
after_start = line[(start_pos + 2)..]
|
|
116
|
+
|
|
117
|
+
if after_start&.include?("*/")
|
|
118
|
+
[nil, nil, pos]
|
|
119
|
+
else
|
|
120
|
+
text = line[start_pos..]
|
|
121
|
+
token = Token.new(
|
|
122
|
+
type: :comment,
|
|
123
|
+
start_col: start_pos,
|
|
124
|
+
end_col: line.length - 1,
|
|
125
|
+
text:
|
|
126
|
+
)
|
|
127
|
+
[:block_comment, token, line.length]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def handle_block_comment(line, pos)
|
|
134
|
+
end_match = line[pos..].match(BLOCK_COMMENT_END)
|
|
135
|
+
if end_match
|
|
136
|
+
end_pos = pos + end_match.begin(0) + 1
|
|
137
|
+
text = line[pos..end_pos]
|
|
138
|
+
token = Token.new(
|
|
139
|
+
type: :comment,
|
|
140
|
+
start_col: pos,
|
|
141
|
+
end_col: end_pos,
|
|
142
|
+
text:
|
|
143
|
+
)
|
|
144
|
+
[token, nil, end_pos + 1]
|
|
145
|
+
else
|
|
146
|
+
text = line[pos..]
|
|
147
|
+
token = if text.empty?
|
|
148
|
+
nil
|
|
149
|
+
else
|
|
150
|
+
Token.new(
|
|
151
|
+
type: :comment,
|
|
152
|
+
start_col: pos,
|
|
153
|
+
end_col: line.length - 1,
|
|
154
|
+
text:
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
[token, :block_comment, line.length]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def handle_raw_string(line, pos)
|
|
162
|
+
end_match = line[pos..].match(RAW_STRING_END)
|
|
163
|
+
if end_match
|
|
164
|
+
end_pos = pos + end_match.begin(0)
|
|
165
|
+
text = line[pos..end_pos]
|
|
166
|
+
token = Token.new(
|
|
167
|
+
type: :string,
|
|
168
|
+
start_col: pos,
|
|
169
|
+
end_col: end_pos,
|
|
170
|
+
text:
|
|
171
|
+
)
|
|
172
|
+
[token, nil, end_pos + 1]
|
|
173
|
+
else
|
|
174
|
+
text = line[pos..]
|
|
175
|
+
token = if text.empty?
|
|
176
|
+
nil
|
|
177
|
+
else
|
|
178
|
+
Token.new(
|
|
179
|
+
type: :string,
|
|
180
|
+
start_col: pos,
|
|
181
|
+
end_col: line.length - 1,
|
|
182
|
+
text:
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
[token, :raw_string, line.length]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def match_token(line, pos)
|
|
190
|
+
# Check for start of raw string
|
|
191
|
+
if line[pos..].match?(RAW_STRING_START)
|
|
192
|
+
rest = line[(pos + 1)..]
|
|
193
|
+
return nil unless rest&.include?("`")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check for start of multiline comment
|
|
197
|
+
if line[pos..].match?(BLOCK_COMMENT_START_ANCHOR)
|
|
198
|
+
rest = line[(pos + 2)..]
|
|
199
|
+
return nil unless rest&.include?("*/")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
super
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|