1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-28 07:47:34 +00:00

DevTools: Move to Userland/DevTools/

This commit is contained in:
Andreas Kling 2021-01-12 12:18:55 +01:00
parent 13d7c09125
commit 4055b03291
125 changed files with 2 additions and 2 deletions

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/String.h>
#include <AK/Types.h>
#include <LibGUI/AutocompleteProvider.h>
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
namespace IPC {
template<>
inline bool encode(IPC::Encoder& encoder, const GUI::AutocompleteProvider::Entry& response)
{
encoder << response.completion;
encoder << (u64)response.partial_input_length;
encoder << (u32)response.kind;
encoder << (u32)response.language;
return true;
}
template<>
inline bool decode(IPC::Decoder& decoder, GUI::AutocompleteProvider::Entry& response)
{
u32 kind = 0;
u32 language = 0;
u64 partial_input_length = 0;
bool ok = decoder.decode(response.completion)
&& decoder.decode(partial_input_length)
&& decoder.decode(kind)
&& decoder.decode(language);
if (ok) {
response.kind = static_cast<GUI::AutocompleteProvider::CompletionKind>(kind);
response.language = static_cast<GUI::AutocompleteProvider::Language>(language);
response.partial_input_length = partial_input_length;
}
return ok;
}
}

View file

@ -0,0 +1,37 @@
add_subdirectory(LanguageServers)
add_subdirectory(LanguageClients)
set(SOURCES
CodeDocument.cpp
CursorTool.cpp
Debugger/BacktraceModel.cpp
Debugger/DebugInfoWidget.cpp
Debugger/Debugger.cpp
Debugger/DisassemblyModel.cpp
Debugger/DisassemblyWidget.cpp
Debugger/RegistersModel.cpp
Debugger/VariablesModel.cpp
Editor.cpp
EditorWrapper.cpp
FindInFilesWidget.cpp
FormEditorWidget.cpp
FormWidget.cpp
Git/DiffViewer.cpp
Git/GitFilesModel.cpp
Git/GitFilesView.cpp
Git/GitRepo.cpp
Git/GitWidget.cpp
HackStudioWidget.cpp
LanguageClient.cpp
Locator.cpp
Project.cpp
ProjectFile.cpp
TerminalWrapper.cpp
WidgetTool.cpp
WidgetTreeModel.cpp
main.cpp
)
serenity_app(HackStudio ICON app-hack-studio)
target_link_libraries(HackStudio LibWeb LibMarkdown LibGUI LibGfx LibCore LibVT LibDebug LibX86 LibDiff LibShell)
add_dependencies(HackStudio CppLanguageServer)

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "CodeDocument.h"
namespace HackStudio {
NonnullRefPtr<CodeDocument> CodeDocument::create(const String& file_path, Client* client)
{
return adopt(*new CodeDocument(file_path, client));
}
NonnullRefPtr<CodeDocument> CodeDocument::create(Client* client)
{
return adopt(*new CodeDocument(client));
}
CodeDocument::CodeDocument(const String& file_path, Client* client)
: TextDocument(client)
, m_file_path(file_path)
{
LexicalPath lexical_path(file_path);
if (lexical_path.has_extension(".cpp") || lexical_path.has_extension(".h"))
m_language = Language::Cpp;
else if (lexical_path.has_extension(".js"))
m_language = Language::JavaScript;
else if (lexical_path.has_extension(".gml"))
m_language = Language::GML;
else if (lexical_path.has_extension(".ini"))
m_language = Language::Ini;
else if (lexical_path.has_extension(".sh"))
m_language = Language::Shell;
}
CodeDocument::CodeDocument(Client* client)
: TextDocument(client)
{
}
CodeDocument::~CodeDocument()
{
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Language.h"
#include <AK/LexicalPath.h>
#include <LibGUI/TextDocument.h>
namespace HackStudio {
class CodeDocument final : public GUI::TextDocument {
public:
virtual ~CodeDocument() override;
static NonnullRefPtr<CodeDocument> create(const String& file_path, Client* client = nullptr);
static NonnullRefPtr<CodeDocument> create(Client* client = nullptr);
const Vector<size_t>& breakpoint_lines() const { return m_breakpoint_lines; }
Vector<size_t>& breakpoint_lines() { return m_breakpoint_lines; }
Optional<size_t> execution_position() const { return m_execution_position; }
void set_execution_position(size_t line) { m_execution_position = line; }
void clear_execution_position() { m_execution_position.clear(); }
const String& file_path() const { return m_file_path; }
Language language() const { return m_language; }
virtual bool is_code_document() const override final { return true; }
private:
explicit CodeDocument(const String& file_path, Client* client = nullptr);
explicit CodeDocument(Client* client = nullptr);
String m_file_path;
Language m_language { Language::Unknown };
Vector<size_t> m_breakpoint_lines;
Optional<size_t> m_execution_position;
};
}

View file

@ -0,0 +1,204 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "CursorTool.h"
#include "FormEditorWidget.h"
#include "FormWidget.h"
#include "WidgetTreeModel.h"
#include <AK/LogStream.h>
#include <LibGfx/Palette.h>
//#define DEBUG_CURSOR_TOOL
namespace HackStudio {
void CursorTool::on_mousedown(GUI::MouseEvent& event)
{
#ifdef DEBUG_CURSOR_TOOL
dbgln("CursorTool::on_mousedown");
#endif
auto& form_widget = m_editor.form_widget();
auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No);
if (event.button() == GUI::MouseButton::Left) {
if (result.widget && result.widget != &form_widget) {
if (event.modifiers() & Mod_Ctrl) {
m_editor.selection().toggle(*result.widget);
} else if (!event.modifiers()) {
if (!m_editor.selection().contains(*result.widget)) {
#ifdef DEBUG_CURSOR_TOOL
dbg() << "Selection didn't contain " << *result.widget << ", making it the only selected one";
#endif
m_editor.selection().set(*result.widget);
}
m_drag_origin = event.position();
m_positions_before_drag.clear();
m_editor.selection().for_each([&](auto& widget) {
m_positions_before_drag.set(&widget, widget.relative_position());
return IterationDecision::Continue;
});
}
} else {
m_editor.selection().clear();
m_rubber_banding = true;
m_rubber_band_origin = event.position();
m_rubber_band_position = event.position();
form_widget.update();
}
// FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget?
form_widget.update();
}
}
void CursorTool::on_mouseup(GUI::MouseEvent& event)
{
#ifdef DEBUG_CURSOR_TOOL
dbgln("CursorTool::on_mouseup");
#endif
if (event.button() == GUI::MouseButton::Left) {
auto& form_widget = m_editor.form_widget();
auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No);
if (!m_dragging && !(event.modifiers() & Mod_Ctrl)) {
if (result.widget && result.widget != &form_widget) {
m_editor.selection().set(*result.widget);
// FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget?
form_widget.update();
}
}
m_dragging = false;
m_rubber_banding = false;
form_widget.update();
}
}
void CursorTool::on_mousemove(GUI::MouseEvent& event)
{
#ifdef DEBUG_CURSOR_TOOL
dbgln("CursorTool::on_mousemove");
#endif
auto& form_widget = m_editor.form_widget();
if (m_rubber_banding) {
set_rubber_band_position(event.position());
return;
}
if (!m_dragging && event.buttons() & GUI::MouseButton::Left && event.position() != m_drag_origin) {
auto result = form_widget.hit_test(event.position(), GUI::Widget::ShouldRespectGreediness::No);
if (result.widget && result.widget != &form_widget) {
if (!m_editor.selection().contains(*result.widget)) {
m_editor.selection().set(*result.widget);
// FIXME: Do we need to update any part of the FormEditorWidget outside the FormWidget?
form_widget.update();
}
}
m_dragging = true;
}
if (m_dragging) {
auto movement_delta = event.position() - m_drag_origin;
m_editor.selection().for_each([&](auto& widget) {
auto new_rect = widget.relative_rect();
new_rect.set_location(m_positions_before_drag.get(&widget).value_or({}).translated(movement_delta));
new_rect.set_x(new_rect.x() - (new_rect.x() % m_editor.form_widget().grid_size()));
new_rect.set_y(new_rect.y() - (new_rect.y() % m_editor.form_widget().grid_size()));
widget.set_relative_rect(new_rect);
return IterationDecision::Continue;
});
m_editor.model().update();
return;
}
}
void CursorTool::on_keydown(GUI::KeyEvent& event)
{
#ifdef DEBUG_CURSOR_TOOL
dbgln("CursorTool::on_keydown");
#endif
auto move_selected_widgets_by = [this](int x, int y) {
m_editor.selection().for_each([&](auto& widget) {
widget.move_by(x, y);
return IterationDecision::Continue;
});
};
if (event.modifiers() == 0) {
switch (event.key()) {
case Key_Down:
move_selected_widgets_by(0, m_editor.form_widget().grid_size());
break;
case Key_Up:
move_selected_widgets_by(0, -m_editor.form_widget().grid_size());
break;
case Key_Left:
move_selected_widgets_by(-m_editor.form_widget().grid_size(), 0);
break;
case Key_Right:
move_selected_widgets_by(m_editor.form_widget().grid_size(), 0);
break;
default:
break;
}
}
}
void CursorTool::set_rubber_band_position(const Gfx::IntPoint& position)
{
if (m_rubber_band_position == position)
return;
m_rubber_band_position = position;
auto rubber_band_rect = this->rubber_band_rect();
m_editor.selection().clear();
m_editor.form_widget().for_each_child_widget([&](auto& child) {
if (child.relative_rect().intersects(rubber_band_rect))
m_editor.selection().add(child);
return IterationDecision::Continue;
});
m_editor.form_widget().update();
}
Gfx::IntRect CursorTool::rubber_band_rect() const
{
if (!m_rubber_banding)
return {};
return Gfx::IntRect::from_two_points(m_rubber_band_origin, m_rubber_band_position);
}
void CursorTool::on_second_paint(GUI::Painter& painter, GUI::PaintEvent&)
{
if (!m_rubber_banding)
return;
auto rect = rubber_band_rect();
painter.fill_rect(rect, m_editor.palette().rubber_band_fill());
painter.draw_rect(rect, m_editor.palette().rubber_band_border());
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Tool.h"
#include <AK/HashMap.h>
#include <LibGUI/Forward.h>
#include <LibGfx/Point.h>
namespace HackStudio {
class CursorTool final : public Tool {
public:
explicit CursorTool(FormEditorWidget& editor)
: Tool(editor)
{
}
virtual ~CursorTool() override { }
private:
virtual const char* class_name() const override { return "CursorTool"; }
virtual void on_mousedown(GUI::MouseEvent&) override;
virtual void on_mouseup(GUI::MouseEvent&) override;
virtual void on_mousemove(GUI::MouseEvent&) override;
virtual void on_keydown(GUI::KeyEvent&) override;
virtual void on_second_paint(GUI::Painter&, GUI::PaintEvent&) override;
void set_rubber_band_position(const Gfx::IntPoint&);
Gfx::IntRect rubber_band_rect() const;
Gfx::IntPoint m_drag_origin;
HashMap<GUI::Widget*, Gfx::IntPoint> m_positions_before_drag;
bool m_dragging { false };
bool m_rubber_banding { false };
Gfx::IntPoint m_rubber_band_origin;
Gfx::IntPoint m_rubber_band_position;
};
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "BacktraceModel.h"
#include "Debugger.h"
#include <LibDebug/StackFrameUtils.h>
namespace HackStudio {
NonnullRefPtr<BacktraceModel> BacktraceModel::create(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
{
return adopt(*new BacktraceModel(create_backtrace(debug_session, regs)));
}
GUI::Variant BacktraceModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
if (role == GUI::ModelRole::Display) {
auto& frame = m_frames.at(index.row());
return frame.function_name;
}
return {};
}
GUI::ModelIndex BacktraceModel::index(int row, int column, const GUI::ModelIndex&) const
{
if (row < 0 || row >= static_cast<int>(m_frames.size()))
return {};
return create_index(row, column, &m_frames.at(row));
}
Vector<BacktraceModel::FrameInfo> BacktraceModel::create_backtrace(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
{
u32 current_ebp = regs.ebp;
u32 current_instruction = regs.eip;
Vector<BacktraceModel::FrameInfo> frames;
do {
auto lib = debug_session.library_at(regs.eip);
if (!lib)
continue;
String name = lib->debug_info->name_of_containing_function(current_instruction - lib->base_address);
if (name.is_null()) {
dbgln("BacktraceModel: couldn't find containing function for address: {:p}", current_instruction);
name = "<missing>";
}
frames.append({ name, current_instruction, current_ebp });
auto frame_info = Debug::StackFrameUtils::get_info(*Debugger::the().session(), current_ebp);
ASSERT(frame_info.has_value());
current_instruction = frame_info.value().return_address;
current_ebp = frame_info.value().next_ebp;
} while (current_ebp && current_instruction);
return frames;
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Vector.h>
#include <LibGUI/ListView.h>
#include <LibGUI/Model.h>
#include <sys/arch/i386/regs.h>
namespace Debug {
class DebugSession;
}
namespace HackStudio {
class BacktraceModel final : public GUI::Model {
public:
static NonnullRefPtr<BacktraceModel> create(const Debug::DebugSession&, const PtraceRegisters& regs);
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_frames.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
virtual String column_name(int) const override
{
return "";
}
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual void update() override { }
virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex&) const override;
struct FrameInfo {
String function_name;
u32 instruction_address;
u32 frame_base;
};
const Vector<FrameInfo>& frames() const { return m_frames; }
private:
explicit BacktraceModel(Vector<FrameInfo>&& frames)
: m_frames(move(frames))
{
}
static Vector<FrameInfo> create_backtrace(const Debug::DebugSession&, const PtraceRegisters&);
Vector<FrameInfo> m_frames;
};
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Function.h>
#include <AK/String.h>
#include <AK/Types.h>
namespace HackStudio {
enum class BreakpointChange {
Added,
Removed,
};
typedef Function<void(const String& file, size_t line, BreakpointChange)> BreakpointChangeCallback;
}

View file

@ -0,0 +1,188 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "DebugInfoWidget.h"
#include "BacktraceModel.h"
#include "Debugger.h"
#include "RegistersModel.h"
#include "VariablesModel.h"
#include <AK/StringBuilder.h>
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Layout.h>
#include <LibGUI/ListView.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Model.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TreeView.h>
namespace HackStudio {
void DebugInfoWidget::init_toolbar()
{
m_continue_action = GUI::Action::create("Continue", Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-continue.png"), [](auto&) {
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::Continue);
});
m_singlestep_action = GUI::Action::create("Step Over", { Mod_None, Key_F10 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-over.png"), [](auto&) {
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceStepOver);
});
m_step_in_action = GUI::Action::create("Step In", { Mod_None, Key_F11 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-in.png"), [](auto&) {
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceSingleStep);
});
m_step_out_action = GUI::Action::create("Step Out", { Mod_Shift, Key_F11 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-step-out.png"), [](auto&) {
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::SourceStepOut);
});
m_toolbar->add_action(*m_continue_action);
m_toolbar->add_action(*m_singlestep_action);
m_toolbar->add_action(*m_step_in_action);
m_toolbar->add_action(*m_step_out_action);
set_debug_actions_enabled(false);
}
DebugInfoWidget::DebugInfoWidget()
{
set_layout<GUI::VerticalBoxLayout>();
auto& toolbar_container = add<GUI::ToolBarContainer>();
m_toolbar = toolbar_container.add<GUI::ToolBar>();
init_toolbar();
auto& bottom_box = add<GUI::Widget>();
bottom_box.set_layout<GUI::HorizontalBoxLayout>();
auto& splitter = bottom_box.add<GUI::HorizontalSplitter>();
m_backtrace_view = splitter.add<GUI::ListView>();
auto& variables_tab_widget = splitter.add<GUI::TabWidget>();
variables_tab_widget.set_tab_position(GUI::TabWidget::TabPosition::Bottom);
variables_tab_widget.add_widget("Variables", build_variables_tab());
variables_tab_widget.add_widget("Registers", build_registers_tab());
m_backtrace_view->on_selection = [this](auto& index) {
auto& model = static_cast<BacktraceModel&>(*m_backtrace_view->model());
// Note: The reconstruction of the register set here is obviously incomplete.
// We currently only reconstruct eip & ebp. Ideally would also reconstruct the other registers somehow.
// (Other registers may be needed to get the values of variables who are not stored on the stack)
PtraceRegisters frame_regs {};
frame_regs.eip = model.frames()[index.row()].instruction_address;
frame_regs.ebp = model.frames()[index.row()].frame_base;
m_variables_view->set_model(VariablesModel::create(frame_regs));
};
}
NonnullRefPtr<GUI::Widget> DebugInfoWidget::build_variables_tab()
{
auto variables_widget = GUI::Widget::construct();
variables_widget->set_layout<GUI::HorizontalBoxLayout>();
m_variables_view = variables_widget->add<GUI::TreeView>();
auto is_valid_index = [](auto& index) {
if (!index.is_valid())
return false;
auto* variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
if (variable->location_type != Debug::DebugInfo::VariableInfo::LocationType::Address)
return false;
return variable->is_enum_type() || variable->type_name.is_one_of("int", "bool");
};
m_variables_view->on_context_menu_request = [this, is_valid_index](auto& index, auto& event) {
if (!is_valid_index(index))
return;
m_variable_context_menu->popup(event.screen_position());
};
m_variables_view->on_activation = [this, is_valid_index](auto& index) {
if (!is_valid_index(index))
return;
String value;
if (GUI::InputBox::show(value, window(), "Enter new value:", "Set variable value") == GUI::InputBox::ExecOK) {
auto& model = static_cast<VariablesModel&>(*m_variables_view->model());
model.set_variable_value(index, value, window());
}
};
auto edit_variable_action = GUI::Action::create("Change value", [this](auto&) {
m_variables_view->on_activation(m_variables_view->selection().first());
});
m_variable_context_menu = GUI::Menu::construct();
m_variable_context_menu->add_action(edit_variable_action);
return variables_widget;
}
NonnullRefPtr<GUI::Widget> DebugInfoWidget::build_registers_tab()
{
auto registers_widget = GUI::Widget::construct();
registers_widget->set_layout<GUI::HorizontalBoxLayout>();
m_registers_view = registers_widget->add<GUI::TableView>();
return registers_widget;
}
void DebugInfoWidget::update_state(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
{
m_variables_view->set_model(VariablesModel::create(regs));
m_backtrace_view->set_model(BacktraceModel::create(debug_session, regs));
if (m_registers_view->model()) {
auto& previous_registers = static_cast<RegistersModel*>(m_registers_view->model())->raw_registers();
m_registers_view->set_model(RegistersModel::create(regs, previous_registers));
} else {
m_registers_view->set_model(RegistersModel::create(regs));
}
auto selected_index = m_backtrace_view->model()->index(0);
if (!selected_index.is_valid()) {
dbgln("Warning: DebugInfoWidget: backtrace selected index is invalid");
return;
}
m_backtrace_view->selection().set(selected_index);
}
void DebugInfoWidget::program_stopped()
{
m_variables_view->set_model({});
m_backtrace_view->set_model({});
m_registers_view->set_model({});
}
void DebugInfoWidget::set_debug_actions_enabled(bool enabled)
{
m_continue_action->set_enabled(enabled);
m_singlestep_action->set_enabled(enabled);
m_step_in_action->set_enabled(enabled);
m_step_out_action->set_enabled(enabled);
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Debugger.h"
#include <AK/NonnullOwnPtr.h>
#include <LibGUI/Action.h>
#include <LibGUI/ListView.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Model.h>
#include <LibGUI/TableView.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
#include <LibGUI/TreeView.h>
#include <LibGUI/Widget.h>
#include <sys/arch/i386/regs.h>
namespace HackStudio {
class DebugInfoWidget final : public GUI::Widget {
C_OBJECT(DebugInfoWidget)
public:
virtual ~DebugInfoWidget() override { }
void update_state(const Debug::DebugSession&, const PtraceRegisters&);
void program_stopped();
void set_debug_actions_enabled(bool enabled);
private:
explicit DebugInfoWidget();
void init_toolbar();
NonnullRefPtr<GUI::Widget> build_variables_tab();
NonnullRefPtr<GUI::Widget> build_registers_tab();
RefPtr<GUI::TreeView> m_variables_view;
RefPtr<GUI::TableView> m_registers_view;
RefPtr<GUI::ListView> m_backtrace_view;
RefPtr<GUI::Menu> m_variable_context_menu;
RefPtr<GUI::ToolBar> m_toolbar;
RefPtr<GUI::Action> m_continue_action;
RefPtr<GUI::Action> m_singlestep_action;
RefPtr<GUI::Action> m_step_in_action;
RefPtr<GUI::Action> m_step_out_action;
};
}

View file

@ -0,0 +1,295 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Debugger.h"
#include <LibDebug/StackFrameUtils.h>
namespace HackStudio {
static Debugger* s_the;
Debugger& Debugger::the()
{
ASSERT(s_the);
return *s_the;
}
void Debugger::initialize(
String source_root,
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
Function<void()> on_continue_callback,
Function<void()> on_exit_callback)
{
s_the = new Debugger(source_root, move(on_stop_callback), move(on_continue_callback), move(on_exit_callback));
}
bool Debugger::is_initialized()
{
return s_the;
}
Debugger::Debugger(
String source_root,
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
Function<void()> on_continue_callback,
Function<void()> on_exit_callback)
: m_source_root(source_root)
, m_on_stopped_callback(move(on_stop_callback))
, m_on_continue_callback(move(on_continue_callback))
, m_on_exit_callback(move(on_exit_callback))
{
pthread_mutex_init(&m_ui_action_mutex, nullptr);
pthread_cond_init(&m_ui_action_cond, nullptr);
}
void Debugger::on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type)
{
auto position = create_source_position(file, line);
if (change_type == BreakpointChange::Added) {
Debugger::the().m_breakpoints.append(position);
} else {
Debugger::the().m_breakpoints.remove_all_matching([&](Debug::DebugInfo::SourcePosition val) { return val == position; });
}
auto session = Debugger::the().session();
if (!session)
return;
auto address = session->get_address_from_source_position(position.file_path, position.line_number);
if (!address.has_value()) {
dbgln("Warning: couldn't get instruction address from source");
// TODO: Currently, the GUI will indicate that a breakpoint was inserted/removed at this line,
// regardless of whether we actually succeeded to insert it. (For example a breakpoint on a comment, or an include statement).
// We should indicate failure via a return value from this function, and not update the breakpoint GUI if we fail.
return;
}
if (change_type == BreakpointChange::Added) {
bool success = session->insert_breakpoint(reinterpret_cast<void*>(address.value().address));
ASSERT(success);
} else {
bool success = session->remove_breakpoint(reinterpret_cast<void*>(address.value().address));
ASSERT(success);
}
}
Debug::DebugInfo::SourcePosition Debugger::create_source_position(const String& file, size_t line)
{
if (!file.starts_with('/') && !file.starts_with("./"))
return { String::formatted("./{}", file), line + 1 };
return { file, line + 1 };
}
int Debugger::start_static()
{
Debugger::the().start();
return 0;
}
void Debugger::start()
{
m_debug_session = Debug::DebugSession::exec_and_attach(m_executable_path, m_source_root);
ASSERT(!!m_debug_session);
for (const auto& breakpoint : m_breakpoints) {
dbgln("inserting breakpoint at: {}:{}", breakpoint.file_path, breakpoint.line_number);
auto address = m_debug_session->get_address_from_source_position(breakpoint.file_path, breakpoint.line_number);
if (address.has_value()) {
bool success = m_debug_session->insert_breakpoint(reinterpret_cast<void*>(address.value().address));
ASSERT(success);
} else {
dbgln("couldn't insert breakpoint");
}
}
debugger_loop();
}
int Debugger::debugger_loop()
{
ASSERT(m_debug_session);
m_debug_session->run(Debug::DebugSession::DesiredInitialDebugeeState::Running, [this](Debug::DebugSession::DebugBreakReason reason, Optional<PtraceRegisters> optional_regs) {
if (reason == Debug::DebugSession::DebugBreakReason::Exited) {
dbgln("Program exited");
m_on_exit_callback();
return Debug::DebugSession::DebugDecision::Detach;
}
remove_temporary_breakpoints();
ASSERT(optional_regs.has_value());
const PtraceRegisters& regs = optional_regs.value();
auto source_position = m_debug_session->get_source_position(regs.eip);
if (!source_position.has_value())
return Debug::DebugSession::DebugDecision::SingleStep;
// We currently do no support stepping through assembly source
if (source_position.value().file_path.ends_with(".S"))
return Debug::DebugSession::DebugDecision::SingleStep;
ASSERT(source_position.has_value());
if (m_state.get() == Debugger::DebuggingState::SingleStepping) {
if (m_state.should_stop_single_stepping(source_position.value())) {
m_state.set_normal();
} else {
return Debug::DebugSession::DebugDecision::SingleStep;
}
}
auto control_passed_to_user = m_on_stopped_callback(regs);
if (control_passed_to_user == HasControlPassedToUser::Yes) {
pthread_mutex_lock(&m_ui_action_mutex);
pthread_cond_wait(&m_ui_action_cond, &m_ui_action_mutex);
pthread_mutex_unlock(&m_ui_action_mutex);
if (m_requested_debugger_action != DebuggerAction::Exit)
m_on_continue_callback();
} else {
m_requested_debugger_action = DebuggerAction::Continue;
}
switch (m_requested_debugger_action) {
case DebuggerAction::Continue:
m_state.set_normal();
return Debug::DebugSession::DebugDecision::Continue;
case DebuggerAction::SourceSingleStep:
m_state.set_single_stepping(source_position.value());
return Debug::DebugSession::DebugDecision::SingleStep;
case DebuggerAction::SourceStepOut:
m_state.set_stepping_out();
do_step_out(regs);
return Debug::DebugSession::DebugDecision::Continue;
case DebuggerAction::SourceStepOver:
m_state.set_stepping_over();
do_step_over(regs);
return Debug::DebugSession::DebugDecision::Continue;
case DebuggerAction::Exit:
// NOTE: Is detaching from the debuggee the best thing to do here?
// We could display a dialog in the UI, remind the user that there is
// a live debugged process, and ask whether they want to terminate/detach.
dbgln("Debugger exiting");
return Debug::DebugSession::DebugDecision::Detach;
}
ASSERT_NOT_REACHED();
});
m_debug_session.clear();
return 0;
}
void Debugger::DebuggingState::set_normal()
{
m_state = State::Normal;
m_original_source_position.clear();
}
void Debugger::DebuggingState::set_single_stepping(Debug::DebugInfo::SourcePosition original_source_position)
{
m_state = State::SingleStepping;
m_original_source_position = original_source_position;
}
bool Debugger::DebuggingState::should_stop_single_stepping(const Debug::DebugInfo::SourcePosition& current_source_position) const
{
ASSERT(m_state == State::SingleStepping);
return m_original_source_position.value() != current_source_position;
}
void Debugger::remove_temporary_breakpoints()
{
for (auto breakpoint_address : m_state.temporary_breakpoints()) {
ASSERT(m_debug_session->breakpoint_exists((void*)breakpoint_address));
bool rc = m_debug_session->remove_breakpoint((void*)breakpoint_address);
ASSERT(rc);
}
m_state.clear_temporary_breakpoints();
}
void Debugger::DebuggingState::clear_temporary_breakpoints()
{
m_addresses_of_temporary_breakpoints.clear();
}
void Debugger::DebuggingState::add_temporary_breakpoint(u32 address)
{
m_addresses_of_temporary_breakpoints.append(address);
}
void Debugger::do_step_out(const PtraceRegisters& regs)
{
// To step out, we simply insert a temporary breakpoint at the
// instruction the current function returns to, and continue
// execution until we hit that instruction (or some other breakpoint).
insert_temporary_breakpoint_at_return_address(regs);
}
void Debugger::do_step_over(const PtraceRegisters& regs)
{
// To step over, we insert a temporary breakpoint at each line in the current function,
// as well as at the current function's return point, and continue execution.
auto lib = m_debug_session->library_at(regs.eip);
if (!lib)
return;
auto current_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address);
if (!current_function.has_value()) {
dbgln("cannot perform step_over, failed to find containing function of: {:p}", regs.eip);
return;
}
ASSERT(current_function.has_value());
auto lines_in_current_function = lib->debug_info->source_lines_in_scope(current_function.value());
for (const auto& line : lines_in_current_function) {
insert_temporary_breakpoint(line.address_of_first_statement.value() + lib->base_address);
}
insert_temporary_breakpoint_at_return_address(regs);
}
void Debugger::insert_temporary_breakpoint_at_return_address(const PtraceRegisters& regs)
{
auto frame_info = Debug::StackFrameUtils::get_info(*m_debug_session, regs.ebp);
ASSERT(frame_info.has_value());
u32 return_address = frame_info.value().return_address;
insert_temporary_breakpoint(return_address);
}
void Debugger::insert_temporary_breakpoint(FlatPtr address)
{
if (m_debug_session->breakpoint_exists((void*)address))
return;
bool success = m_debug_session->insert_breakpoint(reinterpret_cast<void*>(address));
ASSERT(success);
m_state.add_temporary_breakpoint(address);
}
void Debugger::set_requested_debugger_action(DebuggerAction action)
{
pthread_mutex_lock(continue_mutex());
m_requested_debugger_action = action;
pthread_cond_signal(continue_cond());
pthread_mutex_unlock(continue_mutex());
}
}

View file

@ -0,0 +1,139 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "BreakpointCallback.h"
#include <AK/Function.h>
#include <AK/Vector.h>
#include <LibDebug/DebugSession.h>
#include <LibThread/Lock.h>
#include <LibThread/Thread.h>
namespace HackStudio {
class Debugger {
public:
static Debugger& the();
enum class HasControlPassedToUser {
No,
Yes,
};
static void initialize(
String source_root,
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
Function<void()> on_continue_callback,
Function<void()> on_exit_callback);
static bool is_initialized();
static void on_breakpoint_change(const String& file, size_t line, BreakpointChange change_type);
void set_executable_path(const String& path) { m_executable_path = path; }
Debug::DebugSession* session() { return m_debug_session.ptr(); }
// Thread entry point
static int start_static();
pthread_mutex_t* continue_mutex() { return &m_ui_action_mutex; }
pthread_cond_t* continue_cond() { return &m_ui_action_cond; }
enum class DebuggerAction {
Continue,
SourceSingleStep,
SourceStepOut,
SourceStepOver,
Exit,
};
void set_requested_debugger_action(DebuggerAction);
void reset_breakpoints() { m_breakpoints.clear(); }
private:
class DebuggingState {
public:
enum State {
Normal, // Continue normally until we hit a breakpoint / program terminates
SingleStepping,
SteppingOut,
SteppingOver,
};
State get() const { return m_state; }
void set_normal();
void set_single_stepping(Debug::DebugInfo::SourcePosition original_source_position);
void set_stepping_out() { m_state = State::SteppingOut; }
void set_stepping_over() { m_state = State::SteppingOver; }
bool should_stop_single_stepping(const Debug::DebugInfo::SourcePosition& current_source_position) const;
void clear_temporary_breakpoints();
void add_temporary_breakpoint(u32 address);
const Vector<u32>& temporary_breakpoints() const { return m_addresses_of_temporary_breakpoints; }
private:
State m_state { Normal };
Optional<Debug::DebugInfo::SourcePosition> m_original_source_position; // The source position at which we started the current single step
Vector<u32> m_addresses_of_temporary_breakpoints;
};
explicit Debugger(
String source_root,
Function<HasControlPassedToUser(const PtraceRegisters&)> on_stop_callback,
Function<void()> on_continue_callback,
Function<void()> on_exit_callback);
static Debug::DebugInfo::SourcePosition create_source_position(const String& file, size_t line);
void start();
int debugger_loop();
void remove_temporary_breakpoints();
void do_step_out(const PtraceRegisters&);
void do_step_over(const PtraceRegisters&);
void insert_temporary_breakpoint(FlatPtr address);
void insert_temporary_breakpoint_at_return_address(const PtraceRegisters&);
OwnPtr<Debug::DebugSession> m_debug_session;
String m_source_root;
DebuggingState m_state;
pthread_mutex_t m_ui_action_mutex {};
pthread_cond_t m_ui_action_cond {};
DebuggerAction m_requested_debugger_action { DebuggerAction::Continue };
Vector<Debug::DebugInfo::SourcePosition> m_breakpoints;
String m_executable_path;
Function<HasControlPassedToUser(const PtraceRegisters&)> m_on_stopped_callback;
Function<void()> m_on_continue_callback;
Function<void()> m_on_exit_callback;
};
}

View file

@ -0,0 +1,137 @@
/*
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "DisassemblyModel.h"
#include <AK/MappedFile.h>
#include <AK/StringBuilder.h>
#include <LibDebug/DebugSession.h>
#include <LibELF/Image.h>
#include <LibX86/Disassembler.h>
#include <LibX86/ELFSymbolProvider.h>
#include <ctype.h>
#include <stdio.h>
namespace HackStudio {
DisassemblyModel::DisassemblyModel(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
{
auto lib = debug_session.library_at(regs.eip);
if (!lib)
return;
auto containing_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address);
if (!containing_function.has_value()) {
dbgln("Cannot disassemble as the containing function was not found.");
return;
}
OwnPtr<ELF::Image> kernel_elf;
const ELF::Image* elf = nullptr;
if (containing_function.value().address_low >= 0xc0000000) {
auto file_or_error = MappedFile::map("/boot/Kernel");
if (file_or_error.is_error())
return;
kernel_elf = make<ELF::Image>(file_or_error.value()->bytes());
elf = kernel_elf.ptr();
} else {
elf = &lib->debug_info->elf();
}
auto symbol = elf->find_symbol(containing_function.value().address_low);
if (!symbol.has_value())
return;
ASSERT(symbol.has_value());
auto view = symbol.value().raw_data();
X86::ELFSymbolProvider symbol_provider(*elf);
X86::SimpleInstructionStream stream((const u8*)view.characters_without_null_termination(), view.length());
X86::Disassembler disassembler(stream);
size_t offset_into_symbol = 0;
for (;;) {
auto insn = disassembler.next();
if (!insn.has_value())
break;
FlatPtr address_in_profiled_program = symbol.value().value() + offset_into_symbol;
auto disassembly = insn.value().to_string(address_in_profiled_program, &symbol_provider);
StringView instruction_bytes = view.substring_view(offset_into_symbol, insn.value().length());
m_instructions.append({ insn.value(), disassembly, instruction_bytes, address_in_profiled_program });
offset_into_symbol += insn.value().length();
}
}
DisassemblyModel::~DisassemblyModel()
{
}
int DisassemblyModel::row_count(const GUI::ModelIndex&) const
{
return m_instructions.size();
}
String DisassemblyModel::column_name(int column) const
{
switch (column) {
case Column::Address:
return "Address";
case Column::InstructionBytes:
return "Insn Bytes";
case Column::Disassembly:
return "Disassembly";
default:
ASSERT_NOT_REACHED();
return {};
}
}
GUI::Variant DisassemblyModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
auto& insn = m_instructions[index.row()];
if (role == GUI::ModelRole::Display) {
if (index.column() == Column::Address)
return String::formatted("{:p}", insn.address);
if (index.column() == Column::InstructionBytes) {
StringBuilder builder;
for (auto ch : insn.bytes)
builder.appendff("{:02x} ", static_cast<unsigned char>(ch));
return builder.to_string();
}
if (index.column() == Column::Disassembly)
return insn.disassembly;
return {};
}
return {};
}
void DisassemblyModel::update()
{
did_update();
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Vector.h>
#include <LibGUI/Model.h>
#include <LibX86/Instruction.h>
#include <sys/arch/i386/regs.h>
namespace Debug {
class DebugSession;
}
namespace HackStudio {
struct InstructionData {
X86::Instruction insn;
String disassembly;
StringView bytes;
FlatPtr address { 0 };
};
class DisassemblyModel final : public GUI::Model {
public:
static NonnullRefPtr<DisassemblyModel> create(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
{
return adopt(*new DisassemblyModel(debug_session, regs));
}
enum Column {
Address,
InstructionBytes,
Disassembly,
__Count
};
virtual ~DisassemblyModel() override;
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
virtual String column_name(int) const override;
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual void update() override;
private:
DisassemblyModel(const Debug::DebugSession&, const PtraceRegisters&);
Vector<InstructionData> m_instructions;
};
}

View file

@ -0,0 +1,105 @@
/*
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "DisassemblyWidget.h"
#include "DisassemblyModel.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Palette.h>
namespace HackStudio {
void UnavailableDisassemblyWidget::paint_event(GUI::PaintEvent& event)
{
Frame::paint_event(event);
if (reason().is_empty())
return;
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.draw_text(frame_inner_rect(), reason(), Gfx::TextAlignment::Center, palette().window_text(), Gfx::TextElision::Right);
}
DisassemblyWidget::DisassemblyWidget()
{
set_layout<GUI::VerticalBoxLayout>();
m_top_container = add<GUI::Widget>();
m_top_container->set_layout<GUI::HorizontalBoxLayout>();
m_top_container->set_fixed_height(20);
m_function_name_label = m_top_container->add<GUI::Label>("");
m_disassembly_view = add<GUI::TableView>();
m_unavailable_disassembly_widget = add<UnavailableDisassemblyWidget>("");
hide_disassembly("Program isn't running");
}
void DisassemblyWidget::update_state(const Debug::DebugSession& debug_session, const PtraceRegisters& regs)
{
m_disassembly_view->set_model(DisassemblyModel::create(debug_session, regs));
if (m_disassembly_view->model()->row_count() > 0) {
auto lib = debug_session.library_at(regs.eip);
if (!lib)
return;
auto containing_function = lib->debug_info->get_containing_function(regs.eip - lib->base_address);
if (containing_function.has_value())
m_function_name_label->set_text(containing_function.value().name);
else
m_function_name_label->set_text("<missing>");
show_disassembly();
} else {
hide_disassembly("No disassembly to show for this function");
}
}
void DisassemblyWidget::program_stopped()
{
m_disassembly_view->set_model({});
m_function_name_label->set_text("");
hide_disassembly("Program isn't running");
}
void DisassemblyWidget::show_disassembly()
{
m_top_container->set_visible(true);
m_disassembly_view->set_visible(true);
m_function_name_label->set_visible(true);
m_unavailable_disassembly_widget->set_visible(false);
}
void DisassemblyWidget::hide_disassembly(const String& reason)
{
m_top_container->set_visible(false);
m_disassembly_view->set_visible(false);
m_function_name_label->set_visible(false);
m_unavailable_disassembly_widget->set_visible(true);
m_unavailable_disassembly_widget->set_reason(reason);
}
}

View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Debugger.h"
#include <AK/NonnullOwnPtr.h>
#include <LibGUI/Label.h>
#include <LibGUI/Model.h>
#include <LibGUI/TableView.h>
#include <LibGUI/Widget.h>
#include <sys/arch/i386/regs.h>
namespace HackStudio {
class UnavailableDisassemblyWidget final : public GUI::Frame {
C_OBJECT(UnavailableDisassemblyWidget)
public:
virtual ~UnavailableDisassemblyWidget() override { }
const String& reason() const { return m_reason; }
void set_reason(const String& text) { m_reason = text; }
private:
UnavailableDisassemblyWidget(const String& reason)
: m_reason(reason)
{
}
virtual void paint_event(GUI::PaintEvent& event) override;
String m_reason;
};
class DisassemblyWidget final : public GUI::Widget {
C_OBJECT(DisassemblyWidget)
public:
virtual ~DisassemblyWidget() override { }
void update_state(const Debug::DebugSession&, const PtraceRegisters&);
void program_stopped();
private:
DisassemblyWidget();
void show_disassembly();
void hide_disassembly(const String&);
RefPtr<GUI::Widget> m_top_container;
RefPtr<GUI::TableView> m_disassembly_view;
RefPtr<GUI::Label> m_function_name_label;
RefPtr<UnavailableDisassemblyWidget> m_unavailable_disassembly_widget;
};
}

View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "RegistersModel.h"
namespace HackStudio {
RegistersModel::RegistersModel(const PtraceRegisters& regs)
: m_raw_registers(regs)
{
m_registers.append({ "eax", regs.eax });
m_registers.append({ "ebx", regs.ebx });
m_registers.append({ "ecx", regs.ecx });
m_registers.append({ "edx", regs.edx });
m_registers.append({ "esp", regs.esp });
m_registers.append({ "ebp", regs.ebp });
m_registers.append({ "esi", regs.esi });
m_registers.append({ "edi", regs.edi });
m_registers.append({ "eip", regs.eip });
m_registers.append({ "eflags", regs.eflags });
m_registers.append({ "cs", regs.cs });
m_registers.append({ "ss", regs.ss });
m_registers.append({ "ds", regs.ds });
m_registers.append({ "es", regs.es });
m_registers.append({ "fs", regs.fs });
m_registers.append({ "gs", regs.gs });
}
RegistersModel::RegistersModel(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs)
: m_raw_registers(current_regs)
{
m_registers.append({ "eax", current_regs.eax, current_regs.eax != previous_regs.eax });
m_registers.append({ "ebx", current_regs.ebx, current_regs.ebx != previous_regs.ebx });
m_registers.append({ "ecx", current_regs.ecx, current_regs.ecx != previous_regs.ecx });
m_registers.append({ "edx", current_regs.edx, current_regs.edx != previous_regs.edx });
m_registers.append({ "esp", current_regs.esp, current_regs.esp != previous_regs.esp });
m_registers.append({ "ebp", current_regs.ebp, current_regs.ebp != previous_regs.ebp });
m_registers.append({ "esi", current_regs.esi, current_regs.esi != previous_regs.esi });
m_registers.append({ "edi", current_regs.edi, current_regs.edi != previous_regs.edi });
m_registers.append({ "eip", current_regs.eip, current_regs.eip != previous_regs.eip });
m_registers.append({ "eflags", current_regs.eflags, current_regs.eflags != previous_regs.eflags });
m_registers.append({ "cs", current_regs.cs, current_regs.cs != previous_regs.cs });
m_registers.append({ "ss", current_regs.ss, current_regs.ss != previous_regs.ss });
m_registers.append({ "ds", current_regs.ds, current_regs.ds != previous_regs.ds });
m_registers.append({ "es", current_regs.es, current_regs.es != previous_regs.es });
m_registers.append({ "fs", current_regs.fs, current_regs.ds != previous_regs.fs });
m_registers.append({ "gs", current_regs.gs, current_regs.gs != previous_regs.gs });
}
RegistersModel::~RegistersModel()
{
}
int RegistersModel::row_count(const GUI::ModelIndex&) const
{
return m_registers.size();
}
String RegistersModel::column_name(int column) const
{
switch (column) {
case Column::Register:
return "Register";
case Column::Value:
return "Value";
default:
ASSERT_NOT_REACHED();
return {};
}
}
GUI::Variant RegistersModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
auto& reg = m_registers[index.row()];
if (role == GUI::ModelRole::ForegroundColor) {
if (reg.changed)
return Color(Color::Red);
else
return Color(Color::Black);
}
if (role == GUI::ModelRole::Display) {
if (index.column() == Column::Register)
return reg.name;
if (index.column() == Column::Value)
return String::formatted("{:08x}", reg.value);
return {};
}
return {};
}
void RegistersModel::update()
{
did_update();
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (c) 2020, Luke Wilde <luke.wilde@live.co.uk>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Vector.h>
#include <LibGUI/Model.h>
#include <sys/arch/i386/regs.h>
namespace HackStudio {
struct RegisterData {
String name;
u32 value;
bool changed { false };
};
class RegistersModel final : public GUI::Model {
public:
static RefPtr<RegistersModel> create(const PtraceRegisters& regs)
{
return adopt(*new RegistersModel(regs));
}
static RefPtr<RegistersModel> create(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs)
{
return adopt(*new RegistersModel(current_regs, previous_regs));
}
enum Column {
Register,
Value,
__Count
};
virtual ~RegistersModel() override;
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
virtual String column_name(int) const override;
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual void update() override;
const PtraceRegisters& raw_registers() const { return m_raw_registers; }
private:
explicit RegistersModel(const PtraceRegisters& regs);
RegistersModel(const PtraceRegisters& current_regs, const PtraceRegisters& previous_regs);
PtraceRegisters m_raw_registers;
Vector<RegisterData> m_registers;
};
}

View file

@ -0,0 +1,194 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "VariablesModel.h"
#include <LibGUI/Application.h>
#include <LibGUI/MessageBox.h>
namespace HackStudio {
GUI::ModelIndex VariablesModel::index(int row, int column, const GUI::ModelIndex& parent_index) const
{
if (!parent_index.is_valid())
return create_index(row, column, &m_variables[row]);
auto* parent = static_cast<const Debug::DebugInfo::VariableInfo*>(parent_index.internal_data());
auto* child = &parent->members[row];
return create_index(row, column, child);
}
GUI::ModelIndex VariablesModel::parent_index(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto* child = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
auto* parent = child->parent;
if (parent == nullptr)
return {};
if (parent->parent == nullptr) {
for (size_t row = 0; row < m_variables.size(); row++)
if (m_variables.ptr_at(row).ptr() == parent)
return create_index(row, 0, parent);
ASSERT_NOT_REACHED();
}
for (size_t row = 0; row < parent->parent->members.size(); row++) {
Debug::DebugInfo::VariableInfo* child_at_row = parent->parent->members.ptr_at(row).ptr();
if (child_at_row == parent)
return create_index(row, 0, parent);
}
ASSERT_NOT_REACHED();
}
int VariablesModel::row_count(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return m_variables.size();
auto* node = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
return node->members.size();
}
static String variable_value_as_string(const Debug::DebugInfo::VariableInfo& variable)
{
if (variable.location_type != Debug::DebugInfo::VariableInfo::LocationType::Address)
return "N/A";
auto variable_address = variable.location_data.address;
if (variable.is_enum_type()) {
auto value = Debugger::the().session()->peek((u32*)variable_address);
ASSERT(value.has_value());
auto it = variable.type->members.find_if([&enumerator_value = value.value()](const auto& enumerator) {
return enumerator->constant_data.as_u32 == enumerator_value;
});
ASSERT(!it.is_end());
return String::formatted("{}::{}", variable.type_name, (*it)->name);
}
if (variable.type_name == "int") {
auto value = Debugger::the().session()->peek((u32*)variable_address);
ASSERT(value.has_value());
return String::formatted("{}", static_cast<int>(value.value()));
}
if (variable.type_name == "char") {
auto value = Debugger::the().session()->peek((u32*)variable_address);
ASSERT(value.has_value());
return String::formatted("'{0:c}' ({0:d})", value.value());
}
if (variable.type_name == "bool") {
auto value = Debugger::the().session()->peek((u32*)variable_address);
ASSERT(value.has_value());
return (value.value() & 1) ? "true" : "false";
}
return String::formatted("type: {} @ {:p}, ", variable.type_name, variable_address);
}
static Optional<u32> string_to_variable_value(const StringView& string_value, const Debug::DebugInfo::VariableInfo& variable)
{
if (variable.is_enum_type()) {
auto prefix_string = String::formatted("{}::", variable.type_name);
auto string_to_use = string_value;
if (string_value.starts_with(prefix_string))
string_to_use = string_value.substring_view(prefix_string.length(), string_value.length() - prefix_string.length());
auto it = variable.type->members.find_if([string_to_use](const auto& enumerator) {
return enumerator->name == string_to_use;
});
if (it.is_end())
return {};
return (*it)->constant_data.as_u32;
}
if (variable.type_name == "int") {
auto value = string_value.to_int();
if (value.has_value())
return value.value();
return {};
}
if (variable.type_name == "bool") {
if (string_value == "true")
return true;
if (string_value == "false")
return false;
return {};
}
return {};
}
void VariablesModel::set_variable_value(const GUI::ModelIndex& index, const StringView& string_value, GUI::Window* parent_window)
{
auto variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
auto value = string_to_variable_value(string_value, *variable);
if (value.has_value()) {
auto success = Debugger::the().session()->poke((u32*)variable->location_data.address, value.value());
ASSERT(success);
return;
}
GUI::MessageBox::show(
parent_window,
String::formatted("String value \"{}\" could not be converted to a value of type {}.", string_value, variable->type_name),
"Set value failed",
GUI::MessageBox::Type::Error);
}
GUI::Variant VariablesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
auto* variable = static_cast<const Debug::DebugInfo::VariableInfo*>(index.internal_data());
switch (role) {
case GUI::ModelRole::Display: {
auto value_as_string = variable_value_as_string(*variable);
return String::formatted("{}: {}", variable->name, value_as_string);
}
case GUI::ModelRole::Icon:
return m_variable_icon;
default:
return {};
}
}
void VariablesModel::update()
{
did_update();
}
RefPtr<VariablesModel> VariablesModel::create(const PtraceRegisters& regs)
{
auto lib = Debugger::the().session()->library_at(regs.eip);
if (!lib)
return nullptr;
auto variables = lib->debug_info->get_variables_in_current_scope(regs);
return adopt(*new VariablesModel(move(variables), regs));
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Debugger.h"
#include <AK/NonnullOwnPtr.h>
#include <LibGUI/Model.h>
#include <LibGUI/TreeView.h>
#include <sys/arch/i386/regs.h>
namespace HackStudio {
class VariablesModel final : public GUI::Model {
public:
static RefPtr<VariablesModel> create(const PtraceRegisters& regs);
void set_variable_value(const GUI::ModelIndex&, const StringView&, GUI::Window*);
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override;
virtual void update() override;
virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override;
virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& = GUI::ModelIndex()) const override;
private:
explicit VariablesModel(NonnullOwnPtrVector<Debug::DebugInfo::VariableInfo>&& variables, const PtraceRegisters& regs)
: m_variables(move(variables))
, m_regs(regs)
{
m_variable_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
}
NonnullOwnPtrVector<Debug::DebugInfo::VariableInfo> m_variables;
PtraceRegisters m_regs;
GUI::Icon m_variable_icon;
};
}

View file

@ -0,0 +1,530 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Editor.h"
#include "Debugger/Debugger.h"
#include "EditorWrapper.h"
#include "HackStudio.h"
#include "Language.h"
#include <AK/ByteBuffer.h>
#include <AK/LexicalPath.h>
#include <LibCore/DirIterator.h>
#include <LibCore/File.h>
#include <LibGUI/Application.h>
#include <LibGUI/CppSyntaxHighlighter.h>
#include <LibGUI/GMLSyntaxHighlighter.h>
#include <LibGUI/INISyntaxHighlighter.h>
#include <LibGUI/JSSyntaxHighlighter.h>
#include <LibGUI/Label.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGUI/ShellSyntaxHighlighter.h>
#include <LibGUI/SyntaxHighlighter.h>
#include <LibGUI/Window.h>
#include <LibMarkdown/Document.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/HTMLHeadElement.h>
#include <LibWeb/OutOfProcessWebView.h>
#include <fcntl.h>
// #define EDITOR_DEBUG
namespace HackStudio {
Editor::Editor()
{
set_document(CodeDocument::create());
m_documentation_tooltip_window = GUI::Window::construct();
m_documentation_tooltip_window->set_rect(0, 0, 500, 400);
m_documentation_tooltip_window->set_window_type(GUI::WindowType::Tooltip);
m_documentation_page_view = m_documentation_tooltip_window->set_main_widget<Web::OutOfProcessWebView>();
}
Editor::~Editor()
{
}
EditorWrapper& Editor::wrapper()
{
return static_cast<EditorWrapper&>(*parent());
}
const EditorWrapper& Editor::wrapper() const
{
return static_cast<const EditorWrapper&>(*parent());
}
void Editor::focusin_event(GUI::FocusEvent& event)
{
wrapper().set_editor_has_focus({}, true);
if (on_focus)
on_focus();
GUI::TextEditor::focusin_event(event);
}
void Editor::focusout_event(GUI::FocusEvent& event)
{
wrapper().set_editor_has_focus({}, false);
GUI::TextEditor::focusout_event(event);
}
Gfx::IntRect Editor::breakpoint_icon_rect(size_t line_number) const
{
auto ruler_line_rect = ruler_content_rect(line_number);
auto scroll_value = vertical_scrollbar().value();
ruler_line_rect = ruler_line_rect.translated({ 0, -scroll_value });
auto center = ruler_line_rect.center().translated({ ruler_line_rect.width() - 10, -line_spacing() - 3 });
constexpr int size = 32;
return { center.x() - size / 2, center.y() - size / 2, size, size };
}
void Editor::paint_event(GUI::PaintEvent& event)
{
GUI::TextEditor::paint_event(event);
GUI::Painter painter(*this);
if (is_focused()) {
painter.add_clip_rect(event.rect());
auto rect = frame_inner_rect();
if (vertical_scrollbar().is_visible())
rect.set_width(rect.width() - vertical_scrollbar().width());
if (horizontal_scrollbar().is_visible())
rect.set_height(rect.height() - horizontal_scrollbar().height());
painter.draw_rect(rect, palette().selection());
}
if (ruler_visible()) {
size_t first_visible_line = text_position_at(event.rect().top_left()).line();
size_t last_visible_line = text_position_at(event.rect().bottom_right()).line();
for (size_t line : breakpoint_lines()) {
if (line < first_visible_line || line > last_visible_line) {
continue;
}
const auto& icon = breakpoint_icon_bitmap();
painter.blit(breakpoint_icon_rect(line).center(), icon, icon.rect());
}
if (execution_position().has_value()) {
const auto& icon = current_position_icon_bitmap();
painter.blit(breakpoint_icon_rect(execution_position().value()).center(), icon, icon.rect());
}
}
}
static HashMap<String, String>& man_paths()
{
static HashMap<String, String> paths;
if (paths.is_empty()) {
// FIXME: This should also search man3, possibly other places..
Core::DirIterator it("/usr/share/man/man2", Core::DirIterator::Flags::SkipDots);
while (it.has_next()) {
auto path = String::formatted("/usr/share/man/man2/{}", it.next_path());
auto title = LexicalPath(path).title();
paths.set(title, path);
}
}
return paths;
}
void Editor::show_documentation_tooltip_if_available(const String& hovered_token, const Gfx::IntPoint& screen_location)
{
auto it = man_paths().find(hovered_token);
if (it == man_paths().end()) {
#ifdef EDITOR_DEBUG
dbgln("no man path for {}", hovered_token);
#endif
m_documentation_tooltip_window->hide();
return;
}
if (m_documentation_tooltip_window->is_visible() && hovered_token == m_last_parsed_token) {
return;
}
#ifdef EDITOR_DEBUG
dbgln("opening {}", it->value);
#endif
auto file = Core::File::construct(it->value);
if (!file->open(Core::File::ReadOnly)) {
dbgln("failed to open {}, {}", it->value, file->error_string());
return;
}
auto man_document = Markdown::Document::parse(file->read_all());
if (!man_document) {
dbgln("failed to parse markdown");
return;
}
StringBuilder html;
// FIXME: With the InProcessWebView we used to manipulate the document body directly,
// With OutOfProcessWebView this isn't possible (at the moment). The ideal solution
// is probably to tweak Markdown::Document::render_to_html() so we can inject styles
// into the rendered HTML easily.
html.append(man_document->render_to_html());
html.append("<style>body { background-color: #dac7b5; }</style>");
m_documentation_page_view->load_html(html.build(), {});
m_documentation_tooltip_window->move_to(screen_location.translated(4, 4));
m_documentation_tooltip_window->show();
m_last_parsed_token = hovered_token;
}
void Editor::mousemove_event(GUI::MouseEvent& event)
{
GUI::TextEditor::mousemove_event(event);
if (document().spans().is_empty())
return;
auto text_position = text_position_at(event.position());
if (!text_position.is_valid()) {
m_documentation_tooltip_window->hide();
return;
}
auto highlighter = wrapper().editor().syntax_highlighter();
if (!highlighter)
return;
bool hide_tooltip = true;
bool is_over_link = false;
auto ruler_line_rect = ruler_content_rect(text_position.line());
auto hovering_lines_ruler = (event.position().x() < ruler_line_rect.width());
if (hovering_lines_ruler && !is_in_drag_select())
set_override_cursor(Gfx::StandardCursor::Arrow);
else if (m_hovering_editor)
set_override_cursor(m_hovering_link && event.ctrl() ? Gfx::StandardCursor::Hand : Gfx::StandardCursor::IBeam);
for (auto& span : document().spans()) {
if (span.range.contains(m_previous_text_position) && !span.range.contains(text_position)) {
if (highlighter->is_navigatable(span.data) && span.attributes.underline) {
span.attributes.underline = false;
wrapper().editor().update();
}
}
if (span.range.contains(text_position)) {
auto adjusted_range = span.range;
auto end_line_length = document().line(span.range.end().line()).length();
adjusted_range.end().set_column(min(end_line_length, adjusted_range.end().column() + 1));
auto hovered_span_text = document().text_in_range(adjusted_range);
#ifdef EDITOR_DEBUG
dbgln("Hovering: {} \"{}\"", adjusted_range, hovered_span_text);
#endif
if (highlighter->is_navigatable(span.data)) {
is_over_link = true;
bool was_underlined = span.attributes.underline;
span.attributes.underline = event.modifiers() & Mod_Ctrl;
if (span.attributes.underline != was_underlined) {
wrapper().editor().update();
}
}
if (highlighter->is_identifier(span.data)) {
show_documentation_tooltip_if_available(hovered_span_text, event.position().translated(screen_relative_rect().location()));
hide_tooltip = false;
}
}
}
m_previous_text_position = text_position;
if (hide_tooltip)
m_documentation_tooltip_window->hide();
m_hovering_link = is_over_link && (event.modifiers() & Mod_Ctrl);
}
void Editor::mousedown_event(GUI::MouseEvent& event)
{
auto highlighter = wrapper().editor().syntax_highlighter();
if (!highlighter) {
GUI::TextEditor::mousedown_event(event);
return;
}
auto text_position = text_position_at(event.position());
auto ruler_line_rect = ruler_content_rect(text_position.line());
if (event.button() == GUI::MouseButton::Left && event.position().x() < ruler_line_rect.width()) {
if (!breakpoint_lines().contains_slow(text_position.line())) {
breakpoint_lines().append(text_position.line());
Debugger::on_breakpoint_change(wrapper().filename_label().text(), text_position.line(), BreakpointChange::Added);
} else {
breakpoint_lines().remove_first_matching([&](size_t line) { return line == text_position.line(); });
Debugger::on_breakpoint_change(wrapper().filename_label().text(), text_position.line(), BreakpointChange::Removed);
}
}
if (!(event.modifiers() & Mod_Ctrl)) {
GUI::TextEditor::mousedown_event(event);
return;
}
if (!text_position.is_valid()) {
GUI::TextEditor::mousedown_event(event);
return;
}
if (auto* span = document().span_at(text_position)) {
if (!highlighter->is_navigatable(span->data)) {
GUI::TextEditor::mousedown_event(event);
return;
}
auto adjusted_range = span->range;
adjusted_range.end().set_column(adjusted_range.end().column() + 1);
auto span_text = document().text_in_range(adjusted_range);
auto header_path = span_text.substring(1, span_text.length() - 2);
#ifdef EDITOR_DEBUG
dbgln("Ctrl+click: {} \"{}\"", adjusted_range, header_path);
#endif
navigate_to_include_if_available(header_path);
return;
}
GUI::TextEditor::mousedown_event(event);
}
void Editor::enter_event(Core::Event& event)
{
m_hovering_editor = true;
GUI::TextEditor::enter_event(event);
}
void Editor::leave_event(Core::Event& event)
{
m_hovering_editor = false;
GUI::TextEditor::leave_event(event);
}
static HashMap<String, String>& include_paths()
{
static HashMap<String, String> paths;
auto add_directory = [](String base, Optional<String> recursive, auto handle_directory) -> void {
Core::DirIterator it(recursive.value_or(base), Core::DirIterator::Flags::SkipDots);
while (it.has_next()) {
auto path = it.next_full_path();
if (!Core::File::is_directory(path)) {
auto key = path.substring(base.length() + 1, path.length() - base.length() - 1);
#ifdef EDITOR_DEBUG
dbgln("Adding header \"{}\" in path \"{}\"", key, path);
#endif
paths.set(key, path);
} else {
handle_directory(base, path, handle_directory);
}
}
};
if (paths.is_empty()) {
add_directory(".", {}, add_directory);
add_directory("/usr/local/include", {}, add_directory);
add_directory("/usr/local/include/c++/9.2.0", {}, add_directory);
add_directory("/usr/include", {}, add_directory);
}
return paths;
}
void Editor::navigate_to_include_if_available(String path)
{
auto it = include_paths().find(path);
if (it == include_paths().end()) {
#ifdef EDITOR_DEBUG
dbgln("no header {} found.", path);
#endif
return;
}
on_open(it->value);
}
void Editor::set_execution_position(size_t line_number)
{
code_document().set_execution_position(line_number);
scroll_position_into_view({ line_number, 0 });
update(breakpoint_icon_rect(line_number));
}
void Editor::clear_execution_position()
{
if (!execution_position().has_value()) {
return;
}
size_t previous_position = execution_position().value();
code_document().clear_execution_position();
update(breakpoint_icon_rect(previous_position));
}
const Gfx::Bitmap& Editor::breakpoint_icon_bitmap()
{
static auto bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/breakpoint.png");
return *bitmap;
}
const Gfx::Bitmap& Editor::current_position_icon_bitmap()
{
static auto bitmap = Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png");
return *bitmap;
}
const CodeDocument& Editor::code_document() const
{
const auto& doc = document();
ASSERT(doc.is_code_document());
return static_cast<const CodeDocument&>(doc);
}
CodeDocument& Editor::code_document()
{
return const_cast<CodeDocument&>(static_cast<const Editor&>(*this).code_document());
}
void Editor::set_document(GUI::TextDocument& doc)
{
ASSERT(doc.is_code_document());
GUI::TextEditor::set_document(doc);
set_override_cursor(Gfx::StandardCursor::IBeam);
CodeDocument& code_document = static_cast<CodeDocument&>(doc);
switch (code_document.language()) {
case Language::Cpp:
set_syntax_highlighter(make<GUI::CppSyntaxHighlighter>());
m_language_client = get_language_client<LanguageClients::Cpp::ServerConnection>(project().root_path());
break;
case Language::GML:
set_syntax_highlighter(make<GUI::GMLSyntaxHighlighter>());
break;
case Language::JavaScript:
set_syntax_highlighter(make<GUI::JSSyntaxHighlighter>());
break;
case Language::Ini:
set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>());
break;
case Language::Shell:
set_syntax_highlighter(make<GUI::ShellSyntaxHighlighter>());
m_language_client = get_language_client<LanguageClients::Shell::ServerConnection>(project().root_path());
break;
default:
set_syntax_highlighter({});
}
if (m_language_client) {
set_autocomplete_provider(make<LanguageServerAidedAutocompleteProvider>(*m_language_client));
dbgln("Opening {}", code_document.file_path());
int fd = open(code_document.file_path().characters(), O_RDONLY | O_NOCTTY);
if (fd < 0) {
perror("open");
return;
}
m_language_client->open_file(code_document.file_path(), fd);
close(fd);
}
}
Optional<Editor::AutoCompleteRequestData> Editor::get_autocomplete_request_data()
{
if (!wrapper().editor().m_language_client)
return {};
return Editor::AutoCompleteRequestData { cursor() };
}
void Editor::LanguageServerAidedAutocompleteProvider::provide_completions(Function<void(Vector<Entry>)> callback)
{
auto& editor = static_cast<Editor&>(*m_editor).wrapper().editor();
auto data = editor.get_autocomplete_request_data();
if (!data.has_value())
callback({});
m_language_client.on_autocomplete_suggestions = [callback = move(callback)](auto suggestions) {
callback(suggestions);
};
m_language_client.request_autocomplete(
editor.code_document().file_path(),
data.value().position.line(),
data.value().position.column());
}
void Editor::on_edit_action(const GUI::Command& command)
{
if (!m_language_client)
return;
if (command.is_insert_text()) {
const GUI::InsertTextCommand& insert_command = static_cast<const GUI::InsertTextCommand&>(command);
m_language_client->insert_text(
code_document().file_path(),
insert_command.text(),
insert_command.range().start().line(),
insert_command.range().start().column());
return;
}
if (command.is_remove_text()) {
const GUI::RemoveTextCommand& remove_command = static_cast<const GUI::RemoveTextCommand&>(command);
m_language_client->remove_text(
code_document().file_path(),
remove_command.range().start().line(),
remove_command.range().start().column(),
remove_command.range().end().line(),
remove_command.range().end().column());
return;
}
ASSERT_NOT_REACHED();
}
void Editor::undo()
{
TextEditor::undo();
flush_file_content_to_langauge_server();
}
void Editor::redo()
{
TextEditor::redo();
flush_file_content_to_langauge_server();
}
void Editor::flush_file_content_to_langauge_server()
{
if (!m_language_client)
return;
m_language_client->set_file_content(
code_document().file_path(),
document().text());
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "CodeDocument.h"
#include "Debugger/BreakpointCallback.h"
#include "LanguageClient.h"
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <LibGUI/TextEditor.h>
#include <LibWeb/Forward.h>
namespace HackStudio {
class EditorWrapper;
class Editor final : public GUI::TextEditor {
C_OBJECT(Editor)
public:
virtual ~Editor() override;
Function<void()> on_focus;
Function<void(String)> on_open;
EditorWrapper& wrapper();
const EditorWrapper& wrapper() const;
const Vector<size_t>& breakpoint_lines() const { return code_document().breakpoint_lines(); }
Vector<size_t>& breakpoint_lines() { return code_document().breakpoint_lines(); }
Optional<size_t> execution_position() const { return code_document().execution_position(); }
void set_execution_position(size_t line_number);
void clear_execution_position();
const CodeDocument& code_document() const;
CodeDocument& code_document();
virtual void set_document(GUI::TextDocument&) override;
virtual void on_edit_action(const GUI::Command&) override;
virtual void undo() override;
virtual void redo() override;
private:
virtual void focusin_event(GUI::FocusEvent&) override;
virtual void focusout_event(GUI::FocusEvent&) override;
virtual void paint_event(GUI::PaintEvent&) override;
virtual void mousemove_event(GUI::MouseEvent&) override;
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void enter_event(Core::Event&) override;
virtual void leave_event(Core::Event&) override;
void show_documentation_tooltip_if_available(const String&, const Gfx::IntPoint& screen_location);
void navigate_to_include_if_available(String);
Gfx::IntRect breakpoint_icon_rect(size_t line_number) const;
static const Gfx::Bitmap& breakpoint_icon_bitmap();
static const Gfx::Bitmap& current_position_icon_bitmap();
struct AutoCompleteRequestData {
GUI::TextPosition position;
};
class LanguageServerAidedAutocompleteProvider final : virtual public GUI::AutocompleteProvider {
public:
LanguageServerAidedAutocompleteProvider(LanguageClient& client)
: m_language_client(client)
{
}
virtual ~LanguageServerAidedAutocompleteProvider() override { }
private:
virtual void provide_completions(Function<void(Vector<Entry>)> callback) override;
LanguageClient& m_language_client;
};
Optional<AutoCompleteRequestData> get_autocomplete_request_data();
void flush_file_content_to_langauge_server();
explicit Editor();
RefPtr<GUI::Window> m_documentation_tooltip_window;
RefPtr<Web::OutOfProcessWebView> m_documentation_page_view;
String m_last_parsed_token;
GUI::TextPosition m_previous_text_position { 0, 0 };
bool m_hovering_editor { false };
bool m_hovering_link { false };
bool m_autocomplete_in_focus { false };
OwnPtr<LanguageClient> m_language_client;
};
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "EditorWrapper.h"
#include "Editor.h"
#include "HackStudio.h"
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
namespace HackStudio {
EditorWrapper::EditorWrapper()
{
set_layout<GUI::VerticalBoxLayout>();
auto& label_wrapper = add<GUI::Widget>();
label_wrapper.set_fixed_height(14);
label_wrapper.set_fill_with_background_color(true);
label_wrapper.set_layout<GUI::HorizontalBoxLayout>();
label_wrapper.layout()->set_margins({ 2, 0, 2, 0 });
m_filename_label = label_wrapper.add<GUI::Label>("(Untitled)");
m_filename_label->set_text_alignment(Gfx::TextAlignment::CenterLeft);
m_filename_label->set_fixed_height(14);
m_cursor_label = label_wrapper.add<GUI::Label>("(Cursor)");
m_cursor_label->set_text_alignment(Gfx::TextAlignment::CenterRight);
m_cursor_label->set_fixed_height(14);
m_editor = add<Editor>();
m_editor->set_ruler_visible(true);
m_editor->set_line_wrapping_enabled(true);
m_editor->set_automatic_indentation_enabled(true);
m_editor->on_cursor_change = [this] {
m_cursor_label->set_text(String::formatted("Line: {}, Column: {}", m_editor->cursor().line() + 1, m_editor->cursor().column()));
};
m_editor->on_focus = [this] {
set_current_editor_wrapper(this);
};
m_editor->on_open = [](String path) {
open_file(path);
};
}
EditorWrapper::~EditorWrapper()
{
}
void EditorWrapper::set_editor_has_focus(Badge<Editor>, bool focus)
{
m_filename_label->set_font(focus ? Gfx::FontDatabase::default_bold_font() : Gfx::FontDatabase::default_font());
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Debugger/BreakpointCallback.h"
#include <AK/Function.h>
#include <AK/Vector.h>
#include <LibGUI/Widget.h>
#include <string.h>
namespace HackStudio {
class Editor;
class EditorWrapper : public GUI::Widget {
C_OBJECT(EditorWrapper)
public:
virtual ~EditorWrapper() override;
Editor& editor() { return *m_editor; }
const Editor& editor() const { return *m_editor; }
GUI::Label& filename_label() { return *m_filename_label; }
const GUI::Label& filename_label() const { return *m_filename_label; }
void set_editor_has_focus(Badge<Editor>, bool);
private:
EditorWrapper();
RefPtr<GUI::Label> m_filename_label;
RefPtr<GUI::Label> m_cursor_label;
RefPtr<Editor> m_editor;
};
}

View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "FindInFilesWidget.h"
#include "HackStudio.h"
#include "Project.h"
#include <AK/StringBuilder.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextBox.h>
#include <LibGfx/FontDatabase.h>
namespace HackStudio {
struct Match {
String filename;
GUI::TextRange range;
String text;
};
class SearchResultsModel final : public GUI::Model {
public:
enum Column {
Filename,
Location,
MatchedText,
__Count
};
explicit SearchResultsModel(const Vector<Match>&& matches)
: m_matches(move(matches))
{
}
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_matches.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Count; }
virtual String column_name(int column) const override
{
switch (column) {
case Column::Filename:
return "Filename";
case Column::Location:
return "#";
case Column::MatchedText:
return "Text";
default:
ASSERT_NOT_REACHED();
}
}
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
{
if (role == GUI::ModelRole::TextAlignment)
return Gfx::TextAlignment::CenterLeft;
if (role == GUI::ModelRole::Font) {
if (index.column() == Column::MatchedText)
return Gfx::FontDatabase::default_fixed_width_font();
return {};
}
if (role == GUI::ModelRole::Display) {
auto& match = m_matches.at(index.row());
switch (index.column()) {
case Column::Filename:
return match.filename;
case Column::Location:
return (int)match.range.start().line();
case Column::MatchedText:
return match.text;
}
}
return {};
}
virtual void update() override { }
virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& = GUI::ModelIndex()) const override
{
if (row < 0 || row >= (int)m_matches.size())
return {};
if (column < 0 || column >= Column::__Count)
return {};
return create_index(row, column, &m_matches.at(row));
}
private:
Vector<Match> m_matches;
};
static RefPtr<SearchResultsModel> find_in_files(const StringView& text)
{
Vector<Match> matches;
project().for_each_text_file([&](auto& file) {
auto matches_in_file = file.document().find_all(text);
for (auto& range : matches_in_file) {
auto whole_line_range = file.document().range_for_entire_line(range.start().line());
auto whole_line_containing_match = file.document().text_in_range(whole_line_range);
auto left_part = whole_line_containing_match.substring(0, range.start().column());
auto right_part = whole_line_containing_match.substring(range.end().column(), whole_line_containing_match.length() - range.end().column());
StringBuilder builder;
builder.append(left_part);
builder.append(0x01);
builder.append(file.document().text_in_range(range));
builder.append(0x02);
builder.append(right_part);
matches.append({ file.name(), range, builder.to_string() });
}
});
return adopt(*new SearchResultsModel(move(matches)));
}
FindInFilesWidget::FindInFilesWidget()
{
set_layout<GUI::VerticalBoxLayout>();
auto& top_container = add<Widget>();
top_container.set_layout<GUI::HorizontalBoxLayout>();
top_container.set_fixed_height(20);
m_textbox = top_container.add<GUI::TextBox>();
m_button = top_container.add<GUI::Button>("Find in files");
m_button->set_fixed_width(100);
m_result_view = add<GUI::TableView>();
m_result_view->on_activation = [](auto& index) {
auto& match = *(const Match*)index.internal_data();
open_file(match.filename);
current_editor().set_selection(match.range);
current_editor().set_focus(true);
};
m_button->on_click = [this](auto) {
auto results_model = find_in_files(m_textbox->text());
m_result_view->set_model(results_model);
};
m_textbox->on_return_pressed = [this] {
m_button->click();
};
}
void FindInFilesWidget::focus_textbox_and_select_all()
{
m_textbox->select_all();
m_textbox->set_focus(true);
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibGUI/Button.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Widget.h>
namespace HackStudio {
class FindInFilesWidget final : public GUI::Widget {
C_OBJECT(FindInFilesWidget)
public:
virtual ~FindInFilesWidget() override { }
void focus_textbox_and_select_all();
private:
explicit FindInFilesWidget();
RefPtr<GUI::TextBox> m_textbox;
RefPtr<GUI::Button> m_button;
RefPtr<GUI::TableView> m_result_view;
};
}

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "FormEditorWidget.h"
#include "CursorTool.h"
#include "FormWidget.h"
#include "WidgetTreeModel.h"
#include <LibGUI/Painter.h>
namespace HackStudio {
FormEditorWidget::FormEditorWidget()
: m_tool(make<CursorTool>(*this))
{
set_fill_with_background_color(true);
m_form_widget = add<FormWidget>();
m_widget_tree_model = WidgetTreeModel::create(*m_form_widget);
}
FormEditorWidget::~FormEditorWidget()
{
}
void FormEditorWidget::paint_event(GUI::PaintEvent& event)
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
}
void FormEditorWidget::set_tool(NonnullOwnPtr<Tool> tool)
{
m_tool->detach();
m_tool = move(tool);
m_tool->attach();
}
WidgetTreeModel& FormEditorWidget::model()
{
return *m_widget_tree_model;
}
}

View file

@ -0,0 +1,137 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/HashTable.h>
#include <LibGUI/ScrollableWidget.h>
namespace HackStudio {
class FormWidget;
class Tool;
class WidgetTreeModel;
class FormEditorWidget final : public GUI::ScrollableWidget {
C_OBJECT(FormEditorWidget)
public:
virtual ~FormEditorWidget() override;
FormWidget& form_widget() { return *m_form_widget; }
const FormWidget& form_widget() const { return *m_form_widget; }
Tool& tool() { return *m_tool; }
const Tool& tool() const { return *m_tool; }
void set_tool(NonnullOwnPtr<Tool>);
WidgetTreeModel& model();
class WidgetSelection {
public:
Function<void(GUI::Widget&)> on_remove;
Function<void(GUI::Widget&)> on_add;
Function<void()> on_clear;
void enable_hooks() { m_hooks_enabled = true; }
void disable_hooks() { m_hooks_enabled = false; }
bool is_empty() const
{
return m_widgets.is_empty();
}
bool contains(GUI::Widget& widget) const
{
return m_widgets.contains(&widget);
}
void toggle(GUI::Widget& widget)
{
if (contains(widget))
remove(widget);
else
add(widget);
}
void set(GUI::Widget& widget)
{
clear();
add(widget);
}
void remove(GUI::Widget& widget)
{
ASSERT(m_widgets.contains(&widget));
m_widgets.remove(&widget);
if (m_hooks_enabled && on_remove)
on_remove(widget);
}
void add(GUI::Widget& widget)
{
m_widgets.set(&widget);
if (m_hooks_enabled && on_add)
on_add(widget);
}
void clear()
{
m_widgets.clear();
if (m_hooks_enabled && on_clear)
on_clear();
}
template<typename Callback>
void for_each(Callback callback)
{
for (auto& it : m_widgets) {
if (callback(*it) == IterationDecision::Break)
break;
}
}
WidgetSelection() { }
private:
HashTable<GUI::Widget*> m_widgets;
bool m_hooks_enabled { true };
};
WidgetSelection& selection() { return m_selection; }
private:
virtual void paint_event(GUI::PaintEvent&) override;
FormEditorWidget();
RefPtr<FormWidget> m_form_widget;
RefPtr<WidgetTreeModel> m_widget_tree_model;
NonnullOwnPtr<Tool> m_tool;
WidgetSelection m_selection;
};
}

View file

@ -0,0 +1,106 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "FormWidget.h"
#include "FormEditorWidget.h"
#include "Tool.h"
#include <LibGUI/Painter.h>
namespace HackStudio {
FormWidget::FormWidget()
{
set_focus_policy(GUI::FocusPolicy::StrongFocus);
set_fill_with_background_color(true);
set_relative_rect(5, 5, 400, 300);
set_greedy_for_hits(true);
}
FormWidget::~FormWidget()
{
}
FormEditorWidget& FormWidget::editor()
{
return static_cast<FormEditorWidget&>(*parent());
}
const FormEditorWidget& FormWidget::editor() const
{
return static_cast<const FormEditorWidget&>(*parent());
}
void FormWidget::paint_event(GUI::PaintEvent& event)
{
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
for (int y = 0; y < height(); y += m_grid_size) {
for (int x = 0; x < width(); x += m_grid_size) {
painter.set_pixel({ x, y }, Color::from_rgb(0x404040));
}
}
}
void FormWidget::second_paint_event(GUI::PaintEvent& event)
{
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
if (!editor().selection().is_empty()) {
for_each_child_widget([&](auto& child) {
if (editor().selection().contains(child)) {
painter.draw_rect(child.relative_rect(), Color::Blue);
}
return IterationDecision::Continue;
});
}
editor().tool().on_second_paint(painter, event);
}
void FormWidget::mousedown_event(GUI::MouseEvent& event)
{
editor().tool().on_mousedown(event);
}
void FormWidget::mouseup_event(GUI::MouseEvent& event)
{
editor().tool().on_mouseup(event);
}
void FormWidget::mousemove_event(GUI::MouseEvent& event)
{
editor().tool().on_mousemove(event);
}
void FormWidget::keydown_event(GUI::KeyEvent& event)
{
editor().tool().on_keydown(event);
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Badge.h>
#include <LibGUI/Widget.h>
namespace HackStudio {
class CursorTool;
class FormEditorWidget;
class FormWidget final : public GUI::Widget {
C_OBJECT(FormWidget)
public:
virtual ~FormWidget() override;
FormEditorWidget& editor();
const FormEditorWidget& editor() const;
// FIXME: This should be an app-wide preference instead.
int grid_size() const { return m_grid_size; }
private:
virtual void paint_event(GUI::PaintEvent&) override;
virtual void second_paint_event(GUI::PaintEvent&) override;
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual void mouseup_event(GUI::MouseEvent&) override;
virtual void mousemove_event(GUI::MouseEvent&) override;
virtual void keydown_event(GUI::KeyEvent&) override;
FormWidget();
int m_grid_size { 5 };
};
}

View file

@ -0,0 +1,253 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "DiffViewer.h"
#include <LibDiff/Hunks.h>
#include <LibGUI/AbstractView.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGfx/Color.h>
#include <LibGfx/Font.h>
#include <LibGfx/FontDatabase.h>
#include <LibGfx/Palette.h>
// #define DEBUG_DIFF
namespace HackStudio {
DiffViewer::~DiffViewer()
{
}
void DiffViewer::paint_event(GUI::PaintEvent& event)
{
GUI::Painter painter(*this);
painter.add_clip_rect(widget_inner_rect());
painter.add_clip_rect(event.rect());
painter.fill_rect(event.rect(), palette().color(background_role()));
painter.translate(frame_thickness(), frame_thickness());
painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
// Why we need to translate here again? We've already translated the painter.
// Anyways, it paints correctly so I'll leave it like this.
painter.fill_rect_with_dither_pattern(
separator_rect().translated(horizontal_scrollbar().value(), vertical_scrollbar().value()),
Gfx::Color::LightGray,
Gfx::Color::White);
size_t y_offset = 10;
size_t current_original_line_index = 0;
for (const auto& hunk : m_hunks) {
for (size_t i = current_original_line_index; i < hunk.original_start_line; ++i) {
draw_line(painter, m_original_lines[i], y_offset, LinePosition::Both, LineType::Normal);
y_offset += line_height();
}
current_original_line_index = hunk.original_start_line + hunk.removed_lines.size();
size_t left_y_offset = y_offset;
for (const auto& removed_line : hunk.removed_lines) {
draw_line(painter, removed_line, left_y_offset, LinePosition::Left, LineType::Diff);
left_y_offset += line_height();
}
for (int i = 0; i < (int)hunk.added_lines.size() - (int)hunk.removed_lines.size(); ++i) {
draw_line(painter, "", left_y_offset, LinePosition::Left, LineType::Missing);
left_y_offset += line_height();
}
size_t right_y_offset = y_offset;
for (const auto& added_line : hunk.added_lines) {
draw_line(painter, added_line, right_y_offset, LinePosition::Right, LineType::Diff);
right_y_offset += line_height();
}
for (int i = 0; i < (int)hunk.removed_lines.size() - (int)hunk.added_lines.size(); ++i) {
draw_line(painter, "", right_y_offset, LinePosition::Right, LineType::Missing);
right_y_offset += line_height();
}
ASSERT(left_y_offset == right_y_offset);
y_offset = left_y_offset;
}
for (size_t i = current_original_line_index; i < m_original_lines.size(); ++i) {
draw_line(painter, m_original_lines[i], y_offset, LinePosition::Both, LineType::Normal);
y_offset += line_height();
}
}
void DiffViewer::draw_line(GUI::Painter& painter, const String& line, size_t y_offset, LinePosition line_position, LineType line_type)
{
size_t line_width = font().width(line);
constexpr size_t padding = 10;
size_t left_side_x_offset = padding;
size_t right_side_x_offset = separator_rect().x() + padding;
// FIXME: Long lines will overflow out of their side of the diff view
Gfx::IntRect left_line_rect { (int)left_side_x_offset, (int)y_offset, (int)line_width, (int)line_height() };
Gfx::IntRect right_line_rect { (int)right_side_x_offset, (int)y_offset, (int)line_width, (int)line_height() };
auto color = palette().color(foreground_role());
if (line_position == LinePosition::Left || line_position == LinePosition::Both) {
painter.draw_text(left_line_rect, line, Gfx::TextAlignment::TopLeft, color);
if (line_type != LineType::Normal) {
Gfx::IntRect outline = { (int)left_side_x_offset, ((int)y_offset) - 2, separator_rect().x() - (int)(padding * 2), (int)line_height() };
if (line_type == LineType::Diff) {
painter.fill_rect(
outline,
red_background());
}
if (line_type == LineType::Missing) {
painter.fill_rect(
outline,
gray_background());
}
}
}
if (line_position == LinePosition::Right || line_position == LinePosition::Both) {
painter.draw_text(right_line_rect, line, Gfx::TextAlignment::TopLeft, color);
if (line_type != LineType::Normal) {
Gfx::IntRect outline = { (int)right_side_x_offset, ((int)y_offset) - 2, frame_inner_rect().width() - separator_rect().x() - (int)(padding * 2) - 10, (int)line_height() };
if (line_type == LineType::Diff) {
painter.fill_rect(
outline,
green_background());
}
if (line_type == LineType::Missing) {
painter.fill_rect(
outline,
gray_background());
}
}
}
}
size_t DiffViewer::line_height() const
{
return font().glyph_height() + 4;
}
Gfx::IntRect DiffViewer::separator_rect() const
{
return Gfx::IntRect { frame_inner_rect().width() / 2 - 2,
0,
4,
frame_inner_rect().height() };
}
void DiffViewer::set_content(const String& original, const String& diff)
{
m_original_lines = split_to_lines(original);
m_hunks = Diff::parse_hunks(diff);
#ifdef DEBUG_DIFF
for (size_t i = 0; i < m_original_lines.size(); ++i)
dbgln("{}:{}", i, m_original_lines[i]);
#endif
}
DiffViewer::DiffViewer()
{
setup_properties();
}
DiffViewer::DiffViewer(const String& original, const String& diff)
: m_original_lines(split_to_lines(original))
, m_hunks(Diff::parse_hunks(diff))
{
setup_properties();
}
void DiffViewer::setup_properties()
{
set_font(Gfx::FontDatabase::default_fixed_width_font());
set_background_role(ColorRole::Base);
set_foreground_role(ColorRole::BaseText);
}
Vector<String> DiffViewer::split_to_lines(const String& text)
{
// NOTE: This is slightly different than text.split('\n')
Vector<String> lines;
size_t next_line_start_index = 0;
for (size_t i = 0; i < text.length(); ++i) {
if (text[i] == '\n') {
auto line_text = text.substring(next_line_start_index, i - next_line_start_index);
lines.append(move(line_text));
next_line_start_index = i + 1;
}
}
lines.append(text.substring(next_line_start_index, text.length() - next_line_start_index));
return lines;
}
Gfx::Color DiffViewer::red_background()
{
static Gfx::Color color = Gfx::Color::from_rgba(0x88ff0000);
return color;
}
Gfx::Color DiffViewer::green_background()
{
static Gfx::Color color = Gfx::Color::from_rgba(0x8800ff00);
return color;
}
Gfx::Color DiffViewer::gray_background()
{
static Gfx::Color color = Gfx::Color::from_rgba(0x88888888);
return color;
}
void DiffViewer::update_content_size()
{
if (m_hunks.is_empty()) {
set_content_size({ 0, 0 });
return;
}
size_t num_lines = 0;
size_t current_original_line_index = 0;
for (const auto& hunk : m_hunks) {
num_lines += ((int)hunk.original_start_line - (int)current_original_line_index);
num_lines += hunk.removed_lines.size();
if (hunk.added_lines.size() > hunk.removed_lines.size()) {
num_lines += ((int)hunk.added_lines.size() - (int)hunk.removed_lines.size());
}
current_original_line_index = hunk.original_start_line + hunk.removed_lines.size();
}
num_lines += ((int)m_original_lines.size() - (int)current_original_line_index);
// TODO: Support Horizontal scrolling
set_content_size({ 0, (int)(num_lines * line_height()) });
}
void DiffViewer::resize_event(GUI::ResizeEvent& event)
{
ScrollableWidget::resize_event(event);
update_content_size();
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibDiff/Hunks.h>
#include <LibGUI/ScrollableWidget.h>
namespace HackStudio {
class DiffViewer final : public GUI::ScrollableWidget {
C_OBJECT(DiffViewer)
public:
virtual ~DiffViewer() override;
void set_content(const String& original, const String& diff);
private:
DiffViewer(const String& original, const String& diff);
DiffViewer();
void setup_properties();
virtual void paint_event(GUI::PaintEvent&) override;
virtual void resize_event(GUI::ResizeEvent&) override;
void update_content_size();
enum class LinePosition {
Left,
Right,
Both,
};
enum class LineType {
Normal,
Diff,
Missing,
};
void draw_line(GUI::Painter&, const String& line, size_t y_offset, LinePosition, LineType);
static Vector<String> split_to_lines(const String& text);
static Gfx::Color red_background();
static Gfx::Color green_background();
static Gfx::Color gray_background();
size_t line_height() const;
Gfx::IntRect separator_rect() const;
Vector<String> m_original_lines;
Vector<Diff::Hunk> m_hunks;
};
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "GitFilesModel.h"
namespace HackStudio {
NonnullRefPtr<GitFilesModel> GitFilesModel::create(Vector<LexicalPath>&& files)
{
return adopt(*new GitFilesModel(move(files)));
}
GitFilesModel::GitFilesModel(Vector<LexicalPath>&& files)
: m_files(move(files))
{
}
GUI::Variant GitFilesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
if (role == GUI::ModelRole::Display) {
return m_files.at(index.row()).string();
}
return {};
}
GUI::ModelIndex GitFilesModel::index(int row, int column, const GUI::ModelIndex&) const
{
if (row < 0 || row >= static_cast<int>(m_files.size()))
return {};
return create_index(row, column, &m_files.at(row));
}
};

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "GitRepo.h"
#include <AK/LexicalPath.h>
#include <AK/NonnullRefPtr.h>
#include <LibGUI/Model.h>
namespace HackStudio {
class GitFilesModel final : public GUI::Model {
public:
static NonnullRefPtr<GitFilesModel> create(Vector<LexicalPath>&& files);
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_files.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; }
virtual String column_name(int) const override
{
return "";
}
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual void update() override { }
virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex&) const override;
private:
explicit GitFilesModel(Vector<LexicalPath>&& files);
Vector<LexicalPath> m_files;
};
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "GitFilesView.h"
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ScrollBar.h>
#include <LibGfx/Palette.h>
namespace HackStudio {
GitFilesView::~GitFilesView()
{
}
void GitFilesView::paint_list_item(GUI::Painter& painter, int row_index, int painted_item_index)
{
ListView::paint_list_item(painter, row_index, painted_item_index);
painter.blit(action_icon_rect((size_t)painted_item_index).top_left(), *m_action_icon, m_action_icon->rect());
}
Gfx::IntRect GitFilesView::action_icon_rect(size_t painted_item_index)
{
int y = painted_item_index * item_height();
return { content_width() - 20, y, m_action_icon->rect().width(), m_action_icon->rect().height() };
}
GitFilesView::GitFilesView(GitFileActionCallback callback, NonnullRefPtr<Gfx::Bitmap> action_icon)
: m_action_callback(move(callback))
, m_action_icon(action_icon)
{
set_alternating_row_colors(false);
}
void GitFilesView::mousedown_event(GUI::MouseEvent& event)
{
if (event.button() != GUI::MouseButton::Left) {
ListView::mousedown_event(event);
return;
}
if (event.x() < action_icon_rect(0).x() || event.x() > action_icon_rect(0).top_right().x()) {
ListView::mousedown_event(event);
return;
}
size_t item_index = (event.y() + vertical_scrollbar().value()) / item_height();
if (model()->row_count() == 0 || item_index > (size_t)model()->row_count()) {
ListView::mousedown_event(event);
return;
}
auto data = model()->index(item_index, model_column()).data();
ASSERT(data.is_string());
m_action_callback(LexicalPath(data.to_string()));
}
};

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/LexicalPath.h>
#include <LibGUI/ListView.h>
#include <LibGfx/Bitmap.h>
namespace HackStudio {
// A "GitFileAction" is either the staging or the unstaging of a file.
typedef Function<void(const LexicalPath& file)> GitFileActionCallback;
class GitFilesView : public GUI::ListView {
C_OBJECT(GitFilesView)
public:
virtual ~GitFilesView() override;
protected:
GitFilesView(GitFileActionCallback, NonnullRefPtr<Gfx::Bitmap> action_icon);
private:
virtual void paint_list_item(GUI::Painter& painter, int row_index, int painted_item_index);
virtual void mousedown_event(GUI::MouseEvent&) override;
virtual Gfx::IntRect action_icon_rect(size_t painted_item_index);
GitFileActionCallback m_action_callback;
NonnullRefPtr<Gfx::Bitmap> m_action_icon;
};
}

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "GitRepo.h"
#include <LibCore/Command.h>
#include <stdio.h>
#include <stdlib.h>
namespace HackStudio {
GitRepo::CreateResult GitRepo::try_to_create(const LexicalPath& repository_root)
{
if (!git_is_installed()) {
return { CreateResult::Type::GitProgramNotFound, nullptr };
}
if (!git_repo_exists(repository_root)) {
return { CreateResult::Type::NoGitRepo, nullptr };
}
return { CreateResult::Type::Success, adopt(*new GitRepo(repository_root)) };
}
RefPtr<GitRepo> GitRepo::initialize_repository(const LexicalPath& repository_root)
{
auto res = command_wrapper({ "init" }, repository_root);
if (res.is_null())
return {};
ASSERT(git_repo_exists(repository_root));
return adopt(*new GitRepo(repository_root));
}
Vector<LexicalPath> GitRepo::unstaged_files() const
{
auto modified = modified_files();
auto untracked = untracked_files();
modified.append(move(untracked));
return modified;
}
//
Vector<LexicalPath> GitRepo::staged_files() const
{
auto raw_result = command({ "diff", "--cached", "--name-only" });
if (raw_result.is_null())
return {};
return parse_files_list(raw_result);
}
Vector<LexicalPath> GitRepo::modified_files() const
{
auto raw_result = command({ "ls-files", "--modified", "--exclude-standard" });
if (raw_result.is_null())
return {};
return parse_files_list(raw_result);
}
Vector<LexicalPath> GitRepo::untracked_files() const
{
auto raw_result = command({ "ls-files", "--others", "--exclude-standard" });
if (raw_result.is_null())
return {};
return parse_files_list(raw_result);
}
Vector<LexicalPath> GitRepo::parse_files_list(const String& raw_result)
{
auto lines = raw_result.split('\n');
Vector<LexicalPath> files;
for (const auto& line : lines) {
files.empend(line);
}
return files;
}
String GitRepo::command(const Vector<String>& command_parts) const
{
return command_wrapper(command_parts, m_repository_root);
}
String GitRepo::command_wrapper(const Vector<String>& command_parts, const LexicalPath& chdir)
{
return Core::command("git", command_parts, chdir);
}
bool GitRepo::git_is_installed()
{
return !command_wrapper({ "--help" }, LexicalPath("/")).is_null();
}
bool GitRepo::git_repo_exists(const LexicalPath& repo_root)
{
return !command_wrapper({ "status" }, repo_root).is_null();
}
bool GitRepo::stage(const LexicalPath& file)
{
return !command({ "add", file.string() }).is_null();
}
bool GitRepo::unstage(const LexicalPath& file)
{
return !command({ "reset", "HEAD", "--", file.string() }).is_null();
}
bool GitRepo::commit(const String& message)
{
return !command({ "commit", "-m", message }).is_null();
}
Optional<String> GitRepo::original_file_content(const LexicalPath& file) const
{
return command({ "show", String::formatted("HEAD:{}", file) });
}
Optional<String> GitRepo::unstaged_diff(const LexicalPath& file) const
{
return command({ "diff", file.string().characters() });
}
bool GitRepo::is_tracked(const LexicalPath& file) const
{
auto res = command({ "ls-files", file.string() });
if (res.is_null())
return false;
return !res.is_empty();
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/LexicalPath.h>
#include <AK/Optional.h>
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
#include <AK/Vector.h>
namespace HackStudio {
class GitRepo final : public RefCounted<GitRepo> {
public:
struct CreateResult {
enum class Type {
Success,
NoGitRepo,
GitProgramNotFound,
};
Type type;
RefPtr<GitRepo> repo;
};
static CreateResult try_to_create(const LexicalPath& repository_root);
static RefPtr<GitRepo> initialize_repository(const LexicalPath& repository_root);
Vector<LexicalPath> unstaged_files() const;
Vector<LexicalPath> staged_files() const;
bool stage(const LexicalPath& file);
bool unstage(const LexicalPath& file);
bool commit(const String& message);
Optional<String> original_file_content(const LexicalPath& file) const;
Optional<String> unstaged_diff(const LexicalPath& file) const;
bool is_tracked(const LexicalPath& file) const;
private:
static String command_wrapper(const Vector<String>& command_parts, const LexicalPath& chdir);
static bool git_is_installed();
static bool git_repo_exists(const LexicalPath& repo_root);
static Vector<LexicalPath> parse_files_list(const String&);
explicit GitRepo(const LexicalPath& repository_root)
: m_repository_root(repository_root)
{
}
Vector<LexicalPath> modified_files() const;
Vector<LexicalPath> untracked_files() const;
String command(const Vector<String>& command_parts) const;
LexicalPath m_repository_root;
};
}

View file

@ -0,0 +1,188 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "GitWidget.h"
#include "GitFilesModel.h"
#include <AK/LogStream.h>
#include <LibCore/File.h>
#include <LibDiff/Format.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGfx/Bitmap.h>
#include <stdio.h>
namespace HackStudio {
GitWidget::GitWidget(const LexicalPath& repo_root)
: m_repo_root(repo_root)
{
set_layout<GUI::HorizontalBoxLayout>();
auto& unstaged = add<GUI::Widget>();
unstaged.set_layout<GUI::VerticalBoxLayout>();
auto& unstaged_header = unstaged.add<GUI::Widget>();
unstaged_header.set_layout<GUI::HorizontalBoxLayout>();
auto& refresh_button = unstaged_header.add<GUI::Button>();
refresh_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/reload.png"));
refresh_button.set_fixed_size(16, 16);
refresh_button.set_tooltip("refresh");
refresh_button.on_click = [this](int) { refresh(); };
auto& unstaged_label = unstaged_header.add<GUI::Label>();
unstaged_label.set_text("Unstaged");
unstaged_header.set_fixed_height(20);
m_unstaged_files = unstaged.add<GitFilesView>(
[this](const auto& file) { stage_file(file); },
Gfx::Bitmap::load_from_file("/res/icons/16x16/plus.png").release_nonnull());
m_unstaged_files->on_selection = [this](const GUI::ModelIndex& index) {
const auto& selected = index.data().as_string();
show_diff(LexicalPath(selected));
};
auto& staged = add<GUI::Widget>();
staged.set_layout<GUI::VerticalBoxLayout>();
auto& staged_header = staged.add<GUI::Widget>();
staged_header.set_layout<GUI::HorizontalBoxLayout>();
auto& commit_button = staged_header.add<GUI::Button>();
commit_button.set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/commit.png"));
commit_button.set_fixed_size(16, 16);
commit_button.set_tooltip("commit");
commit_button.on_click = [this](int) { commit(); };
auto& staged_label = staged_header.add<GUI::Label>();
staged_label.set_text("Staged");
staged_header.set_fixed_height(20);
m_staged_files = staged.add<GitFilesView>(
[this](const auto& file) { unstage_file(file); },
Gfx::Bitmap::load_from_file("/res/icons/16x16/minus.png").release_nonnull());
}
bool GitWidget::initialize()
{
auto result = GitRepo::try_to_create(m_repo_root);
switch (result.type) {
case GitRepo::CreateResult::Type::Success:
m_git_repo = result.repo;
return true;
case GitRepo::CreateResult::Type::GitProgramNotFound:
GUI::MessageBox::show(window(), "Please install the Git port", "Error", GUI::MessageBox::Type::Error);
return false;
case GitRepo::CreateResult::Type::NoGitRepo: {
auto decision = GUI::MessageBox::show(window(), "Create git repository?", "Git", GUI::MessageBox::Type::Question, GUI::MessageBox::InputType::YesNo);
if (decision != GUI::Dialog::ExecResult::ExecYes)
return false;
m_git_repo = GitRepo::initialize_repository(m_repo_root);
return true;
}
default:
ASSERT_NOT_REACHED();
}
}
bool GitWidget::initialize_if_needed()
{
if (initialized())
return true;
return initialize();
}
void GitWidget::refresh()
{
if (!initialize_if_needed()) {
dbgln("GitWidget initialization failed");
return;
}
ASSERT(!m_git_repo.is_null());
m_unstaged_files->set_model(GitFilesModel::create(m_git_repo->unstaged_files()));
m_staged_files->set_model(GitFilesModel::create(m_git_repo->staged_files()));
}
void GitWidget::stage_file(const LexicalPath& file)
{
dbgln("staging: {}", file);
bool rc = m_git_repo->stage(file);
ASSERT(rc);
refresh();
}
void GitWidget::unstage_file(const LexicalPath& file)
{
dbgln("unstaging: {}", file);
bool rc = m_git_repo->unstage(file);
ASSERT(rc);
refresh();
}
void GitWidget::commit()
{
String message;
auto res = GUI::InputBox::show(message, window(), "Commit message:", "Commit");
if (res != GUI::InputBox::ExecOK || message.is_empty())
return;
dbgln("commit message: {}", message);
m_git_repo->commit(message);
refresh();
}
void GitWidget::set_view_diff_callback(ViewDiffCallback callback)
{
m_view_diff_callback = move(callback);
}
void GitWidget::show_diff(const LexicalPath& file_path)
{
if (!m_git_repo->is_tracked(file_path)) {
auto file = Core::File::construct(file_path.string());
if (!file->open(Core::IODevice::ReadOnly)) {
perror("open");
ASSERT_NOT_REACHED();
}
auto content = file->read_all();
String content_string((char*)content.data(), content.size());
m_view_diff_callback("", Diff::generate_only_additions(content_string));
return;
}
const auto& original_content = m_git_repo->original_file_content(file_path);
const auto& diff = m_git_repo->unstaged_diff(file_path);
ASSERT(original_content.has_value() && diff.has_value());
m_view_diff_callback(original_content.value(), diff.value());
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "GitFilesView.h"
#include "GitRepo.h"
#include <AK/Function.h>
#include <LibGUI/Forward.h>
#include <LibGUI/Widget.h>
namespace HackStudio {
typedef Function<void(const String& original_content, const String& diff)> ViewDiffCallback;
class GitWidget final : public GUI::Widget {
C_OBJECT(GitWidget)
public:
virtual ~GitWidget() override { }
void refresh();
void set_view_diff_callback(ViewDiffCallback callback);
bool initialized() const { return !m_git_repo.is_null(); };
private:
explicit GitWidget(const LexicalPath& repo_root);
bool initialize();
bool initialize_if_needed();
void stage_file(const LexicalPath&);
void unstage_file(const LexicalPath&);
void commit();
void show_diff(const LexicalPath&);
LexicalPath m_repo_root;
RefPtr<GitFilesView> m_unstaged_files;
RefPtr<GitFilesView> m_staged_files;
RefPtr<GitRepo> m_git_repo;
ViewDiffCallback m_view_diff_callback;
};
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "EditorWrapper.h"
#include "LanguageClients/ServerConnections.h"
#include "Project.h"
#include <AK/String.h>
#include <LibGUI/TextEditor.h>
namespace HackStudio {
GUI::TextEditor& current_editor();
void open_file(const String&);
RefPtr<EditorWrapper> current_editor_wrapper();
void open_file(const String&);
Project& project();
String currently_open_file();
void set_current_editor_wrapper(RefPtr<EditorWrapper>);
}

View file

@ -0,0 +1,945 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* Copyright (c) 2020, the SerenityOS developers
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "HackStudioWidget.h"
#include "CursorTool.h"
#include "Debugger/DebugInfoWidget.h"
#include "Debugger/Debugger.h"
#include "Debugger/DisassemblyWidget.h"
#include "Editor.h"
#include "EditorWrapper.h"
#include "FindInFilesWidget.h"
#include "FormEditorWidget.h"
#include "FormWidget.h"
#include "Git/DiffViewer.h"
#include "Git/GitWidget.h"
#include "HackStudio.h"
#include "HackStudioWidget.h"
#include "Locator.h"
#include "Project.h"
#include "TerminalWrapper.h"
#include "WidgetTool.h"
#include "WidgetTreeModel.h"
#include <AK/StringBuilder.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/Event.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibDebug/DebugSession.h>
#include <LibGUI/Action.h>
#include <LibGUI/ActionGroup.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/EditingEngine.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/RegularEditingEngine.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/StackWidget.h>
#include <LibGUI/TabWidget.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
#include <LibGUI/TreeView.h>
#include <LibGUI/VimEditingEngine.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibGfx/FontDatabase.h>
#include <LibThread/Lock.h>
#include <LibThread/Thread.h>
#include <LibVT/TerminalWidget.h>
#include <fcntl.h>
#include <spawn.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
namespace HackStudio {
HackStudioWidget::HackStudioWidget(const String& path_to_project)
{
set_fill_with_background_color(true);
set_layout<GUI::VerticalBoxLayout>();
layout()->set_spacing(2);
open_project(path_to_project);
auto& toolbar_container = add<GUI::ToolBarContainer>();
auto& outer_splitter = add<GUI::HorizontalSplitter>();
auto& left_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
left_hand_splitter.set_fixed_width(150);
create_project_tree_view(left_hand_splitter);
m_project_tree_view_context_menu = create_project_tree_view_context_menu();
create_open_files_view(left_hand_splitter);
m_right_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
m_right_hand_stack = m_right_hand_splitter->add<GUI::StackWidget>();
// Put a placeholder widget front & center since we don't have a file open yet.
m_right_hand_stack->add<GUI::Widget>();
create_form_editor(*m_right_hand_stack);
m_diff_viewer = m_right_hand_stack->add<DiffViewer>();
m_editors_splitter = m_right_hand_stack->add<GUI::VerticalSplitter>();
m_editors_splitter->layout()->set_margins({ 0, 3, 0, 0 });
add_new_editor(*m_editors_splitter);
m_switch_to_next_editor = create_switch_to_next_editor_action();
m_switch_to_previous_editor = create_switch_to_previous_editor_action();
m_remove_current_editor_action = create_remove_current_editor_action();
m_open_action = create_open_action();
m_save_action = create_save_action();
create_action_tab(*m_right_hand_splitter);
m_add_editor_action = create_add_editor_action();
m_add_terminal_action = create_add_terminal_action();
m_remove_current_terminal_action = create_remove_current_terminal_action();
m_locator = add<Locator>();
m_terminal_wrapper->on_command_exit = [this] {
m_stop_action->set_enabled(false);
};
m_build_action = create_build_action();
m_run_action = create_run_action();
m_stop_action = create_stop_action();
m_debug_action = create_debug_action();
initialize_debugger();
create_toolbar(toolbar_container);
}
void HackStudioWidget::update_actions()
{
auto is_remove_terminal_enabled = [this]() {
auto widget = m_action_tab_widget->active_widget();
if (!widget)
return false;
if (StringView { "TerminalWrapper" } != widget->class_name())
return false;
if (!reinterpret_cast<TerminalWrapper*>(widget)->user_spawned())
return false;
return true;
};
m_remove_current_editor_action->set_enabled(m_all_editor_wrappers.size() > 1);
m_remove_current_terminal_action->set_enabled(is_remove_terminal_enabled());
}
void HackStudioWidget::on_action_tab_change()
{
update_actions();
auto git_widget = m_action_tab_widget->active_widget();
if (!git_widget)
return;
if (StringView { "GitWidget" } != git_widget->class_name())
return;
reinterpret_cast<GitWidget*>(git_widget)->refresh();
}
void HackStudioWidget::open_project(const String& root_path)
{
if (chdir(root_path.characters()) < 0) {
perror("chdir");
exit(1);
}
m_project = Project::open_with_root_path(root_path);
ASSERT(m_project);
if (m_project_tree_view) {
m_project_tree_view->set_model(m_project->model());
m_project_tree_view->toggle_index(m_project_tree_view->model()->index(0, 0));
m_project_tree_view->update();
}
if (Debugger::is_initialized()) {
Debugger::the().reset_breakpoints();
}
}
Vector<String> HackStudioWidget::selected_file_names() const
{
Vector<String> files;
m_project_tree_view->selection().for_each_index([&](const GUI::ModelIndex& index) {
files.append(index.data().as_string());
});
return files;
}
void HackStudioWidget::open_file(const String& filename)
{
if (!currently_open_file().is_empty()) {
// Since the file is previously open, it should always be in m_open_files.
ASSERT(m_open_files.find(currently_open_file()) != m_open_files.end());
auto previous_open_project_file = m_open_files.get(currently_open_file()).value();
// Update the scrollbar values of the previous_open_project_file and save them to m_open_files.
previous_open_project_file->vertical_scroll_value(current_editor().vertical_scrollbar().value());
previous_open_project_file->horizontal_scroll_value(current_editor().horizontal_scrollbar().value());
m_open_files.set(currently_open_file(), previous_open_project_file);
}
RefPtr<ProjectFile> new_project_file = nullptr;
if (auto it = m_open_files.find(filename); it != m_open_files.end()) {
new_project_file = it->value;
} else {
new_project_file = m_project->get_file(filename);
if (!new_project_file) {
new_project_file = ProjectFile::construct_with_name(filename);
}
m_open_files.set(filename, *new_project_file);
m_open_files_vector.append(filename);
m_open_files_view->model()->update();
}
current_editor().set_document(const_cast<GUI::TextDocument&>(new_project_file->document()));
current_editor().set_mode(GUI::TextEditor::Editable);
current_editor().horizontal_scrollbar().set_value(new_project_file->horizontal_scroll_value());
current_editor().vertical_scrollbar().set_value(new_project_file->vertical_scroll_value());
current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
if (filename.ends_with(".frm")) {
set_edit_mode(EditMode::Form);
} else {
set_edit_mode(EditMode::Text);
}
m_currently_open_file = filename;
String relative_file_path = m_currently_open_file;
if (m_currently_open_file.starts_with(m_project->root_path()))
relative_file_path = m_currently_open_file.substring(m_project->root_path().length() + 1);
window()->set_title(String::formatted("{} - {} - Hack Studio", relative_file_path, m_project->name()));
m_project_tree_view->update();
current_editor_wrapper().filename_label().set_text(filename);
current_editor().set_focus(true);
}
EditorWrapper& HackStudioWidget::current_editor_wrapper()
{
ASSERT(m_current_editor_wrapper);
return *m_current_editor_wrapper;
}
GUI::TextEditor& HackStudioWidget::current_editor()
{
return current_editor_wrapper().editor();
}
void HackStudioWidget::set_edit_mode(EditMode mode)
{
if (mode == EditMode::Text) {
m_right_hand_stack->set_active_widget(m_editors_splitter);
} else if (mode == EditMode::Form) {
m_right_hand_stack->set_active_widget(m_form_inner_container);
} else if (mode == EditMode::Diff) {
m_right_hand_stack->set_active_widget(m_diff_viewer);
} else {
ASSERT_NOT_REACHED();
}
m_right_hand_stack->active_widget()->update();
}
NonnullRefPtr<GUI::Menu> HackStudioWidget::create_project_tree_view_context_menu()
{
m_open_selected_action = create_open_selected_action();
m_new_action = create_new_action();
m_delete_action = create_delete_action();
auto project_tree_view_context_menu = GUI::Menu::construct("Project Files");
project_tree_view_context_menu->add_action(*m_open_selected_action);
// TODO: Rename, cut, copy, duplicate with new name, show containing folder ...
project_tree_view_context_menu->add_separator();
project_tree_view_context_menu->add_action(*m_new_action);
project_tree_view_context_menu->add_action(*m_delete_action);
return project_tree_view_context_menu;
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_new_action()
{
return GUI::Action::create("Add new file to project...", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) {
String filename;
if (GUI::InputBox::show(filename, window(), "Enter name of new file:", "Add new file to project") != GUI::InputBox::ExecOK)
return;
auto file = Core::File::construct(filename);
if (!file->open((Core::IODevice::OpenMode)(Core::IODevice::WriteOnly | Core::IODevice::MustBeNew))) {
GUI::MessageBox::show(window(), String::formatted("Failed to create '{}'", filename), "Error", GUI::MessageBox::Type::Error);
return;
}
m_project_tree_view->toggle_index(m_project_tree_view->model()->index(0, 0));
open_file(filename);
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_selected_action()
{
auto open_selected_action = GUI::Action::create("Open", [this](const GUI::Action&) {
auto files = selected_file_names();
for (auto& file : files)
open_file(file);
});
open_selected_action->set_enabled(true);
return open_selected_action;
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action()
{
auto delete_action = GUI::CommonActions::make_delete_action([this](const GUI::Action&) {
auto files = selected_file_names();
if (files.is_empty())
return;
String message;
if (files.size() == 1) {
message = String::formatted("Really remove {} from disk?", LexicalPath(files[0]).basename());
} else {
message = String::formatted("Really remove {} files from disk?", files.size());
}
auto result = GUI::MessageBox::show(window(),
message,
"Confirm deletion",
GUI::MessageBox::Type::Warning,
GUI::MessageBox::InputType::OKCancel);
if (result == GUI::MessageBox::ExecCancel)
return;
for (auto& file : files) {
if (1) {
// FIXME: Remove `file` from disk
} else {
GUI::MessageBox::show(window(),
String::formatted("Removing file {} from the project failed.", file),
"Removal failed",
GUI::MessageBox::Type::Error);
break;
}
}
});
delete_action->set_enabled(false);
return delete_action;
}
void HackStudioWidget::add_new_editor(GUI::Widget& parent)
{
auto wrapper = EditorWrapper::construct();
if (m_action_tab_widget) {
parent.insert_child_before(wrapper, *m_action_tab_widget);
} else {
parent.add_child(wrapper);
}
m_current_editor_wrapper = wrapper;
m_all_editor_wrappers.append(wrapper);
wrapper->editor().set_focus(true);
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_next_editor_action()
{
return GUI::Action::create("Switch to next editor", { Mod_Ctrl, Key_E }, [this](auto&) {
if (m_all_editor_wrappers.size() <= 1)
return;
Vector<EditorWrapper*> wrappers;
m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) {
wrappers.append(&child);
return IterationDecision::Continue;
});
for (size_t i = 0; i < wrappers.size(); ++i) {
if (m_current_editor_wrapper.ptr() == wrappers[i]) {
if (i == wrappers.size() - 1)
wrappers[0]->editor().set_focus(true);
else
wrappers[i + 1]->editor().set_focus(true);
}
}
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_previous_editor_action()
{
return GUI::Action::create("Switch to previous editor", { Mod_Ctrl | Mod_Shift, Key_E }, [this](auto&) {
if (m_all_editor_wrappers.size() <= 1)
return;
Vector<EditorWrapper*> wrappers;
m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) {
wrappers.append(&child);
return IterationDecision::Continue;
});
for (int i = wrappers.size() - 1; i >= 0; --i) {
if (m_current_editor_wrapper.ptr() == wrappers[i]) {
if (i == 0)
wrappers.last()->editor().set_focus(true);
else
wrappers[i - 1]->editor().set_focus(true);
}
}
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_editor_action()
{
return GUI::Action::create("Remove current editor", { Mod_Alt | Mod_Shift, Key_E }, [this](auto&) {
if (m_all_editor_wrappers.size() <= 1)
return;
auto wrapper = m_current_editor_wrapper;
m_switch_to_next_editor->activate();
m_editors_splitter->remove_child(*wrapper);
m_all_editor_wrappers.remove_first_matching([&wrapper](auto& entry) { return entry == wrapper.ptr(); });
update_actions();
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_action()
{
return GUI::Action::create("Open project...", { Mod_Ctrl | Mod_Shift, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [this](auto&) {
auto open_path = GUI::FilePicker::get_open_filepath(window(), "Open project");
if (!open_path.has_value())
return;
open_project(open_path.value());
update_actions();
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_save_action()
{
return GUI::Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [this](auto&) {
if (m_currently_open_file.is_empty())
return;
current_editor().write_to_file(m_currently_open_file);
if (m_git_widget->initialized())
m_git_widget->refresh();
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_terminal_action()
{
return GUI::Action::create("Remove current Terminal", { Mod_Alt | Mod_Shift, Key_T }, [this](auto&) {
auto widget = m_action_tab_widget->active_widget();
if (!widget)
return;
if (!is<TerminalWrapper>(widget))
return;
auto& terminal = *static_cast<TerminalWrapper*>(widget);
if (!terminal.user_spawned())
return;
m_action_tab_widget->remove_tab(terminal);
update_actions();
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_editor_action()
{
return GUI::Action::create("Add new editor", { Mod_Ctrl | Mod_Alt, Key_E },
Gfx::Bitmap::load_from_file("/res/icons/16x16/app-text-editor.png"),
[this](auto&) {
add_new_editor(*m_editors_splitter);
update_actions();
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_terminal_action()
{
return GUI::Action::create("Add new Terminal", { Mod_Ctrl | Mod_Alt, Key_T },
Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"),
[this](auto&) {
auto& terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Terminal");
reveal_action_tab(terminal_wrapper);
update_actions();
terminal_wrapper.terminal().set_focus(true);
});
}
void HackStudioWidget::reveal_action_tab(GUI::Widget& widget)
{
if (m_action_tab_widget->min_height() < 200)
m_action_tab_widget->set_fixed_height(200);
m_action_tab_widget->set_active_widget(&widget);
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_debug_action()
{
return GUI::Action::create("Debug", Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-run.png"), [this](auto&) {
if (!GUI::FilePicker::file_exists(get_project_executable_path())) {
GUI::MessageBox::show(window(), String::formatted("Could not find file: {}. (did you build the project?)", get_project_executable_path()), "Error", GUI::MessageBox::Type::Error);
return;
}
if (Debugger::the().session()) {
GUI::MessageBox::show(window(), "Debugger is already running", "Error", GUI::MessageBox::Type::Error);
return;
}
Debugger::the().set_executable_path(get_project_executable_path());
m_debugger_thread = LibThread::Thread::construct(Debugger::start_static);
m_debugger_thread->start();
});
}
void HackStudioWidget::initialize_debugger()
{
Debugger::initialize(
m_project->root_path(),
[this](const PtraceRegisters& regs) {
ASSERT(Debugger::the().session());
const auto& debug_session = *Debugger::the().session();
auto source_position = debug_session.get_source_position(regs.eip);
if (!source_position.has_value()) {
dbgln("Could not find source position for address: {:p}", regs.eip);
return Debugger::HasControlPassedToUser::No;
}
dbgln("Debugger stopped at source position: {}:{}", source_position.value().file_path, source_position.value().line_number);
Core::EventLoop::main().post_event(
*window(),
make<Core::DeferredInvocationEvent>(
[this, source_position, &regs](auto&) {
m_current_editor_in_execution = get_editor_of_file(source_position.value().file_path);
m_current_editor_in_execution->editor().set_execution_position(source_position.value().line_number - 1);
m_debug_info_widget->update_state(*Debugger::the().session(), regs);
m_debug_info_widget->set_debug_actions_enabled(true);
m_disassembly_widget->update_state(*Debugger::the().session(), regs);
HackStudioWidget::reveal_action_tab(*m_debug_info_widget);
}));
Core::EventLoop::wake();
return Debugger::HasControlPassedToUser::Yes;
},
[this]() {
Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) {
m_debug_info_widget->set_debug_actions_enabled(false);
if (m_current_editor_in_execution) {
m_current_editor_in_execution->editor().clear_execution_position();
}
}));
Core::EventLoop::wake();
},
[this]() {
Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) {
m_debug_info_widget->program_stopped();
m_disassembly_widget->program_stopped();
HackStudioWidget::hide_action_tabs();
GUI::MessageBox::show(window(), "Program Exited", "Debugger", GUI::MessageBox::Type::Information);
}));
Core::EventLoop::wake();
});
}
String HackStudioWidget::get_full_path_of_serenity_source(const String& file)
{
auto path_parts = LexicalPath(file).parts();
ASSERT(path_parts[0] == "..");
path_parts.remove(0);
StringBuilder relative_path_builder;
relative_path_builder.join("/", path_parts);
constexpr char SERENITY_LIBS_PREFIX[] = "/usr/src/serenity";
LexicalPath serenity_sources_base(SERENITY_LIBS_PREFIX);
return String::formatted("{}/{}", serenity_sources_base, relative_path_builder.to_string());
}
NonnullRefPtr<EditorWrapper> HackStudioWidget::get_editor_of_file(const String& file_name)
{
String file_path = file_name;
// TODO: We can probably do a more specific condition here, something like
// "if (file.starts_with("../Libraries/") || file.starts_with("../AK/"))"
if (file_name.starts_with("../")) {
file_path = get_full_path_of_serenity_source(file_name);
}
open_file(file_path);
return current_editor_wrapper();
}
String HackStudioWidget::get_project_executable_path() const
{
// FIXME: Dumb heuristic ahead!
// e.g /my/project => /my/project/project
// TODO: Perhaps a Makefile rule for getting the value of $(PROGRAM) would be better?
return String::formatted("{}/{}", m_project->root_path(), LexicalPath(m_project->root_path()).basename());
}
void HackStudioWidget::build(TerminalWrapper& wrapper)
{
if (m_currently_open_file.ends_with(".js"))
wrapper.run_command(String::formatted("js -A {}", m_currently_open_file));
else
wrapper.run_command("make");
}
void HackStudioWidget::run(TerminalWrapper& wrapper)
{
if (m_currently_open_file.ends_with(".js"))
wrapper.run_command(String::formatted("js {}", m_currently_open_file));
else
wrapper.run_command("make run");
}
void HackStudioWidget::hide_action_tabs()
{
m_action_tab_widget->set_fixed_height(24);
};
Project& HackStudioWidget::project()
{
return *m_project;
}
void HackStudioWidget::set_current_editor_wrapper(RefPtr<EditorWrapper> editor_wrapper)
{
m_current_editor_wrapper = editor_wrapper;
}
void HackStudioWidget::create_project_tree_view(GUI::Widget& parent)
{
m_project_tree_view = parent.add<GUI::TreeView>();
m_project_tree_view->set_model(m_project->model());
for (int column_index = 0; column_index < m_project->model().column_count(); ++column_index)
m_project_tree_view->set_column_hidden(column_index, true);
m_project_tree_view->set_column_hidden(GUI::FileSystemModel::Column::Name, false);
m_project_tree_view->on_context_menu_request = [this](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (index.is_valid()) {
m_project_tree_view_context_menu->popup(event.screen_position(), m_open_selected_action);
}
};
m_project_tree_view->on_selection_change = [this] {
m_open_selected_action->set_enabled(!m_project_tree_view->selection().is_empty());
m_delete_action->set_enabled(!m_project_tree_view->selection().is_empty());
};
m_project_tree_view->on_activation = [this](auto& index) {
auto filename = index.data(GUI::ModelRole::Custom).to_string();
open_file(filename);
};
}
void HackStudioWidget::create_open_files_view(GUI::Widget& parent)
{
m_open_files_view = parent.add<GUI::ListView>();
auto open_files_model = GUI::ItemListModel<String>::create(m_open_files_vector);
m_open_files_view->set_model(open_files_model);
m_open_files_view->on_activation = [this](auto& index) {
open_file(index.data().to_string());
};
}
void HackStudioWidget::create_form_editor(GUI::Widget& parent)
{
m_form_inner_container = parent.add<GUI::Widget>();
m_form_inner_container->set_layout<GUI::HorizontalBoxLayout>();
auto& form_widgets_toolbar = m_form_inner_container->add<GUI::ToolBar>(Orientation::Vertical, 26);
form_widgets_toolbar.set_fixed_width(38);
GUI::ActionGroup tool_actions;
tool_actions.set_exclusive(true);
auto cursor_tool_action = GUI::Action::create_checkable("Cursor", Gfx::Bitmap::load_from_file("/res/icons/hackstudio/Cursor.png"), [this](auto&) {
m_form_editor_widget->set_tool(make<CursorTool>(*m_form_editor_widget));
});
cursor_tool_action->set_checked(true);
tool_actions.add_action(cursor_tool_action);
form_widgets_toolbar.add_action(cursor_tool_action);
GUI::WidgetClassRegistration::for_each([&, this](const GUI::WidgetClassRegistration& reg) {
constexpr size_t gui_namespace_prefix_length = sizeof("GUI::") - 1;
auto icon_path = String::formatted("/res/icons/hackstudio/G{}.png",
reg.class_name().substring(gui_namespace_prefix_length, reg.class_name().length() - gui_namespace_prefix_length));
if (!Core::File::exists(icon_path))
return;
auto action = GUI::Action::create_checkable(reg.class_name(), Gfx::Bitmap::load_from_file(icon_path), [&reg, this](auto&) {
m_form_editor_widget->set_tool(make<WidgetTool>(*m_form_editor_widget, reg));
auto widget = reg.construct();
m_form_editor_widget->form_widget().add_child(widget);
widget->set_relative_rect(30, 30, 30, 30);
m_form_editor_widget->model().update();
});
action->set_checked(false);
tool_actions.add_action(action);
form_widgets_toolbar.add_action(move(action));
});
auto& form_editor_inner_splitter = m_form_inner_container->add<GUI::HorizontalSplitter>();
m_form_editor_widget = form_editor_inner_splitter.add<FormEditorWidget>();
auto& form_editing_pane_container = form_editor_inner_splitter.add<GUI::VerticalSplitter>();
form_editing_pane_container.set_fixed_width(190);
form_editing_pane_container.set_layout<GUI::VerticalBoxLayout>();
auto add_properties_pane = [&](auto& text, auto& pane_widget) {
auto& wrapper = form_editing_pane_container.add<GUI::Widget>();
wrapper.set_layout<GUI::VerticalBoxLayout>();
auto& label = wrapper.add<GUI::Label>(text);
label.set_fill_with_background_color(true);
label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
label.set_font(Gfx::FontDatabase::default_bold_font());
label.set_fixed_height(16);
wrapper.add_child(pane_widget);
};
m_form_widget_tree_view = GUI::TreeView::construct();
m_form_widget_tree_view->set_model(m_form_editor_widget->model());
m_form_widget_tree_view->on_selection_change = [this] {
m_form_editor_widget->selection().disable_hooks();
m_form_editor_widget->selection().clear();
m_form_widget_tree_view->selection().for_each_index([this](auto& index) {
// NOTE: Make sure we don't add the FormWidget itself to the selection,
// since that would allow you to drag-move the FormWidget.
if (index.internal_data() != &m_form_editor_widget->form_widget())
m_form_editor_widget->selection().add(*(GUI::Widget*)index.internal_data());
});
m_form_editor_widget->update();
m_form_editor_widget->selection().enable_hooks();
};
m_form_editor_widget->selection().on_add = [this](auto& widget) {
m_form_widget_tree_view->selection().add(m_form_editor_widget->model().index_for_widget(widget));
};
m_form_editor_widget->selection().on_remove = [this](auto& widget) {
m_form_widget_tree_view->selection().remove(m_form_editor_widget->model().index_for_widget(widget));
};
m_form_editor_widget->selection().on_clear = [this] {
m_form_widget_tree_view->selection().clear();
};
add_properties_pane("Form widget tree:", *m_form_widget_tree_view);
add_properties_pane("Widget properties:", *GUI::TableView::construct());
}
void HackStudioWidget::create_toolbar(GUI::Widget& parent)
{
auto& toolbar = parent.add<GUI::ToolBar>();
toolbar.add_action(*m_new_action);
toolbar.add_action(*m_save_action);
toolbar.add_action(*m_delete_action);
toolbar.add_separator();
toolbar.add_action(GUI::CommonActions::make_cut_action([this](auto&) { current_editor().cut_action().activate(); }));
toolbar.add_action(GUI::CommonActions::make_copy_action([this](auto&) { current_editor().copy_action().activate(); }));
toolbar.add_action(GUI::CommonActions::make_paste_action([this](auto&) { current_editor().paste_action().activate(); }));
toolbar.add_separator();
toolbar.add_action(GUI::CommonActions::make_undo_action([this](auto&) { current_editor().undo_action().activate(); }));
toolbar.add_action(GUI::CommonActions::make_redo_action([this](auto&) { current_editor().redo_action().activate(); }));
toolbar.add_separator();
toolbar.add_action(*m_build_action);
toolbar.add_separator();
toolbar.add_action(*m_run_action);
toolbar.add_action(*m_stop_action);
toolbar.add_separator();
toolbar.add_action(*m_debug_action);
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_build_action()
{
return GUI::Action::create("Build", { Mod_Ctrl, Key_B }, Gfx::Bitmap::load_from_file("/res/icons/16x16/build.png"), [this](auto&) {
reveal_action_tab(*m_terminal_wrapper);
build(*m_terminal_wrapper);
m_stop_action->set_enabled(true);
});
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_run_action()
{
return GUI::Action::create("Run", { Mod_Ctrl, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/program-run.png"), [this](auto&) {
reveal_action_tab(*m_terminal_wrapper);
run(*m_terminal_wrapper);
m_stop_action->set_enabled(true);
});
}
void HackStudioWidget::create_action_tab(GUI::Widget& parent)
{
m_action_tab_widget = parent.add<GUI::TabWidget>();
m_action_tab_widget->set_fixed_height(24);
m_action_tab_widget->on_change = [this](auto&) {
on_action_tab_change();
static bool first_time = true;
if (!first_time)
m_action_tab_widget->set_fixed_height(200);
first_time = false;
};
m_find_in_files_widget = m_action_tab_widget->add_tab<FindInFilesWidget>("Find in files");
m_terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Build", false);
m_debug_info_widget = m_action_tab_widget->add_tab<DebugInfoWidget>("Debug");
m_disassembly_widget = m_action_tab_widget->add_tab<DisassemblyWidget>("Disassembly");
m_git_widget = m_action_tab_widget->add_tab<GitWidget>("Git", LexicalPath(m_project->root_path()));
m_git_widget->set_view_diff_callback([this](const auto& original_content, const auto& diff) {
m_diff_viewer->set_content(original_content, diff);
set_edit_mode(EditMode::Diff);
});
}
void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar)
{
auto& app_menu = menubar.add_menu("Hack Studio");
app_menu.add_action(*m_open_action);
app_menu.add_action(*m_save_action);
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
}));
}
void HackStudioWidget::create_project_menubar(GUI::MenuBar& menubar)
{
auto& project_menu = menubar.add_menu("Project");
project_menu.add_action(*m_new_action);
}
void HackStudioWidget::create_edit_menubar(GUI::MenuBar& menubar)
{
auto& edit_menu = menubar.add_menu("Edit");
edit_menu.add_action(GUI::Action::create("Find in files...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [this](auto&) {
reveal_action_tab(*m_find_in_files_widget);
m_find_in_files_widget->focus_textbox_and_select_all();
}));
edit_menu.add_separator();
auto line_wrapping_action = GUI::Action::create_checkable("Line wrapping", [this](auto& action) {
for (auto& wrapper : m_all_editor_wrappers) {
wrapper.editor().set_line_wrapping_enabled(action.is_checked());
}
});
line_wrapping_action->set_checked(current_editor().is_line_wrapping_enabled());
edit_menu.add_action(line_wrapping_action);
edit_menu.add_separator();
auto vim_emulation_setting_action = GUI::Action::create_checkable("Vim emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [this](auto& action) {
if (action.is_checked())
current_editor().set_editing_engine(make<GUI::VimEditingEngine>());
else
current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
});
vim_emulation_setting_action->set_checked(false);
edit_menu.add_action(vim_emulation_setting_action);
}
void HackStudioWidget::create_build_menubar(GUI::MenuBar& menubar)
{
auto& build_menu = menubar.add_menu("Build");
build_menu.add_action(*m_build_action);
build_menu.add_separator();
build_menu.add_action(*m_run_action);
build_menu.add_action(*m_stop_action);
build_menu.add_separator();
build_menu.add_action(*m_debug_action);
}
void HackStudioWidget::create_view_menubar(GUI::MenuBar& menubar)
{
auto hide_action_tabs_action = GUI::Action::create("Hide action tabs", { Mod_Ctrl | Mod_Shift, Key_X }, [this](auto&) {
hide_action_tabs();
});
auto open_locator_action = GUI::Action::create("Open locator", { Mod_Ctrl, Key_K }, [this](auto&) {
m_locator->open();
});
auto& view_menu = menubar.add_menu("View");
view_menu.add_action(hide_action_tabs_action);
view_menu.add_action(open_locator_action);
view_menu.add_separator();
view_menu.add_action(*m_add_editor_action);
view_menu.add_action(*m_remove_current_editor_action);
view_menu.add_action(*m_add_terminal_action);
view_menu.add_action(*m_remove_current_terminal_action);
}
void HackStudioWidget::create_help_menubar(GUI::MenuBar& menubar)
{
auto& help_menu = menubar.add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("Hack Studio", GUI::Icon::default_icon("app-hack-studio"), window()));
}
NonnullRefPtr<GUI::Action> HackStudioWidget::create_stop_action()
{
auto action = GUI::Action::create("Stop", Gfx::Bitmap::load_from_file("/res/icons/16x16/program-stop.png"), [this](auto&) {
m_terminal_wrapper->kill_running_command();
});
action->set_enabled(false);
return action;
}
void HackStudioWidget::initialize_menubar(GUI::MenuBar& menubar)
{
create_app_menubar(menubar);
create_project_menubar(menubar);
create_edit_menubar(menubar);
create_build_menubar(menubar);
create_view_menubar(menubar);
create_help_menubar(menubar);
}
HackStudioWidget::~HackStudioWidget()
{
if (!m_debugger_thread.is_null()) {
Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::Exit);
dbgln("Waiting for debugger thread to terminate");
auto rc = m_debugger_thread->join();
if (rc.is_error()) {
warnln("pthread_join: {}", strerror(rc.error().value()));
dbgln("error joining debugger thread");
}
}
}
}

View file

@ -0,0 +1,170 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* Copyright (c) 2020, the SerenityOS developers
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Debugger/DebugInfoWidget.h"
#include "Debugger/DisassemblyWidget.h"
#include "EditorWrapper.h"
#include "FindInFilesWidget.h"
#include "FormEditorWidget.h"
#include "Git/DiffViewer.h"
#include "Git/GitWidget.h"
#include "Locator.h"
#include "Project.h"
#include "ProjectFile.h"
#include "TerminalWrapper.h"
#include <LibGUI/ScrollBar.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/Widget.h>
#include <LibThread/Thread.h>
namespace HackStudio {
class HackStudioWidget : public GUI::Widget {
C_OBJECT(HackStudioWidget)
public:
virtual ~HackStudioWidget() override;
void open_file(const String& filename);
Vector<String> selected_file_names() const;
void update_actions();
Project& project();
GUI::TextEditor& current_editor();
EditorWrapper& current_editor_wrapper();
void set_current_editor_wrapper(RefPtr<EditorWrapper>);
String currently_open_file() const { return m_currently_open_file; }
void initialize_menubar(GUI::MenuBar&);
private:
static String get_full_path_of_serenity_source(const String& file);
HackStudioWidget(const String& path_to_project);
void open_project(const String& root_path);
enum class EditMode {
Text,
Form,
Diff,
};
void set_edit_mode(EditMode);
NonnullRefPtr<GUI::Menu> create_project_tree_view_context_menu();
NonnullRefPtr<GUI::Action> create_new_action();
NonnullRefPtr<GUI::Action> create_open_selected_action();
NonnullRefPtr<GUI::Action> create_delete_action();
NonnullRefPtr<GUI::Action> create_switch_to_next_editor_action();
NonnullRefPtr<GUI::Action> create_switch_to_previous_editor_action();
NonnullRefPtr<GUI::Action> create_remove_current_editor_action();
NonnullRefPtr<GUI::Action> create_open_action();
NonnullRefPtr<GUI::Action> create_save_action();
NonnullRefPtr<GUI::Action> create_add_editor_action();
NonnullRefPtr<GUI::Action> create_add_terminal_action();
NonnullRefPtr<GUI::Action> create_remove_current_terminal_action();
NonnullRefPtr<GUI::Action> create_debug_action();
NonnullRefPtr<GUI::Action> create_build_action();
NonnullRefPtr<GUI::Action> create_run_action();
NonnullRefPtr<GUI::Action> create_stop_action();
void add_new_editor(GUI::Widget& parent);
NonnullRefPtr<EditorWrapper> get_editor_of_file(const String& file_name);
String get_project_executable_path() const;
void on_action_tab_change();
void reveal_action_tab(GUI::Widget&);
void initialize_debugger();
void create_project_tree_view(GUI::Widget& parent);
void create_open_files_view(GUI::Widget& parent);
void create_form_editor(GUI::Widget& parent);
void create_toolbar(GUI::Widget& parent);
void create_action_tab(GUI::Widget& parent);
void create_app_menubar(GUI::MenuBar&);
void create_project_menubar(GUI::MenuBar&);
void create_edit_menubar(GUI::MenuBar&);
void create_build_menubar(GUI::MenuBar&);
void create_view_menubar(GUI::MenuBar&);
void create_help_menubar(GUI::MenuBar&);
void run(TerminalWrapper& wrapper);
void build(TerminalWrapper& wrapper);
void hide_action_tabs();
NonnullRefPtrVector<EditorWrapper> m_all_editor_wrappers;
RefPtr<EditorWrapper> m_current_editor_wrapper;
// FIXME: This doesn't seem compatible with multiple split editors
String m_currently_open_file;
HashMap<String, NonnullRefPtr<ProjectFile>> m_open_files;
Vector<String> m_open_files_vector; // NOTE: This contains the keys from m_open_files
OwnPtr<Project> m_project;
RefPtr<GUI::TreeView> m_project_tree_view;
RefPtr<GUI::ListView> m_open_files_view;
RefPtr<GUI::VerticalSplitter> m_right_hand_splitter;
RefPtr<GUI::StackWidget> m_right_hand_stack;
RefPtr<GUI::Splitter> m_editors_splitter;
RefPtr<GUI::Widget> m_form_inner_container;
RefPtr<FormEditorWidget> m_form_editor_widget;
RefPtr<GUI::TreeView> m_form_widget_tree_view;
RefPtr<DiffViewer> m_diff_viewer;
RefPtr<GitWidget> m_git_widget;
RefPtr<GUI::Menu> m_project_tree_view_context_menu;
RefPtr<GUI::TabWidget> m_action_tab_widget;
RefPtr<TerminalWrapper> m_terminal_wrapper;
RefPtr<Locator> m_locator;
RefPtr<FindInFilesWidget> m_find_in_files_widget;
RefPtr<DebugInfoWidget> m_debug_info_widget;
RefPtr<DisassemblyWidget> m_disassembly_widget;
RefPtr<LibThread::Thread> m_debugger_thread;
RefPtr<EditorWrapper> m_current_editor_in_execution;
RefPtr<GUI::Action> m_new_action;
RefPtr<GUI::Action> m_open_selected_action;
RefPtr<GUI::Action> m_delete_action;
RefPtr<GUI::Action> m_switch_to_next_editor;
RefPtr<GUI::Action> m_switch_to_previous_editor;
RefPtr<GUI::Action> m_remove_current_editor_action;
RefPtr<GUI::Action> m_open_action;
RefPtr<GUI::Action> m_save_action;
RefPtr<GUI::Action> m_add_editor_action;
RefPtr<GUI::Action> m_add_terminal_action;
RefPtr<GUI::Action> m_remove_current_terminal_action;
RefPtr<GUI::Action> m_stop_action;
RefPtr<GUI::Action> m_debug_action;
RefPtr<GUI::Action> m_build_action;
RefPtr<GUI::Action> m_run_action;
};
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
namespace HackStudio {
enum class Language {
Unknown,
Cpp,
JavaScript,
GML,
Ini,
Shell,
};
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "LanguageClient.h"
#include <AK/String.h>
#include <AK/Vector.h>
namespace HackStudio {
void ServerConnection::handle(const Messages::LanguageClient::AutoCompleteSuggestions& message)
{
if (m_language_client)
m_language_client->provide_autocomplete_suggestions(message.suggestions());
}
void LanguageClient::open_file(const String& path, int fd)
{
m_connection.post_message(Messages::LanguageServer::FileOpened(path, fd));
}
void LanguageClient::set_file_content(const String& path, const String& content)
{
m_connection.post_message(Messages::LanguageServer::SetFileContent(path, content));
}
void LanguageClient::insert_text(const String& path, const String& text, size_t line, size_t column)
{
m_connection.post_message(Messages::LanguageServer::FileEditInsertText(path, text, line, column));
}
void LanguageClient::remove_text(const String& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column)
{
m_connection.post_message(Messages::LanguageServer::FileEditRemoveText(path, from_line, from_column, to_line, to_column));
}
void LanguageClient::request_autocomplete(const String& path, size_t cursor_line, size_t cursor_column)
{
m_connection.post_message(Messages::LanguageServer::AutoCompleteSuggestions(path, cursor_line, cursor_column));
}
void LanguageClient::provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>& suggestions)
{
if (on_autocomplete_suggestions)
on_autocomplete_suggestions(suggestions);
// Otherwise, drop it on the floor :shrug:
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "AutoCompleteResponse.h"
#include <AK/Forward.h>
#include <AK/LexicalPath.h>
#include <AK/Types.h>
#include <LibIPC/ServerConnection.h>
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
namespace HackStudio {
class LanguageClient;
class ServerConnection
: public IPC::ServerConnection<LanguageClientEndpoint, LanguageServerEndpoint>
, public LanguageClientEndpoint {
public:
ServerConnection(const StringView& socket)
: IPC::ServerConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, socket)
{
}
void attach(LanguageClient& client)
{
m_language_client = &client;
}
void detach()
{
m_language_client = nullptr;
}
virtual void handshake() override
{
auto response = send_sync<Messages::LanguageServer::Greet>();
set_my_client_id(response->client_id());
}
template<typename ConcreteType>
static NonnullRefPtr<ServerConnection> get_or_create(const String& project_path)
{
static HashMap<String, NonnullRefPtr<ConcreteType>> s_instances_for_projects;
auto key = LexicalPath { project_path }.string();
if (auto instance = s_instances_for_projects.get(key); instance.has_value())
return *instance.value();
auto connection = ConcreteType::construct();
connection->handshake();
s_instances_for_projects.set(key, *connection);
return *connection;
}
protected:
virtual void handle(const Messages::LanguageClient::AutoCompleteSuggestions&) override;
LanguageClient* m_language_client { nullptr };
};
class LanguageClient {
public:
explicit LanguageClient(NonnullRefPtr<ServerConnection>&& connection)
: m_connection(*connection)
, m_server_connection(move(connection))
{
m_connection.attach(*this);
}
virtual ~LanguageClient()
{
m_connection.detach();
}
virtual void open_file(const String& path, int fd);
virtual void set_file_content(const String& path, const String& content);
virtual void insert_text(const String& path, const String& text, size_t line, size_t column);
virtual void remove_text(const String& path, size_t from_line, size_t from_column, size_t to_line, size_t to_column);
virtual void request_autocomplete(const String& path, size_t cursor_line, size_t cursor_column);
void provide_autocomplete_suggestions(const Vector<GUI::AutocompleteProvider::Entry>&);
Function<void(Vector<GUI::AutocompleteProvider::Entry>)> on_autocomplete_suggestions;
private:
ServerConnection& m_connection;
NonnullRefPtr<ServerConnection> m_server_connection;
};
template<typename ServerConnectionT>
static inline NonnullOwnPtr<LanguageClient> get_language_client(const String& project_path)
{
return make<LanguageClient>(ServerConnection::get_or_create<ServerConnectionT>(project_path));
}
}

View file

@ -0,0 +1,4 @@
set(GENERATED_SOURCES
../../LanguageServers/LanguageServerEndpoint.h
../../LanguageServers/LanguageClientEndpoint.h
)

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "../LanguageClient.h"
#include <AK/LexicalPath.h>
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
#include <LibIPC/ServerConnection.h>
#define LANGUAGE_CLIENT(namespace_, socket_name) \
namespace namespace_ { \
class ServerConnection : public HackStudio::ServerConnection { \
C_OBJECT(ServerConnection) \
private: \
ServerConnection() \
: HackStudio::ServerConnection("/tmp/portal/language/" #socket_name) \
{ \
} \
}; \
}
namespace LanguageClients {
LANGUAGE_CLIENT(Cpp, cpp)
LANGUAGE_CLIENT(Shell, shell)
}
#undef LANGUAGE_CLIENT

View file

@ -0,0 +1,5 @@
compile_ipc(LanguageServer.ipc LanguageServerEndpoint.h)
compile_ipc(LanguageClient.ipc LanguageClientEndpoint.h)
add_subdirectory(Cpp)
add_subdirectory(Shell)

View file

@ -0,0 +1,97 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "AutoComplete.h"
#include <AK/HashTable.h>
#include <LibCpp/Lexer.h>
// #define DEBUG_AUTOCOMPLETE
namespace LanguageServers::Cpp {
Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position)
{
auto lines = code.split('\n', true);
Cpp::Lexer lexer(code);
auto tokens = lexer.lex();
auto index_of_target_token = token_in_position(tokens, autocomplete_position);
if (!index_of_target_token.has_value())
return {};
auto suggestions = identifier_prefixes(lines, tokens, index_of_target_token.value());
#ifdef DEBUG_AUTOCOMPLETE
for (auto& suggestion : suggestions) {
dbgln("suggestion: {}", suggestion.completion);
}
#endif
return suggestions;
}
StringView AutoComplete::text_of_token(const Vector<String>& lines, const Cpp::Token& token)
{
ASSERT(token.m_start.line == token.m_end.line);
ASSERT(token.m_start.column <= token.m_end.column);
return lines[token.m_start.line].substring_view(token.m_start.column, token.m_end.column - token.m_start.column + 1);
}
Optional<size_t> AutoComplete::token_in_position(const Vector<Cpp::Token>& tokens, const GUI::TextPosition& position)
{
for (size_t token_index = 0; token_index < tokens.size(); ++token_index) {
auto& token = tokens[token_index];
if (token.m_start.line != token.m_end.line)
continue;
if (token.m_start.line != position.line())
continue;
if (token.m_start.column + 1 > position.column() || token.m_end.column + 1 < position.column())
continue;
return token_index;
}
return {};
}
Vector<GUI::AutocompleteProvider::Entry> AutoComplete::identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>& tokens, size_t target_token_index)
{
auto partial_input = text_of_token(lines, tokens[target_token_index]);
Vector<GUI::AutocompleteProvider::Entry> suggestions;
HashTable<String> suggestions_lookup; // To avoid duplicate results
for (size_t i = 0; i < target_token_index; ++i) {
auto& token = tokens[i];
if (token.m_type != Cpp::Token::Type::Identifier)
continue;
auto text = text_of_token(lines, token);
if (text.starts_with(partial_input) && suggestions_lookup.set(text) == AK::HashSetResult::InsertedNewEntry) {
suggestions.append({ text, partial_input.length(), GUI::AutocompleteProvider::CompletionKind::Identifier });
}
}
return suggestions;
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/String.h>
#include <AK/Vector.h>
#include <DevTools/HackStudio/AutoCompleteResponse.h>
#include <LibCpp/Lexer.h>
#include <LibGUI/TextPosition.h>
namespace LanguageServers::Cpp {
using namespace ::Cpp;
class AutoComplete {
public:
AutoComplete() = delete;
static Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, const GUI::TextPosition& autocomplete_position);
private:
static Optional<size_t> token_in_position(const Vector<Cpp::Token>&, const GUI::TextPosition&);
static StringView text_of_token(const Vector<String>& lines, const Cpp::Token&);
static Vector<GUI::AutocompleteProvider::Entry> identifier_prefixes(const Vector<String>& lines, const Vector<Cpp::Token>&, size_t target_token_index);
};
}

View file

@ -0,0 +1,15 @@
set(SOURCES
ClientConnection.cpp
main.cpp
AutoComplete.cpp
)
set(GENERATED_SOURCES
../LanguageServerEndpoint.h
../LanguageClientEndpoint.h)
serenity_bin(CppLanguageServer)
# We link with LibGUI because we use GUI::TextDocument to update
# the content of files according to the edit actions we receive over IPC.
target_link_libraries(CppLanguageServer LibIPC LibCpp LibGUI)

View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "ClientConnection.h"
#include "AutoComplete.h"
#include <AK/HashMap.h>
#include <LibCore/File.h>
#include <LibGUI/TextDocument.h>
// #define DEBUG_CPP_LANGUAGE_SERVER
// #define DEBUG_FILE_CONTENT
namespace LanguageServers::Cpp {
static HashMap<int, RefPtr<ClientConnection>> s_connections;
ClientConnection::ClientConnection(NonnullRefPtr<Core::LocalSocket> socket, int client_id)
: IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, move(socket), client_id)
{
s_connections.set(client_id, *this);
}
ClientConnection::~ClientConnection()
{
}
void ClientConnection::die()
{
s_connections.remove(client_id());
exit(0);
}
OwnPtr<Messages::LanguageServer::GreetResponse> ClientConnection::handle(const Messages::LanguageServer::Greet&)
{
return make<Messages::LanguageServer::GreetResponse>(client_id());
}
class DefaultDocumentClient final : public GUI::TextDocument::Client {
public:
virtual ~DefaultDocumentClient() override = default;
virtual void document_did_append_line() override {};
virtual void document_did_insert_line(size_t) override {};
virtual void document_did_remove_line(size_t) override {};
virtual void document_did_remove_all_lines() override {};
virtual void document_did_change() override {};
virtual void document_did_set_text() override {};
virtual void document_did_set_cursor(const GUI::TextPosition&) override {};
virtual bool is_automatic_indentation_enabled() const override { return false; }
virtual int soft_tab_width() const override { return 4; }
};
static DefaultDocumentClient s_default_document_client;
void ClientConnection::handle(const Messages::LanguageServer::FileOpened& message)
{
auto file = Core::File::construct(this);
if (!file->open(message.file().fd(), Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes)) {
errno = file->error();
perror("open");
dbgln("Failed to open project file");
return;
}
auto content = file->read_all();
StringView content_view(content);
auto document = GUI::TextDocument::create(&s_default_document_client);
document->set_text(content_view);
m_open_files.set(message.file_name(), document);
#ifdef DEBUG_FILE_CONTENT
dbg() << document->text();
#endif
}
void ClientConnection::handle(const Messages::LanguageServer::FileEditInsertText& message)
{
#ifdef DEBUG_CPP_LANGUAGE_SERVER
dbgln("InsertText for file: {}", message.file_name());
dbgln("Text: {}", message.text());
dbgln("[{}:{}]", message.start_line(), message.start_column());
#endif
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
document->insert_at(start_position, message.text(), &s_default_document_client);
#ifdef DEBUG_FILE_CONTENT
dbgln("{}", document->text());
#endif
}
void ClientConnection::handle(const Messages::LanguageServer::FileEditRemoveText& message)
{
#ifdef DEBUG_CPP_LANGUAGE_SERVER
dbgln("RemoveText for file: {}", message.file_name());
dbgln("[{}:{} - {}:{}]", message.start_line(), message.start_column(), message.end_line(), message.end_column());
#endif
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
GUI::TextRange range {
GUI::TextPosition { (size_t)message.start_line(),
(size_t)message.start_column() },
GUI::TextPosition { (size_t)message.end_line(),
(size_t)message.end_column() }
};
document->remove(range);
#ifdef DEBUG_FILE_CONTENT
dbgln("{}", document->text());
#endif
}
void ClientConnection::handle(const Messages::LanguageServer::AutoCompleteSuggestions& message)
{
#ifdef DEBUG_CPP_LANGUAGE_SERVER
dbgln("AutoCompleteSuggestions for: {} {}:{}", message.file_name(), message.cursor_line(), message.cursor_column());
#endif
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
auto suggestions = AutoComplete::get_suggestions(document->text(), { (size_t)message.cursor_line(), (size_t)max(message.cursor_column(), message.cursor_column() - 1) });
post_message(Messages::LanguageClient::AutoCompleteSuggestions(move(suggestions)));
}
RefPtr<GUI::TextDocument> ClientConnection::document_for(const String& file_name)
{
auto document_optional = m_open_files.get(file_name);
if (!document_optional.has_value())
return nullptr;
return document_optional.value();
}
void ClientConnection::handle(const Messages::LanguageServer::SetFileContent& message)
{
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
auto content = message.content();
document->set_text(content.view());
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/LexicalPath.h>
#include <DevTools/HackStudio/AutoCompleteResponse.h>
#include <LibGUI/TextDocument.h>
#include <LibIPC/ClientConnection.h>
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
namespace LanguageServers::Cpp {
class ClientConnection final
: public IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>
, public LanguageServerEndpoint {
C_OBJECT(ClientConnection);
public:
explicit ClientConnection(NonnullRefPtr<Core::LocalSocket>, int client_id);
~ClientConnection() override;
virtual void die() override;
private:
virtual OwnPtr<Messages::LanguageServer::GreetResponse> handle(const Messages::LanguageServer::Greet&) override;
virtual void handle(const Messages::LanguageServer::FileOpened&) override;
virtual void handle(const Messages::LanguageServer::FileEditInsertText&) override;
virtual void handle(const Messages::LanguageServer::FileEditRemoveText&) override;
virtual void handle(const Messages::LanguageServer::SetFileContent&) override;
virtual void handle(const Messages::LanguageServer::AutoCompleteSuggestions&) override;
RefPtr<GUI::TextDocument> document_for(const String& file_name);
HashMap<String, NonnullRefPtr<GUI::TextDocument>> m_open_files;
};
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <AK/LexicalPath.h>
#include <DevTools/HackStudio/LanguageServers/Cpp/ClientConnection.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/LocalServer.h>
#include <LibIPC/ClientConnection.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int, char**)
{
Core::EventLoop event_loop;
if (pledge("stdio unix recvfd", nullptr) < 0) {
perror("pledge");
return 1;
}
auto socket = Core::LocalSocket::take_over_accepted_socket_from_system_server();
IPC::new_client_connection<LanguageServers::Cpp::ClientConnection>(socket.release_nonnull(), 1);
if (pledge("stdio recvfd", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil(nullptr, nullptr) < 0) {
perror("unveil");
return 1;
}
return event_loop.exec();
}

View file

@ -0,0 +1,4 @@
endpoint LanguageClient = 8002
{
AutoCompleteSuggestions(Vector<GUI::AutocompleteProvider::Entry> suggestions) =|
}

View file

@ -0,0 +1,11 @@
endpoint LanguageServer = 8001
{
Greet() => (i32 client_id)
FileOpened(String file_name, IPC::File file) =|
FileEditInsertText(String file_name, String text, i32 start_line, i32 start_column) =|
FileEditRemoveText(String file_name, i32 start_line, i32 start_column, i32 end_line, i32 end_column) =|
SetFileContent(String file_name, String content) =|
AutoCompleteSuggestions(String file_name, i32 cursor_line, i32 cursor_column) =|
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "AutoComplete.h"
#include <AK/HashTable.h>
#include <LibLine/SuggestionManager.h>
#include <Shell/AST.h>
#include <Shell/Parser.h>
#include <Shell/Shell.h>
// #define DEBUG_AUTOCOMPLETE
namespace LanguageServers::Shell {
Vector<GUI::AutocompleteProvider::Entry> AutoComplete::get_suggestions(const String& code, size_t offset)
{
// FIXME: No need to reparse this every time!
auto ast = ::Shell::Parser { code }.parse();
if (!ast)
return {};
#ifdef DEBUG_AUTOCOMPLETE
dbgln("Complete '{}'", code);
ast->dump(1);
dbgln("At offset {}", offset);
#endif
auto result = ast->complete_for_editor(m_shell, offset);
Vector<GUI::AutocompleteProvider::Entry> completions;
for (auto& entry : result) {
#ifdef DEBUG_AUTOCOMPLETE
dbgln("Suggestion: '{}' starting at {}", entry.text_string, entry.input_offset);
#endif
completions.append({ entry.text_string, entry.input_offset });
}
return completions;
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/String.h>
#include <AK/Vector.h>
#include <DevTools/HackStudio/AutoCompleteResponse.h>
#include <LibGUI/TextPosition.h>
#include <Shell/Shell.h>
namespace LanguageServers::Shell {
class AutoComplete {
public:
AutoComplete()
: m_shell(::Shell::Shell::construct())
{
}
Vector<GUI::AutocompleteProvider::Entry> get_suggestions(const String& code, size_t autocomplete_position);
private:
NonnullRefPtr<::Shell::Shell> m_shell;
};
}

View file

@ -0,0 +1,15 @@
set(SOURCES
ClientConnection.cpp
main.cpp
AutoComplete.cpp
)
set(GENERATED_SOURCES
../LanguageServerEndpoint.h
../LanguageClientEndpoint.h)
serenity_bin(ShellLanguageServer)
# We link with LibGUI because we use GUI::TextDocument to update
# the content of files according to the edit actions we receive over IPC.
target_link_libraries(ShellLanguageServer LibIPC LibShell LibGUI)

View file

@ -0,0 +1,186 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "ClientConnection.h"
#include "AutoComplete.h"
#include <AK/HashMap.h>
#include <LibCore/File.h>
#include <LibGUI/TextDocument.h>
// #define DEBUG_SH_LANGUAGE_SERVER
// #define DEBUG_FILE_CONTENT
namespace LanguageServers::Shell {
static HashMap<int, RefPtr<ClientConnection>> s_connections;
ClientConnection::ClientConnection(NonnullRefPtr<Core::LocalSocket> socket, int client_id)
: IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>(*this, move(socket), client_id)
{
s_connections.set(client_id, *this);
}
ClientConnection::~ClientConnection()
{
}
void ClientConnection::die()
{
s_connections.remove(client_id());
exit(0);
}
OwnPtr<Messages::LanguageServer::GreetResponse> ClientConnection::handle(const Messages::LanguageServer::Greet&)
{
return make<Messages::LanguageServer::GreetResponse>(client_id());
}
class DefaultDocumentClient final : public GUI::TextDocument::Client {
public:
virtual ~DefaultDocumentClient() override = default;
virtual void document_did_append_line() override {};
virtual void document_did_insert_line(size_t) override {};
virtual void document_did_remove_line(size_t) override {};
virtual void document_did_remove_all_lines() override {};
virtual void document_did_change() override {};
virtual void document_did_set_text() override {};
virtual void document_did_set_cursor(const GUI::TextPosition&) override {};
virtual bool is_automatic_indentation_enabled() const override { return false; }
virtual int soft_tab_width() const override { return 4; }
};
static DefaultDocumentClient s_default_document_client;
void ClientConnection::handle(const Messages::LanguageServer::FileOpened& message)
{
auto file = Core::File::construct(this);
if (!file->open(message.file().fd(), Core::IODevice::ReadOnly, Core::File::ShouldCloseFileDescriptor::Yes)) {
errno = file->error();
perror("open");
dbgln("Failed to open project file");
return;
}
auto content = file->read_all();
StringView content_view(content);
auto document = GUI::TextDocument::create(&s_default_document_client);
document->set_text(content_view);
m_open_files.set(message.file_name(), document);
#ifdef DEBUG_FILE_CONTENT
dbg() << document->text();
#endif
}
void ClientConnection::handle(const Messages::LanguageServer::FileEditInsertText& message)
{
#ifdef DEBUG_SH_LANGUAGE_SERVER
dbgln("InsertText for file: {}", message.file_name());
dbgln("Text: {}", message.text());
dbgln("[{}:{}]", message.start_line(), message.start_column());
#endif
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
document->insert_at(start_position, message.text(), &s_default_document_client);
#ifdef DEBUG_FILE_CONTENT
dbgln("{}", document->text());
#endif
}
void ClientConnection::handle(const Messages::LanguageServer::FileEditRemoveText& message)
{
#ifdef DEBUG_SH_LANGUAGE_SERVER
dbgln("RemoveText for file: {}", message.file_name());
dbgln("[{}:{} - {}:{}]", message.start_line(), message.start_column(), message.end_line(), message.end_column());
#endif
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
GUI::TextPosition start_position { (size_t)message.start_line(), (size_t)message.start_column() };
GUI::TextRange range {
GUI::TextPosition { (size_t)message.start_line(),
(size_t)message.start_column() },
GUI::TextPosition { (size_t)message.end_line(),
(size_t)message.end_column() }
};
document->remove(range);
#ifdef DEBUG_FILE_CONTENT
dbg() << document->text();
#endif
}
void ClientConnection::handle(const Messages::LanguageServer::AutoCompleteSuggestions& message)
{
#ifdef DEBUG_SH_LANGUAGE_SERVER
dbgln("AutoCompleteSuggestions for: {} {}:{}", message.file_name(), message.cursor_line(), message.cursor_column());
#endif
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
auto& lines = document->lines();
size_t offset = 0;
if (message.cursor_line() > 0) {
for (auto i = 0; i < message.cursor_line(); ++i)
offset += lines[i].length() + 1;
}
offset += message.cursor_column();
auto suggestions = m_autocomplete.get_suggestions(document->text(), offset);
post_message(Messages::LanguageClient::AutoCompleteSuggestions(move(suggestions)));
}
RefPtr<GUI::TextDocument> ClientConnection::document_for(const String& file_name)
{
auto document_optional = m_open_files.get(file_name);
if (!document_optional.has_value())
return nullptr;
return document_optional.value();
}
void ClientConnection::handle(const Messages::LanguageServer::SetFileContent& message)
{
auto document = document_for(message.file_name());
if (!document) {
dbgln("file {} has not been opened", message.file_name());
return;
}
auto content = message.content();
document->set_text(content.view());
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "AutoComplete.h"
#include <AK/HashMap.h>
#include <AK/LexicalPath.h>
#include <DevTools/HackStudio/AutoCompleteResponse.h>
#include <LibGUI/TextDocument.h>
#include <LibIPC/ClientConnection.h>
#include <DevTools/HackStudio/LanguageServers/LanguageClientEndpoint.h>
#include <DevTools/HackStudio/LanguageServers/LanguageServerEndpoint.h>
namespace LanguageServers::Shell {
class ClientConnection final
: public IPC::ClientConnection<LanguageClientEndpoint, LanguageServerEndpoint>
, public LanguageServerEndpoint {
C_OBJECT(ClientConnection);
public:
explicit ClientConnection(NonnullRefPtr<Core::LocalSocket>, int client_id);
~ClientConnection() override;
virtual void die() override;
private:
virtual OwnPtr<Messages::LanguageServer::GreetResponse> handle(const Messages::LanguageServer::Greet&) override;
virtual void handle(const Messages::LanguageServer::FileOpened&) override;
virtual void handle(const Messages::LanguageServer::FileEditInsertText&) override;
virtual void handle(const Messages::LanguageServer::FileEditRemoveText&) override;
virtual void handle(const Messages::LanguageServer::SetFileContent&) override;
virtual void handle(const Messages::LanguageServer::AutoCompleteSuggestions&) override;
RefPtr<GUI::TextDocument> document_for(const String& file_name);
HashMap<String, NonnullRefPtr<GUI::TextDocument>> m_open_files;
AutoComplete m_autocomplete;
};
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2020, the SerenityOS developers.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <AK/LexicalPath.h>
#include <DevTools/HackStudio/LanguageServers/Shell/ClientConnection.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/LocalServer.h>
#include <LibIPC/ClientConnection.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int, char**)
{
Core::EventLoop event_loop;
if (pledge("stdio unix rpath recvfd", nullptr) < 0) {
perror("pledge");
return 1;
}
auto socket = Core::LocalSocket::take_over_accepted_socket_from_system_server();
IPC::new_client_connection<LanguageServers::Shell::ClientConnection>(socket.release_nonnull(), 1);
if (pledge("stdio rpath recvfd", nullptr) < 0) {
perror("pledge");
return 1;
}
if (unveil("/etc/passwd", "r") < 0) {
perror("unveil");
return 1;
}
if (unveil("/", "b") < 0) {
perror("unveil");
return 1;
}
if (unveil(nullptr, nullptr) < 0) {
perror("unveil");
return 1;
}
return event_loop.exec();
}

View file

@ -0,0 +1,178 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Locator.h"
#include "HackStudio.h"
#include "Project.h"
#include <LibGUI/BoxLayout.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/TableView.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/Window.h>
namespace HackStudio {
class LocatorSuggestionModel final : public GUI::Model {
public:
explicit LocatorSuggestionModel(Vector<String>&& suggestions)
: m_suggestions(move(suggestions))
{
}
enum Column {
Icon,
Name,
__Column_Count,
};
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return m_suggestions.size(); }
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return Column::__Column_Count; }
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
{
auto& suggestion = m_suggestions.at(index.row());
if (role == GUI::ModelRole::Display) {
if (index.column() == Column::Name)
return suggestion;
if (index.column() == Column::Icon)
return GUI::FileIconProvider::icon_for_path(suggestion);
}
return {};
}
virtual void update() override {};
private:
Vector<String> m_suggestions;
};
Locator::Locator()
{
set_layout<GUI::VerticalBoxLayout>();
set_fixed_height(20);
m_textbox = add<GUI::TextBox>();
m_textbox->on_change = [this] {
update_suggestions();
};
m_textbox->on_escape_pressed = [this] {
m_popup_window->hide();
};
m_textbox->on_up_pressed = [this] {
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() - 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_valid(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
};
m_textbox->on_down_pressed = [this] {
GUI::ModelIndex new_index = m_suggestion_view->selection().first();
if (new_index.is_valid())
new_index = m_suggestion_view->model()->index(new_index.row() + 1);
else
new_index = m_suggestion_view->model()->index(0);
if (m_suggestion_view->model()->is_valid(new_index)) {
m_suggestion_view->selection().set(new_index);
m_suggestion_view->scroll_into_view(new_index, Orientation::Vertical);
}
};
m_textbox->on_return_pressed = [this] {
auto selected_index = m_suggestion_view->selection().first();
if (!selected_index.is_valid())
return;
open_suggestion(selected_index);
};
m_popup_window = GUI::Window::construct();
// FIXME: This is obviously not a tooltip window, but it's the closest thing to what we want atm.
m_popup_window->set_window_type(GUI::WindowType::Tooltip);
m_popup_window->set_rect(0, 0, 500, 200);
m_suggestion_view = m_popup_window->set_main_widget<GUI::TableView>();
m_suggestion_view->set_column_headers_visible(false);
m_suggestion_view->on_activation = [this](auto& index) {
open_suggestion(index);
};
}
Locator::~Locator()
{
}
void Locator::open_suggestion(const GUI::ModelIndex& index)
{
auto filename_index = m_suggestion_view->model()->index(index.row(), LocatorSuggestionModel::Column::Name);
auto filename = filename_index.data().to_string();
open_file(filename);
close();
}
void Locator::open()
{
m_textbox->set_focus(true);
if (!m_textbox->text().is_empty()) {
m_textbox->select_all();
m_popup_window->show();
}
}
void Locator::close()
{
m_popup_window->hide();
}
void Locator::update_suggestions()
{
auto typed_text = m_textbox->text();
Vector<String> suggestions;
project().for_each_text_file([&](auto& file) {
if (file.name().contains(typed_text, CaseSensitivity::CaseInsensitive))
suggestions.append(file.name());
});
dbgln("I have {} suggestion(s):", suggestions.size());
for (auto& s : suggestions) {
dbgln(" {}", s);
}
bool has_suggestions = !suggestions.is_empty();
m_suggestion_view->set_model(adopt(*new LocatorSuggestionModel(move(suggestions))));
if (!has_suggestions)
m_suggestion_view->selection().clear();
else
m_suggestion_view->selection().set(m_suggestion_view->model()->index(0));
m_popup_window->move_to(screen_relative_rect().top_left().translated(0, -m_popup_window->height()));
dbgln("Popup rect: {}", m_popup_window->rect());
m_popup_window->show();
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibGUI/Widget.h>
namespace HackStudio {
class Locator final : public GUI::Widget {
C_OBJECT(Locator)
public:
virtual ~Locator() override;
void open();
void close();
private:
void update_suggestions();
void open_suggestion(const GUI::ModelIndex&);
Locator();
RefPtr<GUI::TextBox> m_textbox;
RefPtr<GUI::Window> m_popup_window;
RefPtr<GUI::TableView> m_suggestion_view;
};
}

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Project.h"
#include "HackStudio.h"
#include <LibCore/File.h>
namespace HackStudio {
Project::Project(const String& root_path)
: m_root_path(root_path)
{
m_model = GUI::FileSystemModel::create(root_path, GUI::FileSystemModel::Mode::FilesAndDirectories);
}
Project::~Project()
{
}
OwnPtr<Project> Project::open_with_root_path(const String& root_path)
{
if (!Core::File::is_directory(root_path))
return {};
return adopt_own(*new Project(root_path));
}
template<typename Callback>
static void traverse_model(const GUI::FileSystemModel& model, const GUI::ModelIndex& index, Callback callback)
{
if (index.is_valid())
callback(index);
auto row_count = model.row_count(index);
if (!row_count)
return;
for (int row = 0; row < row_count; ++row) {
auto child_index = model.index(row, GUI::FileSystemModel::Column::Name, index);
traverse_model(model, child_index, callback);
}
}
void Project::for_each_text_file(Function<void(const ProjectFile&)> callback) const
{
traverse_model(model(), {}, [&](auto& index) {
auto file = get_file(model().full_path(index));
if (file)
callback(*file);
});
}
RefPtr<ProjectFile> Project::get_file(const String& path) const
{
for (auto& file : m_files) {
if (file.name() == path)
return file;
}
auto file = ProjectFile::construct_with_name(path);
m_files.append(file);
return file;
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "ProjectFile.h"
#include <AK/LexicalPath.h>
#include <AK/Noncopyable.h>
#include <AK/OwnPtr.h>
#include <LibGUI/FileSystemModel.h>
namespace HackStudio {
class Project {
AK_MAKE_NONCOPYABLE(Project);
AK_MAKE_NONMOVABLE(Project);
public:
~Project();
static OwnPtr<Project> open_with_root_path(const String& root_path);
GUI::FileSystemModel& model() { return *m_model; }
const GUI::FileSystemModel& model() const { return *m_model; }
String name() const { return LexicalPath(m_root_path).basename(); }
String root_path() const { return m_root_path; }
RefPtr<ProjectFile> get_file(const String& path) const;
void for_each_text_file(Function<void(const ProjectFile&)>) const;
private:
explicit Project(const String& root_path);
RefPtr<GUI::FileSystemModel> m_model;
mutable NonnullRefPtrVector<ProjectFile> m_files;
String m_root_path;
};
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "ProjectFile.h"
#include <LibCore/File.h>
#include <string.h>
namespace HackStudio {
ProjectFile::ProjectFile(const String& name)
: m_name(name)
{
}
GUI::TextDocument& ProjectFile::document() const
{
if (!m_document) {
m_document = CodeDocument::create(m_name);
auto file_or_error = Core::File::open(m_name, Core::File::ReadOnly);
if (file_or_error.is_error()) {
warnln("Couldn't open '{}': {}", m_name, file_or_error.error());
// This is okay though, we'll just go with an empty document and create the file when saving.
} else {
auto& file = *file_or_error.value();
m_document->set_text(file.read_all());
}
}
return *m_document;
}
int ProjectFile::vertical_scroll_value() const
{
return m_vertical_scroll_value;
}
void ProjectFile::vertical_scroll_value(int vertical_scroll_value)
{
m_vertical_scroll_value = vertical_scroll_value;
}
int ProjectFile::horizontal_scroll_value() const
{
return m_horizontal_scroll_value;
}
void ProjectFile::horizontal_scroll_value(int horizontal_scroll_value)
{
m_horizontal_scroll_value = horizontal_scroll_value;
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "CodeDocument.h"
#include <AK/ByteBuffer.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
#include <AK/String.h>
namespace HackStudio {
class ProjectFile : public RefCounted<ProjectFile> {
public:
static NonnullRefPtr<ProjectFile> construct_with_name(const String& name)
{
return adopt(*new ProjectFile(name));
}
const String& name() const { return m_name; }
GUI::TextDocument& document() const;
int vertical_scroll_value() const;
void vertical_scroll_value(int);
int horizontal_scroll_value() const;
void horizontal_scroll_value(int);
private:
explicit ProjectFile(const String& name);
String m_name;
mutable RefPtr<CodeDocument> m_document;
int m_vertical_scroll_value { 0 };
int m_horizontal_scroll_value { 0 };
};
}

View file

@ -0,0 +1,186 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "TerminalWrapper.h"
#include <AK/String.h>
#include <LibCore/ConfigFile.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/MessageBox.h>
#include <LibVT/TerminalWidget.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
namespace HackStudio {
void TerminalWrapper::run_command(const String& command)
{
if (m_pid != -1) {
GUI::MessageBox::show(window(),
"A command is already running in this TerminalWrapper",
"Can't run command",
GUI::MessageBox::Type::Error);
return;
}
int ptm_fd = posix_openpt(O_RDWR | O_CLOEXEC);
if (ptm_fd < 0) {
perror("posix_openpt");
ASSERT_NOT_REACHED();
}
if (grantpt(ptm_fd) < 0) {
perror("grantpt");
ASSERT_NOT_REACHED();
}
if (unlockpt(ptm_fd) < 0) {
perror("unlockpt");
ASSERT_NOT_REACHED();
}
m_terminal_widget->set_pty_master_fd(ptm_fd);
m_terminal_widget->on_command_exit = [this] {
int wstatus;
int rc = waitpid(m_pid, &wstatus, 0);
if (rc < 0) {
perror("waitpid");
ASSERT_NOT_REACHED();
}
if (WIFEXITED(wstatus)) {
m_terminal_widget->inject_string(String::formatted("\033[{};1m(Command exited with code {})\033[0m\n", wstatus == 0 ? 32 : 31, WEXITSTATUS(wstatus)));
} else if (WIFSTOPPED(wstatus)) {
m_terminal_widget->inject_string("\033[34;1m(Command stopped!)\033[0m\n");
} else if (WIFSIGNALED(wstatus)) {
m_terminal_widget->inject_string(String::formatted("\033[34;1m(Command signaled with {}!)\033[0m\n", strsignal(WTERMSIG(wstatus))));
}
m_pid = -1;
if (on_command_exit)
on_command_exit();
};
m_pid = fork();
if (m_pid == 0) {
// Create a new process group.
setsid();
const char* tty_name = ptsname(ptm_fd);
if (!tty_name) {
perror("ptsname");
exit(1);
}
close(ptm_fd);
int pts_fd = open(tty_name, O_RDWR);
if (pts_fd < 0) {
perror("open");
exit(1);
}
tcsetpgrp(pts_fd, getpid());
// NOTE: It's okay if this fails.
int rc = ioctl(0, TIOCNOTTY);
close(0);
close(1);
close(2);
rc = dup2(pts_fd, 0);
if (rc < 0) {
perror("dup2");
exit(1);
}
rc = dup2(pts_fd, 1);
if (rc < 0) {
perror("dup2");
exit(1);
}
rc = dup2(pts_fd, 2);
if (rc < 0) {
perror("dup2");
exit(1);
}
rc = close(pts_fd);
if (rc < 0) {
perror("close");
exit(1);
}
rc = ioctl(0, TIOCSCTTY);
if (rc < 0) {
perror("ioctl(TIOCSCTTY)");
exit(1);
}
setenv("TERM", "xterm", true);
auto parts = command.split(' ');
ASSERT(!parts.is_empty());
const char** args = (const char**)calloc(parts.size() + 1, sizeof(const char*));
for (size_t i = 0; i < parts.size(); i++) {
args[i] = parts[i].characters();
}
rc = execvp(args[0], const_cast<char**>(args));
if (rc < 0) {
perror("execve");
exit(1);
}
ASSERT_NOT_REACHED();
}
// (In parent process)
terminal().scroll_to_bottom();
}
void TerminalWrapper::kill_running_command()
{
ASSERT(m_pid != -1);
// Kill our child process and its whole process group.
[[maybe_unused]] auto rc = killpg(m_pid, SIGTERM);
}
TerminalWrapper::TerminalWrapper(bool user_spawned)
: m_user_spawned(user_spawned)
{
set_layout<GUI::VerticalBoxLayout>();
RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("Terminal");
m_terminal_widget = add<TerminalWidget>(-1, false, config);
if (user_spawned)
run_command("Shell");
}
TerminalWrapper::~TerminalWrapper()
{
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibGUI/Widget.h>
class TerminalWidget;
namespace HackStudio {
class TerminalWrapper final : public GUI::Widget {
C_OBJECT(TerminalWrapper)
public:
virtual ~TerminalWrapper() override;
void run_command(const String&);
void kill_running_command();
bool user_spawned() const { return m_user_spawned; }
TerminalWidget& terminal() { return *m_terminal_widget; }
Function<void()> on_command_exit;
private:
explicit TerminalWrapper(bool user_spawned = true);
RefPtr<TerminalWidget> m_terminal_widget;
pid_t m_pid { -1 };
bool m_user_spawned { true };
};
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/Noncopyable.h>
#include <LibGUI/Forward.h>
namespace HackStudio {
class FormEditorWidget;
class Tool {
AK_MAKE_NONCOPYABLE(Tool);
AK_MAKE_NONMOVABLE(Tool);
public:
virtual ~Tool() { }
virtual void on_mousedown(GUI::MouseEvent&) = 0;
virtual void on_mouseup(GUI::MouseEvent&) = 0;
virtual void on_mousemove(GUI::MouseEvent&) = 0;
virtual void on_keydown(GUI::KeyEvent&) = 0;
virtual void on_second_paint(GUI::Painter&, GUI::PaintEvent&) { }
virtual const char* class_name() const = 0;
virtual void attach() { }
virtual void detach() { }
protected:
explicit Tool(FormEditorWidget& editor)
: m_editor(editor)
{
}
FormEditorWidget& m_editor;
};
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "WidgetTool.h"
#include <AK/LogStream.h>
namespace HackStudio {
void WidgetTool::on_mousedown([[maybe_unused]] GUI::MouseEvent& event)
{
dbgln("WidgetTool::on_mousedown");
}
void WidgetTool::on_mouseup([[maybe_unused]] GUI::MouseEvent& event)
{
dbgln("WidgetTool::on_mouseup");
}
void WidgetTool::on_mousemove([[maybe_unused]] GUI::MouseEvent& event)
{
dbgln("WidgetTool::on_mousemove");
}
void WidgetTool::on_keydown([[maybe_unused]] GUI::KeyEvent& event)
{
dbgln("WidgetTool::on_keydown");
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include "Tool.h"
namespace HackStudio {
class WidgetTool final : public Tool {
public:
explicit WidgetTool(FormEditorWidget& editor, const GUI::WidgetClassRegistration& meta_class)
: Tool(editor)
, m_meta_class(meta_class)
{
}
virtual ~WidgetTool() override { }
private:
virtual const char* class_name() const override { return "WidgetTool"; }
virtual void on_mousedown(GUI::MouseEvent&) override;
virtual void on_mouseup(GUI::MouseEvent&) override;
virtual void on_mousemove(GUI::MouseEvent&) override;
virtual void on_keydown(GUI::KeyEvent&) override;
const GUI::WidgetClassRegistration& m_meta_class;
};
}

View file

@ -0,0 +1,117 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "WidgetTreeModel.h"
#include <AK/StringBuilder.h>
#include <LibGUI/Widget.h>
namespace HackStudio {
WidgetTreeModel::WidgetTreeModel(GUI::Widget& root)
: m_root(root)
{
m_widget_icon.set_bitmap_for_size(16, Gfx::Bitmap::load_from_file("/res/icons/16x16/inspector-object.png"));
}
WidgetTreeModel::~WidgetTreeModel()
{
}
GUI::ModelIndex WidgetTreeModel::index(int row, int column, const GUI::ModelIndex& parent) const
{
if (!parent.is_valid()) {
return create_index(row, column, m_root.ptr());
}
auto& parent_node = *static_cast<GUI::Widget*>(parent.internal_data());
return create_index(row, column, parent_node.child_widgets().at(row));
}
GUI::ModelIndex WidgetTreeModel::parent_index(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return {};
auto& widget = *static_cast<GUI::Widget*>(index.internal_data());
if (&widget == m_root.ptr())
return {};
if (widget.parent_widget() == m_root.ptr())
return create_index(0, 0, m_root.ptr());
// Walk the grandparent's children to find the index of widget's parent in its parent.
// (This is needed to produce the row number of the GUI::ModelIndex corresponding to widget's parent.)
int grandparent_child_index = 0;
for (auto& grandparent_child : widget.parent_widget()->parent_widget()->child_widgets()) {
if (grandparent_child == widget.parent_widget())
return create_index(grandparent_child_index, 0, widget.parent_widget());
++grandparent_child_index;
}
ASSERT_NOT_REACHED();
return {};
}
int WidgetTreeModel::row_count(const GUI::ModelIndex& index) const
{
if (!index.is_valid())
return 1;
auto& widget = *static_cast<GUI::Widget*>(index.internal_data());
return widget.child_widgets().size();
}
int WidgetTreeModel::column_count(const GUI::ModelIndex&) const
{
return 1;
}
GUI::Variant WidgetTreeModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
{
auto* widget = static_cast<GUI::Widget*>(index.internal_data());
if (role == GUI::ModelRole::Icon) {
return m_widget_icon;
}
if (role == GUI::ModelRole::Display) {
return String::formatted("{} ({})", widget->class_name(), widget->relative_rect());
}
return {};
}
void WidgetTreeModel::update()
{
did_update();
}
GUI::ModelIndex WidgetTreeModel::index_for_widget(GUI::Widget& widget) const
{
int parent_child_index = 0;
for (auto& parent_child : widget.parent_widget()->child_widgets()) {
if (parent_child == &widget)
return create_index(parent_child_index, 0, &widget);
++parent_child_index;
}
return {};
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
namespace HackStudio {
class WidgetTreeModel final : public GUI::Model {
public:
static NonnullRefPtr<WidgetTreeModel> create(GUI::Widget& root) { return adopt(*new WidgetTreeModel(root)); }
virtual ~WidgetTreeModel() override;
virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override;
virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override;
virtual void update() override;
GUI::ModelIndex index_for_widget(GUI::Widget&) const;
private:
explicit WidgetTreeModel(GUI::Widget&);
NonnullRefPtr<GUI::Widget> m_root;
GUI::Icon m_widget_icon;
};
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "HackStudio.h"
#include "HackStudioWidget.h"
#include "Project.h"
#include <AK/StringBuilder.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/Event.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibGUI/Application.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibThread/Lock.h>
#include <LibThread/Thread.h>
#include <LibVT/TerminalWidget.h>
#include <fcntl.h>
#include <spawn.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace HackStudio;
static RefPtr<GUI::Window> s_window;
static RefPtr<HackStudioWidget> s_hack_studio_widget;
static bool make_is_available();
static void update_path_environment_variable();
int main(int argc, char** argv)
{
if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec unix fattr thread unix sendfd ptrace", nullptr) < 0) {
perror("pledge");
return 1;
}
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio tty accept rpath cpath wpath shared_buffer proc exec fattr thread unix sendfd ptrace", nullptr) < 0) {
perror("pledge");
return 1;
}
s_window = GUI::Window::construct();
s_window->resize(840, 600);
s_window->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-hack-studio.png"));
update_path_environment_variable();
if (!make_is_available())
GUI::MessageBox::show(s_window, "The 'make' command is not available. You probably want to install the binutils, gcc, and make ports from the root of the Serenity repository.", "Error", GUI::MessageBox::Type::Error);
const char* path_argument = nullptr;
Core::ArgsParser args_parser;
args_parser.add_positional_argument(path_argument, "Path to a workspace or a file", "path", Core::ArgsParser::Required::No);
args_parser.parse(argc, argv);
auto argument_absolute_path = Core::File::real_path_for(path_argument);
auto project_path = argument_absolute_path;
if (argument_absolute_path.is_null())
project_path = Core::File::real_path_for(".");
s_hack_studio_widget = s_window->set_main_widget<HackStudioWidget>(project_path);
s_window->set_title(String::formatted("{} - Hack Studio", s_hack_studio_widget->project().name()));
auto menubar = GUI::MenuBar::construct();
s_hack_studio_widget->initialize_menubar(menubar);
app->set_menubar(menubar);
s_window->show();
s_hack_studio_widget->update_actions();
return app->exec();
}
static bool make_is_available()
{
pid_t pid;
const char* argv[] = { "make", "--version", nullptr };
posix_spawn_file_actions_t action;
posix_spawn_file_actions_init(&action);
posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY, 0);
if ((errno = posix_spawnp(&pid, "make", &action, nullptr, const_cast<char**>(argv), environ))) {
perror("posix_spawn");
return false;
}
int wstatus;
waitpid(pid, &wstatus, 0);
posix_spawn_file_actions_destroy(&action);
return WEXITSTATUS(wstatus) == 0;
}
static void update_path_environment_variable()
{
StringBuilder path;
path.append(getenv("PATH"));
if (path.length())
path.append(":");
path.append("/bin:/usr/bin:/usr/local/bin");
setenv("PATH", path.to_string().characters(), true);
}
namespace HackStudio {
GUI::TextEditor& current_editor()
{
return s_hack_studio_widget->current_editor();
}
void open_file(const String& file_name)
{
return s_hack_studio_widget->open_file(file_name);
}
RefPtr<EditorWrapper> current_editor_wrapper()
{
if (!s_hack_studio_widget)
return nullptr;
return s_hack_studio_widget->current_editor_wrapper();
}
Project& project()
{
return s_hack_studio_widget->project();
}
String currently_open_file()
{
if (!s_hack_studio_widget)
return {};
return s_hack_studio_widget->currently_open_file();
}
void set_current_editor_wrapper(RefPtr<EditorWrapper> wrapper)
{
s_hack_studio_widget->set_current_editor_wrapper(wrapper);
}
}